@djangocfg/layouts 1.4.28 → 1.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +8 -8
  2. package/src/auth/context/AuthContext.tsx +4 -1
  3. package/src/auth/hooks/index.ts +2 -0
  4. package/src/auth/hooks/useAuthForm.ts +2 -0
  5. package/src/auth/hooks/useAuthGuard.ts +2 -0
  6. package/src/auth/hooks/useAutoAuth.ts +16 -11
  7. package/src/auth/hooks/useLocalStorage.ts +2 -0
  8. package/src/auth/hooks/useProfileCache.ts +2 -0
  9. package/src/auth/hooks/useSessionStorage.ts +2 -0
  10. package/src/layouts/AppLayout/AppLayout.tsx +9 -7
  11. package/src/layouts/AppLayout/components/ErrorBoundary.tsx +6 -3
  12. package/src/layouts/AppLayout/components/PageProgress.tsx +2 -0
  13. package/src/layouts/AppLayout/components/Seo.tsx +2 -0
  14. package/src/layouts/AppLayout/components/UpdateNotifier/UpdateNotifier.tsx +3 -2
  15. package/src/layouts/AppLayout/hooks/index.ts +2 -0
  16. package/src/layouts/AppLayout/hooks/useNavigation.ts +3 -1
  17. package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +1 -0
  18. package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +2 -0
  19. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +2 -0
  20. package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +4 -0
  21. package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +2 -0
  22. package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +1 -0
  23. package/src/layouts/AppLayout/providers/CoreProviders.tsx +1 -0
  24. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +1 -0
  25. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +1 -0
  26. package/src/layouts/PaymentsLayout/events.ts +2 -0
  27. package/src/layouts/ProfileLayout/ProfileLayout.tsx +1 -0
  28. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +1 -0
  29. package/src/layouts/SimpleLayout/SimpleLayout.tsx +72 -0
  30. package/src/layouts/SimpleLayout/index.ts +3 -0
  31. package/src/layouts/SupportLayout/SupportLayout.tsx +1 -0
  32. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +1 -0
  33. package/src/layouts/SupportLayout/components/TicketList.tsx +6 -5
  34. package/src/layouts/SupportLayout/events.ts +2 -0
  35. package/src/layouts/index.ts +1 -0
  36. package/src/snippets/AuthDialog/useAuthDialog.ts +2 -0
  37. package/src/snippets/Chat/components/MessageList.tsx +12 -11
  38. package/src/snippets/Chat/index.tsx +1 -0
  39. package/src/snippets/ContactForm/ContactForm.tsx +7 -2
  40. package/src/snippets/ContactForm/ContactPage.tsx +9 -9
  41. package/src/snippets/VideoPlayer/README.md +35 -0
  42. package/src/snippets/VideoPlayer/VideoControls.tsx +13 -9
  43. package/src/snippets/VideoPlayer/VideoPlayer.tsx +159 -25
  44. package/src/snippets/VideoPlayer/index.ts +1 -1
  45. package/src/validation/utils/curl-generator.ts +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.4.28",
3
+ "version": "1.4.29",
4
4
  "description": "Pre-built dashboard layouts, authentication pages, and admin templates for Next.js applications with Tailwind CSS",
5
5
  "keywords": [
6
6
  "layouts",
@@ -85,9 +85,9 @@
85
85
  "check": "tsc --noEmit"
86
86
  },
87
87
  "peerDependencies": {
88
- "@djangocfg/api": "^1.4.28",
89
- "@djangocfg/og-image": "^1.4.28",
90
- "@djangocfg/ui": "^1.4.28",
88
+ "@djangocfg/api": "^1.4.29",
89
+ "@djangocfg/og-image": "^1.4.29",
90
+ "@djangocfg/ui": "^1.4.29",
91
91
  "@hookform/resolvers": "^5.2.0",
92
92
  "consola": "^3.4.2",
93
93
  "lucide-react": "^0.468.0",
@@ -103,13 +103,13 @@
103
103
  "zod": "^4.0.10"
104
104
  },
105
105
  "dependencies": {
106
- "@vidstack/react": "^0.6.15",
107
- "maverick.js": "0.37.0",
106
+ "@vidstack/react": "next",
107
+ "media-icons": "next",
108
108
  "react-ga4": "^2.1.0",
109
- "vidstack": "0.6.15"
109
+ "vidstack": "next"
110
110
  },
111
111
  "devDependencies": {
112
- "@djangocfg/typescript-config": "^1.4.28",
112
+ "@djangocfg/typescript-config": "^1.4.29",
113
113
  "@types/node": "^24.7.2",
114
114
  "@types/react": "19.2.2",
115
115
  "@types/react-dom": "19.2.1",
@@ -1,4 +1,7 @@
1
- import { useRouter } from 'next/router';
1
+ // @ts-nocheck
2
+ 'use client';
3
+
4
+ import { useRouter } from 'next/navigation';
2
5
  import React, {
3
6
  createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState
4
7
  } from 'react';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  export { useAuthRedirectManager } from './useAuthRedirect';
2
4
  export { useAuthGuard } from './useAuthGuard';
3
5
  export { useSessionStorage } from './useSessionStorage';
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { useCallback, useEffect, useState } from 'react';
2
4
 
3
5
  import { useAuth } from '../context';
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { useRouter } from 'next/router';
2
4
  import { useEffect } from 'react';
3
5
 
@@ -1,5 +1,7 @@
1
- import { useRouter } from 'next/router';
1
+ 'use client';
2
+
2
3
  import { useEffect } from 'react';
4
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
3
5
  import { authLogger } from '../../utils/logger';
4
6
 
5
7
  export interface UseAutoAuthOptions {
@@ -13,12 +15,17 @@ export interface UseAutoAuthOptions {
13
15
  */
14
16
  export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
15
17
  const { onOTPDetected, cleanupUrl = true } = options;
18
+ const searchParams = useSearchParams();
19
+ const pathname = usePathname();
16
20
  const router = useRouter();
17
21
 
22
+ const isReady = !!pathname && !!searchParams.get('otp');
23
+ const hasOTP = !!(searchParams.get('otp'));
24
+
18
25
  useEffect(() => {
19
- if (!router.isReady) return;
26
+ if (!isReady) return;
20
27
 
21
- const queryOtp = router.query.otp as string;
28
+ const queryOtp = searchParams.get('otp') as string;
22
29
 
23
30
  // Handle OTP detection
24
31
  if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
@@ -28,16 +35,14 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
28
35
 
29
36
  // Clean up URL to remove sensitive params for security
30
37
  if (cleanupUrl && queryOtp) {
31
- const { otp: _, ...cleanQuery } = router.query;
32
- router.replace({
33
- pathname: router.pathname,
34
- query: cleanQuery
35
- }, undefined, { shallow: true });
38
+ const cleanQuery = Object.fromEntries(searchParams.entries());
39
+ delete cleanQuery.otp;
40
+ router.push(`${pathname}?${new URLSearchParams(cleanQuery).toString()}`);
36
41
  }
37
- }, [router.isReady, router.query, router.pathname, onOTPDetected, cleanupUrl]);
42
+ }, [pathname, searchParams, onOTPDetected, cleanupUrl]);
38
43
 
39
44
  return {
40
- isReady: router.isReady,
41
- hasOTP: !!(router.query.otp),
45
+ isReady,
46
+ hasOTP,
42
47
  };
43
48
  };
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import { useEffect, useState } from 'react';
2
4
  import { authLogger } from '../../utils/logger';
3
5
 
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  /**
2
4
  * Profile Cache Hook
3
5
  *
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import { useState } from 'react';
2
4
  import { authLogger } from '../../utils/logger';
3
5
 
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * AppLayout - Unified Application Layout System
3
4
  *
@@ -342,13 +343,14 @@ export function AppLayout({ children, config, component, pageProps, fontFamily,
342
343
  // Wrap with ErrorBoundary if enabled
343
344
  if (enableErrorBoundary) {
344
345
  return (
345
- <ErrorBoundary
346
- key={router.pathname}
347
- supportEmail={supportEmail}
348
- onError={onError}
349
- >
350
- {content}
351
- </ErrorBoundary>
346
+ <React.Fragment key={router.pathname}>
347
+ <ErrorBoundary
348
+ supportEmail={supportEmail}
349
+ onError={onError}
350
+ >
351
+ {content}
352
+ </ErrorBoundary>
353
+ </React.Fragment>
352
354
  );
353
355
  }
354
356
 
@@ -37,6 +37,9 @@ interface ErrorBoundaryState {
37
37
  * Catches React errors and shows ErrorLayout
38
38
  */
39
39
  export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
40
+ declare state: ErrorBoundaryState;
41
+ declare props: Readonly<ErrorBoundaryProps>;
42
+
40
43
  constructor(props: ErrorBoundaryProps) {
41
44
  super(props);
42
45
  this.state = { hasError: false };
@@ -62,12 +65,12 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
62
65
  // Call optional callback
63
66
  this.props.onError?.(error, errorInfo);
64
67
 
65
- // Store error info in state
66
- this.setState({ errorInfo });
68
+ // Store error info in state using base class method
69
+ Component.prototype.setState.call(this, { errorInfo });
67
70
  }
68
71
 
69
72
  resetError = () => {
70
- this.setState({ hasError: false, error: undefined, errorInfo: undefined });
73
+ Component.prototype.setState.call(this, { hasError: false, error: undefined, errorInfo: undefined });
71
74
  };
72
75
 
73
76
  render() {
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { useRouter } from 'next/router';
2
4
  import { useEffect, useRef, useState } from 'react';
3
5
 
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { Fragment } from 'react';
2
4
  import Head from 'next/head';
3
5
  import { useRouter } from 'next/router';
@@ -9,6 +9,7 @@
9
9
  'use client';
10
10
 
11
11
  import React, { useEffect, useState } from 'react';
12
+ import consola from 'consola';
12
13
  import { toast } from '@djangocfg/ui/hooks';
13
14
 
14
15
  const PACKAGE_NAME = '@djangocfg/layouts';
@@ -63,7 +64,7 @@ async function fetchLatestVersion(): Promise<string | null> {
63
64
  const data = await response.json();
64
65
  return data.version || null;
65
66
  } catch (error) {
66
- console.warn('[UpdateNotifier] Failed to check for updates:', error);
67
+ consola.warn('[UpdateNotifier] Failed to check for updates:', error);
67
68
  return null;
68
69
  }
69
70
  }
@@ -94,7 +95,7 @@ function setCache(data: UpdateCheckCache): void {
94
95
  try {
95
96
  localStorage.setItem(CACHE_KEY, JSON.stringify(data));
96
97
  } catch (error) {
97
- console.warn('[UpdateNotifier] Failed to cache update check:', error);
98
+ consola.warn('[UpdateNotifier] Failed to cache update check:', error);
98
99
  }
99
100
  }
100
101
 
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  /**
2
4
  * Hooks Module
3
5
  */
@@ -4,7 +4,9 @@
4
4
  * Navigation utilities
5
5
  */
6
6
 
7
- import { useRouter } from 'next/router';
7
+ 'use client';
8
+
9
+ import { useRouter } from 'next/navigation';
8
10
  import { useAppContext } from '../context';
9
11
 
10
12
  export interface UseNavigationReturn {
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  // ============================================================================
2
3
  // AdminLayout - Django CFG Layout with iframe Integration
3
4
  // ============================================================================
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import React, { createContext, useContext } from 'react';
2
4
 
3
5
  import { useAuthForm } from '../../../../auth/hooks';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import React, { useState, useEffect } from 'react';
2
4
  import { Mail, Phone, User, Send } from 'lucide-react';
3
5
 
@@ -1,3 +1,6 @@
1
+ "use client"
2
+
3
+ // @ts-nocheck
1
4
  import React from 'react';
2
5
  import { Mail, MessageCircle, ArrowLeft, RotateCw, ShieldCheck } from 'lucide-react';
3
6
 
@@ -70,6 +73,7 @@ export const OTPForm: React.FC = () => {
70
73
  Enter verification code
71
74
  </label>
72
75
  <div className="flex justify-center">
76
+ {/* @ts-expect-error - TypeScript doesn't recognize children in JSX props for discriminated union */}
73
77
  <InputOTP
74
78
  value={otp}
75
79
  onChange={setOtp}
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  export { AuthLayout } from './AuthLayout';
2
4
  export {
3
5
  AuthProvider as AuthLayoutProvider,
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Private Layout
3
4
  *
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Core Providers
3
4
  *
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Payments Layout (v2.0 - Simplified)
3
4
  *
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Create Payment Dialog (v2.0 - Simplified)
3
4
  * Dialog for creating new payments
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  /**
2
4
  * Payment Events System (v2.0 - Simplified)
3
5
  *
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  'use client';
2
3
 
3
4
  import React from 'react';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  'use client';
2
3
 
3
4
  import React, { useEffect, useState } from 'react';
@@ -0,0 +1,72 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * SimpleLayout - Lightweight provider for docs and marketing sites
4
+ *
5
+ * Provides essential UI infrastructure without the overhead of full AppLayout:
6
+ * - TooltipProvider for all tooltip components
7
+ * - Toaster for notifications
8
+ * - Basic theme support
9
+ *
10
+ * Perfect for documentation sites, landing pages, and simple apps.
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import React from 'react';
16
+ import { TooltipProvider, Toaster } from '@djangocfg/ui';
17
+
18
+ export interface SimpleLayoutProps {
19
+ children: React.ReactNode;
20
+ /**
21
+ * Delay before tooltips appear (in milliseconds)
22
+ * @default 200
23
+ */
24
+ tooltipDelayDuration?: number;
25
+ /**
26
+ * Skip delay when moving between tooltips
27
+ * @default 300
28
+ */
29
+ tooltipSkipDelayDuration?: number;
30
+ }
31
+
32
+ /**
33
+ * Lightweight layout provider for documentation and marketing sites.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * // In your root layout.tsx
38
+ * import { SimpleLayout } from '@djangocfg/layouts';
39
+ *
40
+ * export default function RootLayout({ children }) {
41
+ * return (
42
+ * <html>
43
+ * <body>
44
+ * <SimpleLayout>
45
+ * {children}
46
+ * </SimpleLayout>
47
+ * </body>
48
+ * </html>
49
+ * );
50
+ * }
51
+ * ```
52
+ */
53
+ export function SimpleLayout({
54
+ children,
55
+ tooltipDelayDuration = 200,
56
+ tooltipSkipDelayDuration = 300,
57
+ }: SimpleLayoutProps) {
58
+ return (
59
+ <>
60
+ <TooltipProvider
61
+ delayDuration={tooltipDelayDuration}
62
+ skipDelayDuration={tooltipSkipDelayDuration}
63
+ >
64
+ {children}
65
+ </TooltipProvider>
66
+ <Toaster />
67
+ </>
68
+ );
69
+ }
70
+
71
+ SimpleLayout.displayName = 'SimpleLayout';
72
+
@@ -0,0 +1,3 @@
1
+ export { SimpleLayout } from './SimpleLayout';
2
+ export type { SimpleLayoutProps } from './SimpleLayout';
3
+
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Support Layout
3
4
  * Modern support layout with resizable panels for desktop and mobile-optimized view
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Create Ticket Dialog
3
4
  * Dialog for creating new support tickets
@@ -73,11 +73,12 @@ export const TicketList: React.FC = () => {
73
73
  return (
74
74
  <div className="p-4 space-y-2">
75
75
  {[1, 2, 3, 4, 5].map((i) => (
76
- <Skeleton
77
- key={i}
78
- className="h-24 w-full animate-pulse"
79
- style={{ animationDelay: `${i * 100}ms` }}
80
- />
76
+ <div key={i}>
77
+ <Skeleton
78
+ className="h-24 w-full animate-pulse"
79
+ style={{ animationDelay: `${i * 100}ms` }}
80
+ />
81
+ </div>
81
82
  ))}
82
83
  </div>
83
84
  );
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  /**
2
4
  * Support Layout Events
3
5
  * Event system for SupportLayout
@@ -8,6 +8,7 @@ export * from './SupportLayout';
8
8
  export * from './PaymentsLayout';
9
9
  export * from './AppLayout';
10
10
  export * from './ErrorLayout';
11
+ export * from './SimpleLayout';
11
12
 
12
13
  // Note: CfgLayout is now part of AppLayout exports
13
14
  // Import it via: import { CfgLayout, useCfgApp } from '@djangocfg/layouts';
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { useCallback } from 'react';
2
4
 
3
5
  import { AUTH_EVENTS, type OpenAuthDialogPayload } from './events';
@@ -111,17 +111,18 @@ export const MessageList: React.FC<MessageListProps> = ({
111
111
  message.sources.length > 0 && (
112
112
  <div className="flex flex-wrap gap-2 px-1 animate-in fade-in slide-in-from-left-2 duration-300 delay-100">
113
113
  {message.sources.map((source, idx) => (
114
- <Badge
115
- key={idx}
116
- variant="secondary"
117
- className="text-xs flex items-center gap-1 cursor-pointer
118
- hover:bg-secondary/80 hover:scale-105 active:scale-95
119
- transition-all duration-200 animate-in fade-in zoom-in-95"
120
- style={{ animationDelay: `${(idx + 1) * 100}ms` }}
121
- >
122
- {source.document_title || `Source ${idx + 1}`}
123
- <ExternalLink className="h-3 w-3" />
124
- </Badge>
114
+ <div key={idx}>
115
+ <Badge
116
+ variant="secondary"
117
+ className="text-xs flex items-center gap-1 cursor-pointer
118
+ hover:bg-secondary/80 hover:scale-105 active:scale-95
119
+ transition-all duration-200 animate-in fade-in zoom-in-95"
120
+ style={{ animationDelay: `${(idx + 1) * 100}ms` }}
121
+ >
122
+ {source.document_title || `Source ${idx + 1}`}
123
+ <ExternalLink className="h-3 w-3" />
124
+ </Badge>
125
+ </div>
125
126
  ))}
126
127
  </div>
127
128
  )}
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Knowledge Chat Module
3
4
  * Complete RAG-powered chat widget with context providers
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  'use client';
2
3
 
3
4
  import React, { useEffect, useState } from 'react';
@@ -131,12 +132,13 @@ function ContactFormInner({
131
132
  // Apply draft from localStorage and set site_url
132
133
  const currentValues = form.getValues();
133
134
  const hasDraftData = draft.name || draft.email || draft.company || draft.subject || draft.message;
135
+ const currentSiteUrl = typeof window !== 'undefined' ? window.location.href : '';
134
136
 
135
137
  if (hasDraftData || !currentValues.site_url) {
136
138
  form.reset({
137
139
  ...emptyDraft,
138
140
  ...draft,
139
- site_url: window.location.href,
141
+ site_url: currentSiteUrl,
140
142
  });
141
143
  }
142
144
  }, [draft, form, isHydrated]);
@@ -165,14 +167,17 @@ function ContactFormInner({
165
167
  const result = await submit(data);
166
168
  if (resetOnSuccess) {
167
169
  // Keep contact info (name, email, company), clear only message content
170
+ // Store current site_url before reset to avoid re-reading window.location
168
171
  const currentValues = form.getValues();
172
+ const currentSiteUrl = currentValues.site_url || (typeof window !== 'undefined' ? window.location.href : '');
173
+
169
174
  form.reset({
170
175
  name: currentValues.name,
171
176
  email: currentValues.email,
172
177
  company: currentValues.company,
173
178
  subject: '',
174
179
  message: '',
175
- site_url: currentValues.site_url,
180
+ site_url: currentSiteUrl, // Keep the original site_url
176
181
  });
177
182
  // Update draft to keep contact info
178
183
  setDraft({
@@ -15,7 +15,7 @@ const isDev = process.env.NODE_ENV === 'development';
15
15
  const DEFAULT_CONFIG = {
16
16
  apiUrl: isDev ? 'http://localhost:8000' : 'https://api.reforms.ai',
17
17
  email: 'markolofsen@gmail.com',
18
- telegram: '+62 813 39646301',
18
+ whatsapp: '+62 813 39646301',
19
19
  calendly: 'https://calendly.com/markolofsen/meeting',
20
20
  };
21
21
 
@@ -28,8 +28,8 @@ export interface ContactPageProps {
28
28
  apiUrl?: string;
29
29
  /** Override email */
30
30
  email?: string;
31
- /** Override telegram */
32
- telegram?: string;
31
+ /** Override whatsapp */
32
+ whatsapp?: string;
33
33
  /** Override calendly link */
34
34
  calendlyUrl?: string;
35
35
  /** Page title */
@@ -64,7 +64,7 @@ export interface ContactPageProps {
64
64
  export function ContactPageBase({
65
65
  apiUrl = DEFAULT_CONFIG.apiUrl,
66
66
  email = DEFAULT_CONFIG.email,
67
- telegram = DEFAULT_CONFIG.telegram,
67
+ whatsapp = DEFAULT_CONFIG.whatsapp,
68
68
  calendlyUrl = DEFAULT_CONFIG.calendly,
69
69
  title = 'Get in Touch',
70
70
  subtitle = "Have a question or want to work together? We'd love to hear from you.",
@@ -73,8 +73,8 @@ export function ContactPageBase({
73
73
  className,
74
74
  onSuccess,
75
75
  }: ContactPageProps) {
76
- // Format phone for tel: link (remove spaces)
77
- const telegramPhone = telegram.replace(/\s/g, '');
76
+ // Format phone for WhatsApp link (remove spaces and special chars)
77
+ const whatsappPhone = whatsapp.replace(/[\s\-\(\)]/g, '');
78
78
 
79
79
  const contactDetails: ContactDetail[] = [
80
80
  {
@@ -85,9 +85,9 @@ export function ContactPageBase({
85
85
  },
86
86
  {
87
87
  icon: <MessageCircle className="h-5 w-5" />,
88
- label: 'Telegram',
89
- value: telegram,
90
- href: `https://t.me/${telegramPhone}`,
88
+ label: 'WhatsApp',
89
+ value: whatsapp,
90
+ href: `https://wa.me/${whatsappPhone}`,
91
91
  },
92
92
  {
93
93
  icon: <MapPin className="h-5 w-5" />,
@@ -79,6 +79,41 @@ function AdvancedPlayer() {
79
79
 
80
80
  ## Supported Video Sources
81
81
 
82
+ ### YouTube
83
+ - **URL Format**: `https://www.youtube.com/watch?v=VIDEO_ID` or `youtube/VIDEO_ID`
84
+ - **Auto-conversion**: Full YouTube URLs are automatically converted to `youtube/ID` format
85
+ - **Poster**: ⚠️ YouTube iframe ignores custom poster images and always shows YouTube's thumbnail
86
+ - **Examples**:
87
+ ```tsx
88
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
89
+ url: 'https://youtu.be/dQw4w9WgXcQ'
90
+ url: 'youtube/dQw4w9WgXcQ'
91
+ ```
92
+
93
+ ### Vimeo
94
+ - **URL Format**: `https://vimeo.com/VIDEO_ID` or `vimeo/VIDEO_ID`
95
+ - **Auto-conversion**: Full Vimeo URLs are automatically converted to `vimeo/ID` format
96
+ - **Poster**: ⚠️ Vimeo may ignore custom poster and use their own thumbnail
97
+ - **Example**: `url: 'vimeo/76979871'`
98
+
99
+ ### Direct Video Files (MP4, WebM, OGG)
100
+ - **Poster**: ✅ **Works perfectly!** Custom poster images are fully supported
101
+ - **Examples**:
102
+ ```tsx
103
+ url: 'https://example.com/video.mp4',
104
+ poster: '/images/video-poster.jpg' // This works!
105
+ ```
106
+
107
+ ### HLS Streams
108
+ - **Poster**: ✅ Custom poster supported
109
+ - **Example**: `url: 'https://example.com/stream.m3u8'`
110
+
111
+ ### DASH Streams
112
+ - **Poster**: ✅ Custom poster supported
113
+ - **Example**: `url: 'https://example.com/stream.mpd'`
114
+
115
+ > **Note**: The `poster` prop works for direct video files, HLS, and DASH streams. For YouTube and Vimeo, the platform's own thumbnail is displayed regardless of the `poster` prop due to iframe limitations.
116
+
82
117
  ### YouTube
83
118
  ```tsx
84
119
  <VideoPlayer
@@ -6,12 +6,12 @@
6
6
 
7
7
  import React from 'react';
8
8
  import { useMediaStore, useMediaRemote } from '@vidstack/react';
9
- import type { MediaPlayerElement } from 'vidstack';
9
+ import type { MediaPlayerInstance } from '@vidstack/react';
10
10
  import { Play, Pause, Volume2, VolumeX, Maximize, Minimize } from 'lucide-react';
11
11
  import { cn } from '@djangocfg/ui';
12
12
 
13
13
  interface VideoControlsProps {
14
- player: React.RefObject<MediaPlayerElement | null>;
14
+ player: React.RefObject<MediaPlayerInstance | null>;
15
15
  className?: string;
16
16
  }
17
17
 
@@ -55,13 +55,17 @@ export function VideoControls({ player, className }: VideoControlsProps) {
55
55
  const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
56
56
 
57
57
  return (
58
- <div className={cn(
59
- "absolute inset-0 flex flex-col justify-end opacity-0 hover:opacity-100 focus-within:opacity-100 transition-opacity duration-300",
60
- "bg-gradient-to-t from-black/80 via-black/20 to-transparent",
61
- className
62
- )}>
58
+ <div
59
+ className={cn(
60
+ "absolute inset-0 flex flex-col justify-end transition-opacity duration-300",
61
+ "bg-gradient-to-t from-black/80 via-black/20 to-transparent",
62
+ "opacity-0 group-hover:opacity-100 focus-within:opacity-100",
63
+ "pointer-events-none group-hover:pointer-events-auto",
64
+ className
65
+ )}
66
+ >
63
67
  {/* Progress Bar */}
64
- <div className="px-4 pb-2">
68
+ <div className="px-4 pb-2 pointer-events-auto">
65
69
  <div
66
70
  className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all group"
67
71
  onClick={handleProgressClick}
@@ -76,7 +80,7 @@ export function VideoControls({ player, className }: VideoControlsProps) {
76
80
  </div>
77
81
 
78
82
  {/* Control Bar */}
79
- <div className="flex items-center gap-4 px-4 pb-4">
83
+ <div className="flex items-center gap-4 px-4 pb-4 pointer-events-auto">
80
84
  {/* Play/Pause */}
81
85
  <button
82
86
  onClick={() => remote.togglePaused()}
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * Professional VideoPlayer - Vidstack Implementation
3
4
  * Supports YouTube, Vimeo, MP4, HLS and more with custom controls
@@ -5,12 +6,103 @@
5
6
 
6
7
  'use client';
7
8
 
8
- import React, { forwardRef, useImperativeHandle, useRef } from 'react';
9
- import { MediaPlayer, MediaOutlet } from '@vidstack/react';
10
- import { MediaRemoteControl, type MediaPlayerElement } from 'vidstack';
9
+ import React, { forwardRef, useImperativeHandle, useRef, useMemo } from 'react';
10
+ import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
11
+ import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
12
+ import type { MediaPlayerInstance } from '@vidstack/react';
13
+ import consola from 'consola';
11
14
  import { cn } from '@djangocfg/ui';
12
15
  import { type VideoPlayerProps, type VideoPlayerRef } from './types';
13
- import { VideoControls } from './VideoControls';
16
+
17
+ // Import Vidstack base styles
18
+ import '@vidstack/react/player/styles/base.css';
19
+ // Import default theme
20
+ import '@vidstack/react/player/styles/default/theme.css';
21
+ import '@vidstack/react/player/styles/default/layouts/video.css';
22
+
23
+ /**
24
+ * Custom error class for invalid video URLs
25
+ */
26
+ export class VideoUrlError extends Error {
27
+ constructor(
28
+ message: string,
29
+ public readonly url: string,
30
+ public readonly suggestion?: string
31
+ ) {
32
+ super(message);
33
+ this.name = 'VideoUrlError';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Validates and normalizes video URL for Vidstack
39
+ * @throws {VideoUrlError} If URL format is invalid for the detected provider
40
+ */
41
+ function normalizeVideoUrl(url: string): string {
42
+ if (!url || typeof url !== 'string') {
43
+ throw new VideoUrlError('Video URL is required', url || '');
44
+ }
45
+
46
+ const trimmedUrl = url.trim();
47
+
48
+ // Already in correct format (youtube/ID, vimeo/ID, or direct URL)
49
+ if (trimmedUrl.startsWith('youtube/') || trimmedUrl.startsWith('vimeo/')) {
50
+ return trimmedUrl;
51
+ }
52
+
53
+ // YouTube URL patterns - auto-convert to youtube/ID format
54
+ const youtubePatterns = [
55
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
56
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
57
+ ];
58
+
59
+ for (const pattern of youtubePatterns) {
60
+ const match = trimmedUrl.match(pattern);
61
+ if (match) {
62
+ // Auto-convert YouTube URL to youtube/ID format
63
+ const videoId = match[1];
64
+ if (process.env.NODE_ENV === 'development') {
65
+ consola.info(
66
+ `[VideoPlayer] Auto-converted YouTube URL to "youtube/${videoId}" format`
67
+ );
68
+ }
69
+ return `youtube/${videoId}`;
70
+ }
71
+ }
72
+
73
+ // Vimeo URL patterns - auto-convert to vimeo/ID format
74
+ const vimeoPattern = /vimeo\.com\/(\d+)/;
75
+ const vimeoMatch = trimmedUrl.match(vimeoPattern);
76
+ if (vimeoMatch) {
77
+ const videoId = vimeoMatch[1];
78
+ if (process.env.NODE_ENV === 'development') {
79
+ consola.info(
80
+ `[VideoPlayer] Auto-converted Vimeo URL to "vimeo/${videoId}" format`
81
+ );
82
+ }
83
+ return `vimeo/${videoId}`;
84
+ }
85
+
86
+ // Direct video URLs (mp4, webm, etc.) - allow as-is
87
+ if (/\.(mp4|webm|ogg|m3u8|mpd)(\?.*)?$/i.test(trimmedUrl)) {
88
+ return trimmedUrl;
89
+ }
90
+
91
+ // HLS/DASH streams
92
+ if (trimmedUrl.includes('.m3u8') || trimmedUrl.includes('.mpd')) {
93
+ return trimmedUrl;
94
+ }
95
+
96
+ // Unknown format - return as-is but warn in dev
97
+ if (process.env.NODE_ENV === 'development') {
98
+ consola.warn(
99
+ `[VideoPlayer] Unknown URL format: "${trimmedUrl}". ` +
100
+ `Supported formats: youtube/{id}, vimeo/{id}, or direct video URLs (.mp4, .webm, .m3u8)`
101
+ );
102
+ }
103
+
104
+ return trimmedUrl;
105
+ }
14
106
 
15
107
  export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
16
108
  source,
@@ -27,26 +119,54 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
27
119
  onEnded,
28
120
  onError,
29
121
  }, ref) => {
30
- const playerRef = useRef<MediaPlayerElement | null>(null);
122
+ const playerRef = useRef<MediaPlayerInstance | null>(null);
123
+
124
+ // Debug log
125
+ if (process.env.NODE_ENV === 'development') {
126
+ consola.info('[VideoPlayer] Received props:', {
127
+ url: source.url,
128
+ poster: source.poster,
129
+ title: source.title,
130
+ });
131
+ }
132
+
133
+ // Validate and normalize URL - throws VideoUrlError if invalid
134
+ const normalizedUrl = useMemo(() => {
135
+ try {
136
+ return normalizeVideoUrl(source.url);
137
+ } catch (error) {
138
+ if (error instanceof VideoUrlError) {
139
+ // Call onError callback and re-throw
140
+ onError?.(error.message + (error.suggestion ? ` Use: "${error.suggestion}"` : ''));
141
+ throw error;
142
+ }
143
+ throw error;
144
+ }
145
+ }, [source.url, onError]);
31
146
 
32
147
  // Expose player methods via ref
33
148
  useImperativeHandle(ref, () => {
34
- const getRemote = () => {
35
- if (!playerRef.current) return null;
36
- const remote = new MediaRemoteControl();
37
- remote.setTarget(playerRef.current as unknown as EventTarget);
38
- return remote;
39
- };
149
+ const player = playerRef.current;
40
150
 
41
151
  return {
42
- play: () => getRemote()?.play(),
43
- pause: () => getRemote()?.pause(),
44
- togglePlay: () => getRemote()?.togglePaused(),
45
- seekTo: (time: number) => getRemote()?.seek(time),
46
- setVolume: (volume: number) => getRemote()?.changeVolume(Math.max(0, Math.min(1, volume))),
47
- toggleMute: () => getRemote()?.toggleMuted(),
48
- enterFullscreen: () => getRemote()?.enterFullscreen(),
49
- exitFullscreen: () => getRemote()?.exitFullscreen(),
152
+ play: () => player?.play(),
153
+ pause: () => player?.pause(),
154
+ togglePlay: () => {
155
+ if (player) {
156
+ player.paused ? player.play() : player.pause();
157
+ }
158
+ },
159
+ seekTo: (time: number) => {
160
+ if (player) player.currentTime = time;
161
+ },
162
+ setVolume: (volume: number) => {
163
+ if (player) player.volume = Math.max(0, Math.min(1, volume));
164
+ },
165
+ toggleMute: () => {
166
+ if (player) player.muted = !player.muted;
167
+ },
168
+ enterFullscreen: () => player?.enterFullscreen(),
169
+ exitFullscreen: () => player?.exitFullscreen(),
50
170
  };
51
171
  }, []);
52
172
 
@@ -71,7 +191,7 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
71
191
  {/* Video Player */}
72
192
  <div
73
193
  className={cn(
74
- "relative w-full overflow-hidden rounded-sm bg-black",
194
+ "relative w-full rounded-sm bg-black overflow-hidden",
75
195
  theme === 'minimal' && "rounded-none",
76
196
  theme === 'modern' && "rounded-xl shadow-2xl"
77
197
  )}
@@ -80,8 +200,7 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
80
200
  <MediaPlayer
81
201
  ref={playerRef}
82
202
  title={source.title || 'Video'}
83
- src={source.url}
84
- poster={source.poster}
203
+ src={normalizedUrl}
85
204
  autoPlay={autoplay}
86
205
  muted={muted}
87
206
  playsInline={playsInline}
@@ -91,10 +210,25 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
91
210
  onError={handleError}
92
211
  className="w-full h-full"
93
212
  >
94
- <MediaOutlet />
213
+ <MediaProvider />
95
214
 
96
- {/* Custom controls */}
97
- {controls && <VideoControls player={playerRef} />}
215
+ {/* Poster with proper aspect ratio handling */}
216
+ {source.poster && (
217
+ <Poster
218
+ className="vds-poster"
219
+ src={source.poster}
220
+ alt={source.title || 'Video poster'}
221
+ style={{ objectFit: 'cover' }}
222
+ />
223
+ )}
224
+
225
+ {/* Use Vidstack's built-in default layout */}
226
+ {controls && (
227
+ <DefaultVideoLayout
228
+ icons={defaultLayoutIcons}
229
+ thumbnails={source.poster}
230
+ />
231
+ )}
98
232
  </MediaPlayer>
99
233
  </div>
100
234
 
@@ -3,6 +3,6 @@
3
3
  * Export all components and types
4
4
  */
5
5
 
6
- export { VideoPlayer } from './VideoPlayer';
6
+ export { VideoPlayer, VideoUrlError } from './VideoPlayer';
7
7
  export { VideoControls } from './VideoControls';
8
8
  export type { VideoSource, VideoPlayerProps, VideoPlayerRef } from './types';
@@ -1,9 +1,13 @@
1
+ "use client"
2
+
1
3
  /**
2
4
  * cURL Generator
3
5
  *
4
6
  * Generates cURL commands from API request details with authentication token
5
7
  */
6
8
 
9
+ import consola from 'consola';
10
+
7
11
  export interface CurlOptions {
8
12
  method: string;
9
13
  path: string;
@@ -28,7 +32,7 @@ export function getAuthToken(): string | null {
28
32
 
29
33
  return token;
30
34
  } catch (error) {
31
- console.error('Failed to get auth token:', error);
35
+ consola.error('Failed to get auth token:', error);
32
36
  return null;
33
37
  }
34
38
  }