diff --git a/components.d.ts b/components.d.ts index 8cb46a0..8b97abb 100644 --- a/components.d.ts +++ b/components.d.ts @@ -13,19 +13,26 @@ export {} declare module 'vue' { export interface GlobalComponents { Add: typeof import('./src/components/icons/Add.vue')['default'] + AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default'] + ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] Bell: typeof import('./src/components/icons/Bell.vue')['default'] Button: typeof import('primevue/button')['default'] Chart: typeof import('./src/components/icons/Chart.vue')['default'] Checkbox: typeof import('primevue/checkbox')['default'] + CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default'] CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] + CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] Credit: typeof import('./src/components/icons/Credit.vue')['default'] + CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default'] FloatLabel: typeof import('primevue/floatlabel')['default'] + GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default'] HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] Home: typeof import('./src/components/icons/Home.vue')['default'] IconField: typeof import('primevue/iconfield')['default'] + InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default'] InputIcon: typeof import('primevue/inputicon')['default'] InputText: typeof import('primevue/inputtext')['default'] Layout: typeof import('./src/components/icons/Layout.vue')['default'] @@ -38,30 +45,41 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Select: typeof import('primevue/select')['default'] + SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default'] StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default'] TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default'] + TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default'] Upload: typeof import('./src/components/icons/Upload.vue')['default'] Video: typeof import('./src/components/icons/Video.vue')['default'] + VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default'] VueHead: typeof import('./src/components/VueHead.tsx')['default'] + XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default'] } } // For TSX support declare global { const Add: typeof import('./src/components/icons/Add.vue')['default'] + const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default'] + const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] const Bell: typeof import('./src/components/icons/Bell.vue')['default'] const Button: typeof import('primevue/button')['default'] const Chart: typeof import('./src/components/icons/Chart.vue')['default'] const Checkbox: typeof import('primevue/checkbox')['default'] + const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default'] const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] + const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] const Credit: typeof import('./src/components/icons/Credit.vue')['default'] + const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default'] const FloatLabel: typeof import('primevue/floatlabel')['default'] + const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default'] const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] const Home: typeof import('./src/components/icons/Home.vue')['default'] const IconField: typeof import('primevue/iconfield')['default'] + const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default'] const InputIcon: typeof import('primevue/inputicon')['default'] const InputText: typeof import('primevue/inputtext')['default'] const Layout: typeof import('./src/components/icons/Layout.vue')['default'] @@ -74,9 +92,13 @@ declare global { const RouterLink: typeof import('vue-router')['RouterLink'] const RouterView: typeof import('vue-router')['RouterView'] const Select: typeof import('primevue/select')['default'] + const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default'] const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default'] const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default'] + const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default'] const Upload: typeof import('./src/components/icons/Upload.vue')['default'] const Video: typeof import('./src/components/icons/Video.vue')['default'] + const VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default'] const VueHead: typeof import('./src/components/VueHead.tsx')['default'] + const XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default'] } \ No newline at end of file diff --git a/src/components/DashboardLayout.vue b/src/components/DashboardLayout.vue index 11f4427..c1cc9e3 100644 --- a/src/components/DashboardLayout.vue +++ b/src/components/DashboardLayout.vue @@ -6,6 +6,7 @@ import Video from "@/components/icons/Video.vue"; import Credit from "@/components/icons/Credit.vue"; import Upload from "./icons/Upload.vue"; import NotificationDrawer from "./NotificationDrawer.vue"; +import GlobalUploadIndicator from "./GlobalUploadIndicator.vue"; import { cn } from "@/lib/utils"; import { createStaticVNode, ref } from "vue"; @@ -38,14 +39,9 @@ const links = [ - + @@ -53,11 +49,11 @@ const links = [ isNotificationOpen = val" /> - - + + - + + - diff --git a/src/components/GlobalUploadIndicator.vue b/src/components/GlobalUploadIndicator.vue new file mode 100644 index 0000000..0fb2ddd --- /dev/null +++ b/src/components/GlobalUploadIndicator.vue @@ -0,0 +1,103 @@ + + + + + + + + + + Uploads + + View All + + + + + + + + + + + + + + + + + + + + + + + + + {{ progress }}% + + + + + + {{ isUploading ? 'Uploading...' : (completeCount === items.length ? 'Completed' : 'Pending') }} + + + {{ completeCount }} / {{ items.length }} files + + + + + {{ pendingCount }} + + + + diff --git a/src/components/NotificationDrawer.vue b/src/components/NotificationDrawer.vue index fb288be..54bbbb6 100644 --- a/src/components/NotificationDrawer.vue +++ b/src/components/NotificationDrawer.vue @@ -1,7 +1,7 @@ diff --git a/src/components/icons/ArrowRightIcon.vue b/src/components/icons/ArrowRightIcon.vue new file mode 100644 index 0000000..5219d46 --- /dev/null +++ b/src/components/icons/ArrowRightIcon.vue @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/components/icons/CheckCircleIcon.vue b/src/components/icons/CheckCircleIcon.vue new file mode 100644 index 0000000..60f2266 --- /dev/null +++ b/src/components/icons/CheckCircleIcon.vue @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/components/icons/CheckMarkIcon.vue b/src/components/icons/CheckMarkIcon.vue new file mode 100644 index 0000000..cd541f1 --- /dev/null +++ b/src/components/icons/CheckMarkIcon.vue @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/components/icons/CreditCardIcon.vue b/src/components/icons/CreditCardIcon.vue new file mode 100644 index 0000000..e45780c --- /dev/null +++ b/src/components/icons/CreditCardIcon.vue @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/components/icons/InfoIcon.vue b/src/components/icons/InfoIcon.vue new file mode 100644 index 0000000..5e26ce6 --- /dev/null +++ b/src/components/icons/InfoIcon.vue @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/components/icons/SettingsIcon.vue b/src/components/icons/SettingsIcon.vue new file mode 100644 index 0000000..ea1491f --- /dev/null +++ b/src/components/icons/SettingsIcon.vue @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/src/components/icons/TrashIcon.vue b/src/components/icons/TrashIcon.vue new file mode 100644 index 0000000..efff4b7 --- /dev/null +++ b/src/components/icons/TrashIcon.vue @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/components/icons/VideoIcon.vue b/src/components/icons/VideoIcon.vue new file mode 100644 index 0000000..8fc1485 --- /dev/null +++ b/src/components/icons/VideoIcon.vue @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/components/icons/XCircleIcon.vue b/src/components/icons/XCircleIcon.vue new file mode 100644 index 0000000..9f762fd --- /dev/null +++ b/src/components/icons/XCircleIcon.vue @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/composables/useUploadQueue.ts b/src/composables/useUploadQueue.ts new file mode 100644 index 0000000..a13a9be --- /dev/null +++ b/src/composables/useUploadQueue.ts @@ -0,0 +1,164 @@ +import { ref, computed } from 'vue'; + +export interface QueueItem { + id: string; + name: string; + type: 'local' | 'remote'; + status: 'uploading' | 'processing' | 'fetching' | 'complete' | 'error' | 'pending'; + progress?: number; + uploaded?: string; + total?: string; + speed?: string; + thumbnail?: string; + file?: File; // Keep reference to file for local uploads + url?: string; // Keep reference to url for remote uploads +} + +const items = ref([]); + +export function useUploadQueue() { + + const addFiles = (files: FileList) => { + const newItems: QueueItem[] = Array.from(files).map((file) => ({ + id: Math.random().toString(36).substring(2, 9), + name: file.name, + type: 'local', + status: 'pending', // Start as pending + progress: 0, + uploaded: '0 MB', + total: formatSize(file.size), + speed: '0 MB/s', + file: file, + thumbnail: undefined // We could generate a thumbnail here if needed + })); + + items.value.push(...newItems); + }; + + const addRemoteUrls = (urls: string[]) => { + const newItems: QueueItem[] = urls.map((url) => ({ + id: Math.random().toString(36).substring(2, 9), + name: url.split('/').pop() || 'Remote File', + type: 'remote', + status: 'fetching', // Remote URLs start fetching immediately or pending? User said "khi nao nhan upload". Let's use pending. + progress: 0, + uploaded: '0 MB', + total: 'Unknown', + speed: '0 MB/s', + url: url + })); + + // Override status to pending for consistency with user request + newItems.forEach(i => i.status = 'pending'); + + items.value.push(...newItems); + }; + + const removeItem = (id: string) => { + const index = items.value.findIndex(item => item.id === id); + if (index !== -1) { + items.value.splice(index, 1); + } + }; + + const startQueue = () => { + items.value.forEach(item => { + if (item.status === 'pending') { + if (item.type === 'local') { + startMockUpload(item.id); + } else { + startMockRemoteFetch(item.id); + } + } + }); + }; + + // Mock Upload Logic + const startMockUpload = (id: string) => { + const item = items.value.find(i => i.id === id); + if (!item) return; + + item.status = 'uploading'; + let progress = 0; + const totalSize = item.file ? item.file.size : 1024 * 1024 * 50; // Default 50MB if unknown + + // Random speed between 1MB/s and 5MB/s + const speedBytesPerStep = (1024 * 1024) + Math.random() * (1024 * 1024 * 4); + + const interval = setInterval(() => { + if (progress >= 100) { + clearInterval(interval); + item.status = 'complete'; + item.progress = 100; + item.uploaded = item.total; + return; + } + + // Increment progress randomly + const increment = Math.random() * 5 + 1; // 1-6% increment + progress = Math.min(progress + increment, 100); + + item.progress = Math.floor(progress); + + // Calculate uploaded size string + const currentBytes = (progress / 100) * totalSize; + item.uploaded = formatSize(currentBytes); + + // Re-randomize speed for realism + const currentSpeed = (1024 * 1024) + Math.random() * (1024 * 1024 * 2); + item.speed = formatSize(currentSpeed) + '/s'; + + }, 500); + }; + + // Mock Remote Fetch Logic + const startMockRemoteFetch = (id: string) => { + const item = items.value.find(i => i.id === id); + if (!item) return; + + item.status = 'fetching'; // Update status to fetching + + // Remote fetch takes some time then completes + setTimeout(() => { + // Switch to uploading/processing phase if we wanted, or just finish + item.status = 'complete'; + item.progress = 100; + }, 3000 + Math.random() * 3000); + }; + + + const formatSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const totalSize = computed(() => { + let total = 0; + items.value.forEach(item => { + if (item.file) total += item.file.size; + }); + return formatSize(total); + }); + + const completeCount = computed(() => { + return items.value.filter(i => i.status === 'complete').length; + }); + + const pendingCount = computed(() => { + return items.value.filter(i => i.status === 'pending').length; + }); + + return { + items, + addFiles, + addRemoteUrls, + removeItem, + startQueue, + totalSize, + completeCount, + pendingCount + }; +} diff --git a/src/main.ts b/src/main.ts index 3bd0796..77d04c8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ import { createPinia } from "pinia"; import { useAuthStore } from './stores/auth'; import ToastService from 'primevue/toastservice'; import Tooltip from 'primevue/tooltip'; -const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen" +const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen bg-[#FAF8F8]" export function createApp() { const pinia = createPinia(); const app = createSSRApp(withErrorBoundary(RouterView)); diff --git a/src/routes/notification/components/NotificationItem.vue b/src/routes/notification/components/NotificationItem.vue index 40da859..0874cbd 100644 --- a/src/routes/notification/components/NotificationItem.vue +++ b/src/routes/notification/components/NotificationItem.vue @@ -1,5 +1,15 @@ - + - - + + @@ -63,47 +84,34 @@ const bgClass = computed(() => { {{ notification.time }} {{ notification.message }} - + - + {{ notification.actionLabel || 'View Details' }} - + - - + - + title="Mark as read"> + - - + title="Delete"> + - + + + - - diff --git a/src/routes/overview/Overview.vue b/src/routes/overview/Overview.vue index 85b3a2d..cc635c3 100644 --- a/src/routes/overview/Overview.vue +++ b/src/routes/overview/Overview.vue @@ -1,13 +1,13 @@ @@ -44,6 +50,8 @@ const handleRemoteUrls = (urls: string[]) => { - + \ No newline at end of file diff --git a/src/routes/upload/components/RemoteUrlForm.vue b/src/routes/upload/components/RemoteUrlForm.vue index 7a2d068..a2a08d8 100644 --- a/src/routes/upload/components/RemoteUrlForm.vue +++ b/src/routes/upload/components/RemoteUrlForm.vue @@ -19,7 +19,7 @@ const handleSubmit = () => { - + Enter Video URL diff --git a/src/routes/upload/components/UploadModeToggle.vue b/src/routes/upload/components/UploadModeToggle.vue index 31cd1c9..3d8df1a 100644 --- a/src/routes/upload/components/UploadModeToggle.vue +++ b/src/routes/upload/components/UploadModeToggle.vue @@ -30,7 +30,7 @@ const mode = computed({ - + diff --git a/src/routes/upload/components/UploadQueue.vue b/src/routes/upload/components/UploadQueue.vue index d36a7d4..23544bd 100644 --- a/src/routes/upload/components/UploadQueue.vue +++ b/src/routes/upload/components/UploadQueue.vue @@ -1,15 +1,18 @@ @@ -45,8 +48,7 @@ const emit = defineEmits<{ Empty queue! - + @@ -54,9 +56,21 @@ const emit = defineEmits<{ Total size: {{ totalSize || '0 MB' }} + + + + + + + + Start Upload ({{ pendingCount }}) + + + :disabled="!completeCount || (pendingCount ? pendingCount > 0 : false)"> diff --git a/src/routes/upload/components/UploadQueueItem.vue b/src/routes/upload/components/UploadQueueItem.vue index 408f0b9..6df6f9b 100644 --- a/src/routes/upload/components/UploadQueueItem.vue +++ b/src/routes/upload/components/UploadQueueItem.vue @@ -1,31 +1,46 @@ - - + class="bg-white rounded-2xl p-5 shadow-soft border border-slate-100/50 relative group overflow-hidden transition-all" + :class="{ '!p-2 !border-0 !shadow-none !rounded-xl': minimal }"> + + @@ -54,8 +69,8 @@ const emit = defineEmits<{ - - Uploading... + + {{ statusLabel }} {{ item.progress || 0 }}% @@ -76,12 +91,15 @@ const emit = defineEmits<{ - + class="bg-[#F0F3FF] rounded-2xl p-5 shadow-soft border border-indigo-100/50 relative overflow-hidden group transition-all hover:shadow-md" + :class="{ '!p-3 !bg-slate-50 !border-0 !shadow-none !rounded-xl': minimal }"> + - - + + @@ -104,13 +122,21 @@ const emit = defineEmits<{ - - + - Fetching from Google Drive... + + + + + {{ statusLabel }} diff --git a/src/routes/video/Videos.vue b/src/routes/video/Videos.vue index 7c4df58..b1f0259 100644 --- a/src/routes/video/Videos.vue +++ b/src/routes/video/Videos.vue @@ -45,17 +45,17 @@ const fetchVideos = async () => { console.warn('Unexpected video list format:', body); videos.value = []; } - + // Apply filters if (searchQuery.value) { - videos.value = videos.value.filter(v => + videos.value = videos.value.filter(v => v.title?.toLowerCase().includes(searchQuery.value.toLowerCase()) || v.description?.toLowerCase().includes(searchQuery.value.toLowerCase()) ); } - + if (selectedStatus.value !== 'all') { - videos.value = videos.value.filter(v => + videos.value = videos.value.filter(v => v.status?.toLowerCase() === selectedStatus.value.toLowerCase() ); } @@ -72,7 +72,7 @@ const formatDuration = (seconds?: number) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); - + if (h > 0) { return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } @@ -99,7 +99,7 @@ const formatBytes = (bytes?: number) => { }; const getStatusClass = (status?: string) => { - switch(status?.toLowerCase()) { + switch (status?.toLowerCase()) { case 'ready': return 'bg-green-100 text-green-700'; case 'processing': return 'bg-yellow-100 text-yellow-700'; case 'failed': return 'bg-red-100 text-red-700'; @@ -124,7 +124,7 @@ const handlePageChange = (newPage: number) => { const deleteVideo = async (videoId?: string) => { if (!videoId || !confirm('Are you sure you want to delete this video?')) return; - + try { // await client.videos.videosDelete({ id: videoId }); fetchVideos(); @@ -142,70 +142,65 @@ onMounted(() => { - + - + - + - - + + + + class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" /> - + Status - - - - + + + + - - - + + + @@ -214,85 +209,85 @@ onMounted(() => { - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + {{ error }} - + Try Again - + imageUrl="https://cdn-icons-png.flaticon.com/512/7486/7486747.png" actionLabel="Upload Video" + :onAction="() => router.push('/upload')" /> - + - - - + + + - + {{ formatDuration(video.duration) }} - + {{ video.title }} {{ video.description || 'No description' }} - + {{ video.status }} - + @@ -300,12 +295,13 @@ onMounted(() => { - + - + {{ formatDate(video.created_at) }} {{ formatBytes(video.size) }} @@ -324,7 +320,8 @@ onMounted(() => { Status Duration Size - Upload Date + Upload Date + Actions @@ -333,7 +330,8 @@ onMounted(() => { - + @@ -345,10 +343,12 @@ onMounted(() => { - + {{ video.status || 'Unknown' }} - + + {{ formatDuration(video.duration) }} @@ -365,7 +365,8 @@ onMounted(() => { - + @@ -378,24 +379,18 @@ onMounted(() => { - Showing {{ (page - 1) * limit + 1 }} to - {{ Math.min(page * limit, total) }} of + Showing {{ (page - 1) * limit + 1 }} to + {{ Math.min(page * limit, total) }} of {{ total }} results - + Previous {{ page }} - + Next diff --git a/uno.config.ts b/uno.config.ts index 6a13ef5..60ac647 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -28,37 +28,128 @@ export default defineConfig({ colors: { primary: { DEFAULT: "#14a74b", - light: "#76da83", - active: "#119c45", - "active-light": "#aff6b8", - dark: "#025c15", + 50: "#effcf3", + 100: "#dcf9e2", + 200: "#bbf0c8", + 300: "#86efac", + 400: "#4ade80", + 500: "#14a74b", + 600: "#16a34a", + 700: "#15803d", + 800: "#166534", + 900: "#14532d", + 950: "#052e16", + light: "#4ade80", + active: "#15803d", + "active-light": "#bbf0c8", + dark: "#14532d", + }, + accent: { + DEFAULT: "#6366f1", + 50: "#eef2ff", + 100: "#e0e7ff", + 200: "#c7d2fe", + 300: "#a5b4fc", + 400: "#818cf8", + 500: "#6366f1", + 600: "#4f46e5", + 700: "#4338ca", + 800: "#3730a3", + 900: "#312e81", + 950: "#1e1b4b", }, success: { - DEFAULT: "#2dc76b", - light: "#17c653", + DEFAULT: "#22c55e", + 50: "#f0fdf4", + 100: "#dcfce7", + 200: "#bbf7d0", + 300: "#86efac", + 400: "#4ade80", + 500: "#22c55e", + 600: "#16a34a", + 700: "#15803d", + 800: "#166534", + 900: "#14532d", + 950: "#052e16", + light: "#4ade80", }, info: { - DEFAULT: "#39a6ea", - light: "#39c1ea", - }, - danger: { - DEFAULT: "#f8285a", - light: "#f8285a", - active: "#d1214c", + DEFAULT: "#0ea5e9", + 50: "#f0f9ff", + 100: "#e0f2fe", + 200: "#bae6fd", + 300: "#7dd3fc", + 400: "#38bdf8", + 500: "#0ea5e9", + 600: "#0284c7", + 700: "#0369a1", + 800: "#075985", + 900: "#0c4a6e", + 950: "#082f49", + light: "#38bdf8", }, warning: { - DEFAULT: "#f0f9ff", - light: "#f0f9ff", + DEFAULT: "#f59e0b", + 50: "#fffbeb", + 100: "#fef3c7", + 200: "#fde68a", + 300: "#fcd34d", + 400: "#fbbf24", + 500: "#f59e0b", + 600: "#d97706", + 700: "#b45309", + 800: "#92400e", + 900: "#78350f", + 950: "#451a03", + light: "#fbbf24", + }, + danger: { + DEFAULT: "#ef4444", + 50: "#fef2f2", + 100: "#fee2e2", + 200: "#fecaca", + 300: "#fca5a5", + 400: "#f87171", + 500: "#ef4444", + 600: "#dc2626", + 700: "#b91c1c", + 800: "#991b1b", + 900: "#7f1d1d", + 950: "#450a0a", + light: "#f87171", + active: "#dc2626", }, secondary: { DEFAULT: "#fd7906", - light: "#fbb06f", + 50: "#fff7ed", + 100: "#ffedd5", + 200: "#fed7aa", + 300: "#fdba74", + 400: "#fb923c", + 500: "#fd7906", + 600: "#ea580c", + 700: "#c2410c", + 800: "#9a3412", + 900: "#7c2d12", + 950: "#431407", + light: "#fb923c", inverse: "#4b5675", - dark: "#b34700", + dark: "#c2410c", }, dark: { - DEFAULT: "#161f2d", - light: "#4d4d4d", + DEFAULT: "#111827", + light: "#374151", + 50: "#f9fafb", + 100: "#f3f4f6", + 200: "#e5e7eb", + 300: "#d1d5db", + 400: "#9ca3af", + 500: "#6b7280", + 600: "#4b5563", + 700: "#374151", + 800: "#1f2937", + 900: "#111827", + 950: "#030712", }, white: { DEFAULT: "#ffffff",
{{ notification.message }}
Empty queue!
{{ error }}
{{ video.description || 'No description' }}