@djangocfg/layouts 1.4.30 → 2.0.2
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.
- package/README.md +277 -18
- package/package.json +15 -24
- package/src/auth/context/AuthContext.tsx +5 -5
- package/src/auth/hooks/useAuthGuard.ts +1 -1
- package/src/auth/hooks/useAutoAuth.ts +8 -7
- package/src/components/ErrorBoundary.tsx +78 -0
- package/src/components/JsonLd.tsx +31 -0
- package/src/components/LucideIcon.tsx +91 -0
- package/src/components/PageProgress.tsx +127 -0
- package/src/components/Suspense.tsx +29 -0
- package/src/{layouts/AppLayout/components → components}/UpdateNotifier/UpdateNotifier.tsx +56 -49
- package/src/components/index.ts +10 -0
- package/src/index.ts +25 -7
- package/src/layouts/AdminLayout/AdminLayout.tsx +46 -0
- package/src/layouts/AdminLayout/index.ts +7 -0
- package/src/layouts/AppLayout/AppLayout.tsx +278 -326
- package/src/layouts/AppLayout/index.ts +2 -39
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthContext.tsx +3 -2
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthHelp.tsx +1 -0
- package/src/layouts/AuthLayout/AuthLayout.tsx +61 -0
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/IdentifierForm.tsx +47 -34
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/OTPForm.tsx +2 -3
- package/src/layouts/AuthLayout/index.ts +24 -0
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/types.ts +1 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +144 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +32 -0
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +57 -0
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +141 -0
- package/src/layouts/PrivateLayout/components/index.ts +8 -0
- package/src/layouts/PrivateLayout/index.ts +7 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +15 -7
- package/src/layouts/PublicLayout/PublicLayout.tsx +121 -0
- package/src/layouts/PublicLayout/components/PublicFooter.tsx +190 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +117 -0
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +101 -0
- package/src/layouts/PublicLayout/components/index.ts +8 -0
- package/src/layouts/PublicLayout/index.ts +7 -0
- package/src/layouts/_components/UserMenu.tsx +160 -0
- package/src/layouts/_components/index.ts +7 -0
- package/src/layouts/index.ts +15 -8
- package/src/snippets/Analytics/AnalyticsProvider.tsx +8 -4
- package/src/snippets/Analytics/useAnalytics.ts +11 -21
- package/src/snippets/Chat/ChatWidget.tsx +4 -4
- package/src/snippets/ContactForm/ContactFormProvider.tsx +32 -19
- package/src/snippets/ContactForm/ContactPage.tsx +2 -4
- package/src/snippets/ContactForm/types.ts +3 -2
- package/src/snippets/index.ts +0 -1
- package/src/layouts/AppLayout/README.md +0 -204
- package/src/layouts/AppLayout/SUMMARY.md +0 -240
- package/src/layouts/AppLayout/USAGE.md +0 -312
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +0 -112
- package/src/layouts/AppLayout/components/PageProgress.tsx +0 -123
- package/src/layouts/AppLayout/components/Seo.tsx +0 -171
- package/src/layouts/AppLayout/components/UserMenu.tsx +0 -385
- package/src/layouts/AppLayout/components/index.ts +0 -11
- package/src/layouts/AppLayout/context/AppContext.tsx +0 -151
- package/src/layouts/AppLayout/context/index.ts +0 -5
- package/src/layouts/AppLayout/hooks/index.ts +0 -8
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +0 -26
- package/src/layouts/AppLayout/hooks/useNavigation.ts +0 -51
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +0 -224
- package/src/layouts/AppLayout/layouts/AdminLayout/README.md +0 -409
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.example.tsx +0 -98
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.tsx +0 -149
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +0 -146
- package/src/layouts/AppLayout/layouts/AdminLayout/components/index.ts +0 -3
- package/src/layouts/AppLayout/layouts/AdminLayout/context/CfgAppContext.tsx +0 -48
- package/src/layouts/AppLayout/layouts/AdminLayout/context/index.ts +0 -2
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/index.ts +0 -6
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +0 -279
- package/src/layouts/AppLayout/layouts/AdminLayout/index.ts +0 -24
- package/src/layouts/AppLayout/layouts/AdminLayout/lottie/energizing.json +0 -1
- package/src/layouts/AppLayout/layouts/AdminLayout/types/index.ts +0 -45
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +0 -41
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +0 -15
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +0 -82
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +0 -62
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +0 -89
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +0 -181
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +0 -9
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +0 -5
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +0 -44
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +0 -242
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileDrawer.tsx +0 -150
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +0 -169
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +0 -5
- package/src/layouts/AppLayout/layouts/index.ts +0 -7
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +0 -80
- package/src/layouts/AppLayout/providers/index.ts +0 -5
- package/src/layouts/AppLayout/types/config.ts +0 -79
- package/src/layouts/AppLayout/types/index.ts +0 -11
- package/src/layouts/AppLayout/types/layout.ts +0 -54
- package/src/layouts/AppLayout/types/navigation.ts +0 -43
- package/src/layouts/AppLayout/types/page.ts +0 -80
- package/src/layouts/AppLayout/types/routes.ts +0 -43
- package/src/layouts/AppLayout/utils/index.ts +0 -5
- package/src/layouts/AppLayout/utils/routeDetection.ts +0 -31
- package/src/layouts/ErrorLayout/ErrorLayout.tsx +0 -173
- package/src/layouts/ErrorLayout/errorConfig.tsx +0 -152
- package/src/layouts/ErrorLayout/index.ts +0 -8
- package/src/layouts/SimpleLayout/SimpleLayout.tsx +0 -72
- package/src/layouts/SimpleLayout/index.ts +0 -3
- package/src/snippets/VideoPlayer/README.md +0 -238
- package/src/snippets/VideoPlayer/VideoControls.tsx +0 -137
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +0 -248
- package/src/snippets/VideoPlayer/index.ts +0 -8
- package/src/snippets/VideoPlayer/types.ts +0 -61
- package/src/types/index.ts +0 -2
- package/src/types/pageConfig.ts +0 -100
- /package/src/{validation → components/ErrorsTracker}/README.md +0 -0
- /package/src/{validation → components/ErrorsTracker}/components/ErrorButtons.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/components/ErrorToast.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/hooks.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/index.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/providers/ErrorTrackingProvider.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/types.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/utils/curl-generator.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/utils/formatters.ts +0 -0
- /package/src/{layouts/AppLayout/components → components}/UpdateNotifier/index.ts +0 -0
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
# VideoPlayer - Professional Vidstack Implementation
|
|
2
|
-
|
|
3
|
-
A professional, accessible video player built with Vidstack React that supports YouTube, Vimeo, MP4, HLS, and more.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- ✅ **Multi-platform support**: YouTube, Vimeo, MP4, HLS, DASH
|
|
8
|
-
- ✅ **Custom controls**: Professional UI with hover effects
|
|
9
|
-
- ✅ **Accessibility**: Full keyboard navigation and screen reader support
|
|
10
|
-
- ✅ **Responsive**: Works on all screen sizes
|
|
11
|
-
- ✅ **TypeScript**: Full type safety
|
|
12
|
-
- ✅ **Themes**: Default, minimal, and modern themes
|
|
13
|
-
- ✅ **No recommendations**: Clean playback without YouTube distractions
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
The VideoPlayer is already included in the UI package with all dependencies:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
pnpm add @vidstack/react@next media-icons@next
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Basic Usage
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
import { VideoPlayer } from '@repo/ui/snippets/VideoPlayer';
|
|
27
|
-
|
|
28
|
-
function MyComponent() {
|
|
29
|
-
return (
|
|
30
|
-
<VideoPlayer
|
|
31
|
-
source={{
|
|
32
|
-
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
33
|
-
title: 'Never Gonna Give You Up',
|
|
34
|
-
description: 'Rick Astley - Never Gonna Give You Up (Official Video)'
|
|
35
|
-
}}
|
|
36
|
-
autoplay={false}
|
|
37
|
-
controls={true}
|
|
38
|
-
className="max-w-4xl mx-auto"
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Advanced Usage
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
import { VideoPlayer, VideoPlayerRef } from '@repo/ui/snippets/VideoPlayer';
|
|
48
|
-
import { useRef } from 'react';
|
|
49
|
-
|
|
50
|
-
function AdvancedPlayer() {
|
|
51
|
-
const playerRef = useRef<VideoPlayerRef>(null);
|
|
52
|
-
|
|
53
|
-
const handleCustomPlay = () => {
|
|
54
|
-
playerRef.current?.play();
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<VideoPlayer
|
|
59
|
-
ref={playerRef}
|
|
60
|
-
source={{
|
|
61
|
-
url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
|
62
|
-
title: 'Big Buck Bunny',
|
|
63
|
-
poster: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
|
|
64
|
-
duration: 596
|
|
65
|
-
}}
|
|
66
|
-
theme="modern"
|
|
67
|
-
autoplay={false}
|
|
68
|
-
muted={false}
|
|
69
|
-
playsInline={true}
|
|
70
|
-
showInfo={true}
|
|
71
|
-
onPlay={() => console.log('Video started')}
|
|
72
|
-
onPause={() => console.log('Video paused')}
|
|
73
|
-
onEnded={() => console.log('Video ended')}
|
|
74
|
-
onError={(error) => console.error('Video error:', error)}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Supported Video Sources
|
|
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
|
-
|
|
117
|
-
### YouTube
|
|
118
|
-
```tsx
|
|
119
|
-
<VideoPlayer
|
|
120
|
-
source={{
|
|
121
|
-
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
122
|
-
title: 'YouTube Video'
|
|
123
|
-
}}
|
|
124
|
-
/>
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Vimeo
|
|
128
|
-
```tsx
|
|
129
|
-
<VideoPlayer
|
|
130
|
-
source={{
|
|
131
|
-
url: 'https://vimeo.com/76979871',
|
|
132
|
-
title: 'Vimeo Video'
|
|
133
|
-
}}
|
|
134
|
-
/>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Direct MP4
|
|
138
|
-
```tsx
|
|
139
|
-
<VideoPlayer
|
|
140
|
-
source={{
|
|
141
|
-
url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
|
142
|
-
title: 'Direct MP4',
|
|
143
|
-
poster: 'https://example.com/poster.jpg'
|
|
144
|
-
}}
|
|
145
|
-
/>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### HLS Stream
|
|
149
|
-
```tsx
|
|
150
|
-
<VideoPlayer
|
|
151
|
-
source={{
|
|
152
|
-
url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
|
|
153
|
-
title: 'HLS Stream'
|
|
154
|
-
}}
|
|
155
|
-
/>
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## API Reference
|
|
159
|
-
|
|
160
|
-
### VideoPlayerProps
|
|
161
|
-
|
|
162
|
-
| Prop | Type | Default | Description |
|
|
163
|
-
|------|------|---------|-------------|
|
|
164
|
-
| `source` | `VideoSource` | - | Video source configuration |
|
|
165
|
-
| `aspectRatio` | `number` | `16/9` | Video aspect ratio |
|
|
166
|
-
| `autoplay` | `boolean` | `false` | Auto-play video |
|
|
167
|
-
| `muted` | `boolean` | `false` | Mute video by default |
|
|
168
|
-
| `playsInline` | `boolean` | `true` | Play inline on mobile |
|
|
169
|
-
| `controls` | `boolean` | `true` | Show custom controls |
|
|
170
|
-
| `showInfo` | `boolean` | `false` | Show video info below player |
|
|
171
|
-
| `theme` | `'default' \| 'minimal' \| 'modern'` | `'default'` | Player theme |
|
|
172
|
-
| `className` | `string` | - | Custom CSS class |
|
|
173
|
-
| `onPlay` | `() => void` | - | Play event callback |
|
|
174
|
-
| `onPause` | `() => void` | - | Pause event callback |
|
|
175
|
-
| `onEnded` | `() => void` | - | End event callback |
|
|
176
|
-
| `onError` | `(error: string) => void` | - | Error event callback |
|
|
177
|
-
|
|
178
|
-
### VideoSource
|
|
179
|
-
|
|
180
|
-
| Property | Type | Description |
|
|
181
|
-
|----------|------|-------------|
|
|
182
|
-
| `url` | `string` | Video URL (YouTube, Vimeo, MP4, HLS, etc.) |
|
|
183
|
-
| `title` | `string?` | Video title |
|
|
184
|
-
| `description` | `string?` | Video description |
|
|
185
|
-
| `poster` | `string?` | Custom poster/thumbnail URL |
|
|
186
|
-
| `duration` | `number?` | Video duration in seconds |
|
|
187
|
-
|
|
188
|
-
### VideoPlayerRef Methods
|
|
189
|
-
|
|
190
|
-
| Method | Description |
|
|
191
|
-
|--------|-------------|
|
|
192
|
-
| `play()` | Play the video |
|
|
193
|
-
| `pause()` | Pause the video |
|
|
194
|
-
| `togglePlay()` | Toggle play/pause |
|
|
195
|
-
| `seekTo(time: number)` | Seek to specific time |
|
|
196
|
-
| `setVolume(volume: number)` | Set volume (0-1) |
|
|
197
|
-
| `toggleMute()` | Toggle mute |
|
|
198
|
-
| `enterFullscreen()` | Enter fullscreen |
|
|
199
|
-
| `exitFullscreen()` | Exit fullscreen |
|
|
200
|
-
|
|
201
|
-
## Themes
|
|
202
|
-
|
|
203
|
-
### Default Theme
|
|
204
|
-
Clean, professional look with rounded corners and subtle shadows.
|
|
205
|
-
|
|
206
|
-
### Minimal Theme
|
|
207
|
-
No rounded corners, minimal styling for embedding in tight spaces.
|
|
208
|
-
|
|
209
|
-
### Modern Theme
|
|
210
|
-
Enhanced shadows and larger border radius for a contemporary look.
|
|
211
|
-
|
|
212
|
-
## Accessibility
|
|
213
|
-
|
|
214
|
-
The VideoPlayer includes full accessibility support:
|
|
215
|
-
|
|
216
|
-
- ✅ Keyboard navigation (Space, Arrow keys, F for fullscreen)
|
|
217
|
-
- ✅ Screen reader announcements
|
|
218
|
-
- ✅ Focus indicators
|
|
219
|
-
- ✅ ARIA labels and roles
|
|
220
|
-
- ✅ High contrast support
|
|
221
|
-
|
|
222
|
-
## Performance
|
|
223
|
-
|
|
224
|
-
- ✅ Lazy loading of video content
|
|
225
|
-
- ✅ Efficient re-renders with Vidstack's optimized state management
|
|
226
|
-
- ✅ Minimal bundle size impact
|
|
227
|
-
- ✅ Hardware-accelerated playback when available
|
|
228
|
-
|
|
229
|
-
## Browser Support
|
|
230
|
-
|
|
231
|
-
Supports all modern browsers through Vidstack's comprehensive compatibility layer:
|
|
232
|
-
|
|
233
|
-
- ✅ Chrome 63+
|
|
234
|
-
- ✅ Firefox 67+
|
|
235
|
-
- ✅ Safari 12+
|
|
236
|
-
- ✅ Edge 79+
|
|
237
|
-
- ✅ iOS Safari 12+
|
|
238
|
-
- ✅ Chrome Android 63+
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Video Controls for Vidstack Player
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
'use client';
|
|
6
|
-
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { useMediaStore, useMediaRemote } from '@vidstack/react';
|
|
9
|
-
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
10
|
-
import { Play, Pause, Volume2, VolumeX, Maximize, Minimize } from 'lucide-react';
|
|
11
|
-
import { cn } from '@djangocfg/ui';
|
|
12
|
-
|
|
13
|
-
interface VideoControlsProps {
|
|
14
|
-
player: React.RefObject<MediaPlayerInstance | null>;
|
|
15
|
-
className?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function VideoControls({ player, className }: VideoControlsProps) {
|
|
19
|
-
const store = useMediaStore(player);
|
|
20
|
-
const remote = useMediaRemote();
|
|
21
|
-
|
|
22
|
-
const isPaused = store.paused;
|
|
23
|
-
const isMuted = store.muted;
|
|
24
|
-
const isFullscreen = store.fullscreen;
|
|
25
|
-
const currentTime = store.currentTime;
|
|
26
|
-
const duration = store.duration;
|
|
27
|
-
const volume = store.volume;
|
|
28
|
-
|
|
29
|
-
const formatTime = (seconds: number): string => {
|
|
30
|
-
if (!seconds || seconds < 0) return '0:00';
|
|
31
|
-
const minutes = Math.floor(seconds / 60);
|
|
32
|
-
const secs = Math.floor(seconds % 60);
|
|
33
|
-
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
37
|
-
if (!duration) return;
|
|
38
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
39
|
-
const clickX = e.clientX - rect.left;
|
|
40
|
-
const percentage = clickX / rect.width;
|
|
41
|
-
const newTime = percentage * duration;
|
|
42
|
-
remote.seek(newTime);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const handleVolumeChange = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
46
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
47
|
-
const clickX = e.clientX - rect.left;
|
|
48
|
-
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
|
49
|
-
remote.changeVolume(percentage);
|
|
50
|
-
if (percentage > 0 && isMuted) {
|
|
51
|
-
remote.toggleMuted();
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
56
|
-
|
|
57
|
-
return (
|
|
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
|
-
>
|
|
67
|
-
{/* Progress Bar */}
|
|
68
|
-
<div className="px-4 pb-2 pointer-events-auto">
|
|
69
|
-
<div
|
|
70
|
-
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all group"
|
|
71
|
-
onClick={handleProgressClick}
|
|
72
|
-
>
|
|
73
|
-
<div
|
|
74
|
-
className="h-full bg-primary rounded-full transition-all relative group-hover:bg-primary/90"
|
|
75
|
-
style={{ width: `${progress}%` }}
|
|
76
|
-
>
|
|
77
|
-
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{/* Control Bar */}
|
|
83
|
-
<div className="flex items-center gap-4 px-4 pb-4 pointer-events-auto">
|
|
84
|
-
{/* Play/Pause */}
|
|
85
|
-
<button
|
|
86
|
-
onClick={() => remote.togglePaused()}
|
|
87
|
-
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
88
|
-
>
|
|
89
|
-
{isPaused ? <Play className="h-6 w-6" /> : <Pause className="h-6 w-6" />}
|
|
90
|
-
</button>
|
|
91
|
-
|
|
92
|
-
{/* Time */}
|
|
93
|
-
<div className="text-white text-sm font-medium">
|
|
94
|
-
{formatTime(currentTime)} / {formatTime(duration)}
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div className="flex-1" />
|
|
98
|
-
|
|
99
|
-
{/* Volume Control */}
|
|
100
|
-
<div className="flex items-center gap-2 group/volume">
|
|
101
|
-
<button
|
|
102
|
-
onClick={() => remote.toggleMuted()}
|
|
103
|
-
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
104
|
-
>
|
|
105
|
-
{isMuted || volume === 0 ? (
|
|
106
|
-
<VolumeX className="h-5 w-5" />
|
|
107
|
-
) : (
|
|
108
|
-
<Volume2 className="h-5 w-5" />
|
|
109
|
-
)}
|
|
110
|
-
</button>
|
|
111
|
-
|
|
112
|
-
<div
|
|
113
|
-
className="w-0 group-hover/volume:w-20 transition-all overflow-hidden"
|
|
114
|
-
>
|
|
115
|
-
<div
|
|
116
|
-
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all"
|
|
117
|
-
onClick={handleVolumeChange}
|
|
118
|
-
>
|
|
119
|
-
<div
|
|
120
|
-
className="h-full bg-white rounded-full transition-all"
|
|
121
|
-
style={{ width: `${volume * 100}%` }}
|
|
122
|
-
/>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
{/* Fullscreen */}
|
|
128
|
-
<button
|
|
129
|
-
onClick={() => isFullscreen ? remote.exitFullscreen() : remote.enterFullscreen()}
|
|
130
|
-
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
131
|
-
>
|
|
132
|
-
{isFullscreen ? <Minimize className="h-5 w-5" /> : <Maximize className="h-5 w-5" />}
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* Professional VideoPlayer - Vidstack Implementation
|
|
4
|
-
* Supports YouTube, Vimeo, MP4, HLS and more with custom controls
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use client';
|
|
8
|
-
|
|
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';
|
|
14
|
-
import { cn } from '@djangocfg/ui';
|
|
15
|
-
import { type VideoPlayerProps, type VideoPlayerRef } from './types';
|
|
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
|
-
}
|
|
106
|
-
|
|
107
|
-
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
|
108
|
-
source,
|
|
109
|
-
aspectRatio = 16 / 9,
|
|
110
|
-
autoplay = false,
|
|
111
|
-
muted = false,
|
|
112
|
-
playsInline = true,
|
|
113
|
-
controls = true,
|
|
114
|
-
className,
|
|
115
|
-
showInfo = false,
|
|
116
|
-
theme = 'default',
|
|
117
|
-
onPlay,
|
|
118
|
-
onPause,
|
|
119
|
-
onEnded,
|
|
120
|
-
onError,
|
|
121
|
-
}, ref) => {
|
|
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]);
|
|
146
|
-
|
|
147
|
-
// Expose player methods via ref
|
|
148
|
-
useImperativeHandle(ref, () => {
|
|
149
|
-
const player = playerRef.current;
|
|
150
|
-
|
|
151
|
-
return {
|
|
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(),
|
|
170
|
-
};
|
|
171
|
-
}, []);
|
|
172
|
-
|
|
173
|
-
const handlePlay = () => {
|
|
174
|
-
onPlay?.();
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const handlePause = () => {
|
|
178
|
-
onPause?.();
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const handleEnded = () => {
|
|
182
|
-
onEnded?.();
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const handleError = (detail: any) => {
|
|
186
|
-
onError?.(detail?.message || 'Video playback error');
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<div className={cn("w-full", className)}>
|
|
191
|
-
{/* Video Player */}
|
|
192
|
-
<div
|
|
193
|
-
className={cn(
|
|
194
|
-
"relative w-full rounded-sm bg-black overflow-hidden",
|
|
195
|
-
theme === 'minimal' && "rounded-none",
|
|
196
|
-
theme === 'modern' && "rounded-xl shadow-2xl"
|
|
197
|
-
)}
|
|
198
|
-
style={{ aspectRatio: aspectRatio }}
|
|
199
|
-
>
|
|
200
|
-
<MediaPlayer
|
|
201
|
-
ref={playerRef}
|
|
202
|
-
title={source.title || 'Video'}
|
|
203
|
-
src={normalizedUrl}
|
|
204
|
-
autoPlay={autoplay}
|
|
205
|
-
muted={muted}
|
|
206
|
-
playsInline={playsInline}
|
|
207
|
-
onPlay={handlePlay}
|
|
208
|
-
onPause={handlePause}
|
|
209
|
-
onEnded={handleEnded}
|
|
210
|
-
onError={handleError}
|
|
211
|
-
className="w-full h-full"
|
|
212
|
-
>
|
|
213
|
-
<MediaProvider />
|
|
214
|
-
|
|
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
|
-
)}
|
|
232
|
-
</MediaPlayer>
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
|
-
{/* Video Info */}
|
|
236
|
-
{showInfo && source.title && (
|
|
237
|
-
<div className="mt-4 space-y-2">
|
|
238
|
-
<h3 className="text-xl font-semibold text-foreground">{source.title}</h3>
|
|
239
|
-
{source.description && (
|
|
240
|
-
<p className="text-muted-foreground">{source.description}</p>
|
|
241
|
-
)}
|
|
242
|
-
</div>
|
|
243
|
-
)}
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
VideoPlayer.displayName = 'VideoPlayer';
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Professional VideoPlayer - Vidstack Implementation
|
|
3
|
-
* Export all components and types
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export { VideoPlayer, VideoUrlError } from './VideoPlayer';
|
|
7
|
-
export { VideoControls } from './VideoControls';
|
|
8
|
-
export type { VideoSource, VideoPlayerProps, VideoPlayerRef } from './types';
|