upload ui
This commit is contained in:
4
components.d.ts
vendored
4
components.d.ts
vendored
@@ -22,11 +22,13 @@ declare module 'vue' {
|
|||||||
DashboardSidebar: typeof import('./src/components/dashboard/DashboardSidebar.vue')['default']
|
DashboardSidebar: typeof import('./src/components/dashboard/DashboardSidebar.vue')['default']
|
||||||
DashboardTopbar: typeof import('./src/components/dashboard/DashboardTopbar.vue')['default']
|
DashboardTopbar: typeof import('./src/components/dashboard/DashboardTopbar.vue')['default']
|
||||||
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
||||||
|
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
Home: typeof import('./src/components/icons/Home.vue')['default']
|
Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||||
IconField: typeof import('primevue/iconfield')['default']
|
IconField: typeof import('primevue/iconfield')['default']
|
||||||
InputIcon: typeof import('primevue/inputicon')['default']
|
InputIcon: typeof import('primevue/inputicon')['default']
|
||||||
InputText: typeof import('primevue/inputtext')['default']
|
InputText: typeof import('primevue/inputtext')['default']
|
||||||
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
|
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
Message: typeof import('primevue/message')['default']
|
Message: typeof import('primevue/message')['default']
|
||||||
OverlayPanel: typeof import('primevue/overlaypanel')['default']
|
OverlayPanel: typeof import('primevue/overlaypanel')['default']
|
||||||
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||||
@@ -55,11 +57,13 @@ declare global {
|
|||||||
const DashboardSidebar: typeof import('./src/components/dashboard/DashboardSidebar.vue')['default']
|
const DashboardSidebar: typeof import('./src/components/dashboard/DashboardSidebar.vue')['default']
|
||||||
const DashboardTopbar: typeof import('./src/components/dashboard/DashboardTopbar.vue')['default']
|
const DashboardTopbar: typeof import('./src/components/dashboard/DashboardTopbar.vue')['default']
|
||||||
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
||||||
|
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||||
const IconField: typeof import('primevue/iconfield')['default']
|
const IconField: typeof import('primevue/iconfield')['default']
|
||||||
const InputIcon: typeof import('primevue/inputicon')['default']
|
const InputIcon: typeof import('primevue/inputicon')['default']
|
||||||
const InputText: typeof import('primevue/inputtext')['default']
|
const InputText: typeof import('primevue/inputtext')['default']
|
||||||
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
|
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
const Message: typeof import('primevue/message')['default']
|
const Message: typeof import('primevue/message')['default']
|
||||||
const OverlayPanel: typeof import('primevue/overlaypanel')['default']
|
const OverlayPanel: typeof import('primevue/overlaypanel')['default']
|
||||||
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||||
|
|||||||
3
src/components/icons/HardDriveUpload.vue
Normal file
3
src/components/icons/HardDriveUpload.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 503"><path d="M10 397v32c0 35 29 64 64 64h320c35 0 64-29 64-64v-32c0-35-29-64-64-64H266v32c0 18-14 32-32 32s-32-14-32-32v-32H74c-35 0-64 29-64 64zm392 16c0 13-11 24-24 24s-24-11-24-24 11-24 24-24 24 11 24 24z" fill="#a6acb9"/><path d="M234 397c18 0 32-14 32-32V122l41 42c13 12 33 12 46 0 12-13 12-33 0-46l-96-96c-13-12-33-12-46 0l-96 96c-12 13-12 33 0 46 13 12 33 12 46 0l41-42v243c0 18 14 32 32 32z" fill="#1e3050"/></svg>
|
||||||
|
</template>
|
||||||
3
src/components/icons/LinkIcon.vue
Normal file
3
src/components/icons/LinkIcon.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 404"><path d="M74 170v64c0 53 43 96 96 96h96v64h64v-64h96c53 0 96-43 96-96v-64c0-53-43-96-96-96h-96V10h-64v64h-96c-53 0-96 43-96 96zm96 0h256v64H170v-64z" fill="#a6acb9"/><path d="M170 10C82 10 10 82 10 170v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96V10h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM202 170h-32v64h256v-64H202z" fill="#1e3050"/></svg>
|
||||||
|
</template>
|
||||||
@@ -1,181 +1,199 @@
|
|||||||
<template>
|
|
||||||
<div class="p-6 max-w-2xl mx-auto">
|
|
||||||
<h1 class="text-3xl font-bold mb-6">Upload Video</h1>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
|
|
||||||
<div v-if="!file" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-10 text-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" @drop.prevent="handleDrop" @dragover.prevent>
|
|
||||||
<input type="file" id="fileInput" class="hidden" accept="video/*" @change="handleFileSelect">
|
|
||||||
<label for="fileInput" class="cursor-pointer">
|
|
||||||
<span class="i-heroicons-cloud-arrow-up text-6xl text-gray-400 mb-4 inline-block"></span>
|
|
||||||
<p class="text-xl font-medium text-gray-700 dark:text-gray-200">Drag & drop your video here</p>
|
|
||||||
<p class="text-sm text-gray-500 mt-2">or click to browse</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-6">
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded">
|
|
||||||
<div class="flex items-center space-x-3 overflow-hidden">
|
|
||||||
<span class="i-heroicons-video-camera text-2xl text-primary-500 flex-shrink-0"></span>
|
|
||||||
<span class="truncate font-medium">{{ file.name }}</span>
|
|
||||||
<span class="text-xs text-gray-500 flex-shrink-0">({{ formatBytes(file.size) }})</span>
|
|
||||||
</div>
|
|
||||||
<button @click="resetFile" class="text-red-500 hover:text-red-700 flex-shrink-0" :disabled="uploading">
|
|
||||||
<span class="i-heroicons-x-mark text-xl"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Title</label>
|
|
||||||
<input v-model="form.title" type="text" class="w-full rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 p-2" placeholder="My Awesome Video">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Description</label>
|
|
||||||
<textarea v-model="form.description" class="w-full rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 p-2" rows="3" placeholder="Describe your video..."></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="uploadError" class="text-red-500 text-sm">
|
|
||||||
{{ uploadError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="uploading" class="space-y-2">
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span>Uploading...</span>
|
|
||||||
<span>{{ uploadProgress }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
||||||
<div class="h-full bg-primary-600 transition-all duration-300" :style="{ width: `${uploadProgress}%` }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="startUpload"
|
|
||||||
class="w-full py-3 bg-primary-600 hover:bg-primary-700 text-white rounded font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
:disabled="uploading || !form.title"
|
|
||||||
>
|
|
||||||
{{ uploading ? 'Uploading...' : 'Upload Video' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from 'vue';
|
import Upload from '@/components/icons/Upload.vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { client } from '@/api/client';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const file = ref<File | null>(null);
|
|
||||||
const uploading = ref(false);
|
|
||||||
const uploadProgress = ref(0);
|
|
||||||
const uploadError = ref<string | null>(null);
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
title: '',
|
|
||||||
description: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFileSelect = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
if (target.files && target.files.length > 0) {
|
|
||||||
file.value = target.files[0];
|
|
||||||
form.title = file.value.name.replace(/\.[^/.]+$/, ""); // Default title from filename
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent) => {
|
|
||||||
if (event.dataTransfer?.files.length) {
|
|
||||||
file.value = event.dataTransfer.files[0];
|
|
||||||
form.title = file.value.name.replace(/\.[^/.]+$/, "");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFile = () => {
|
|
||||||
file.value = null;
|
|
||||||
form.title = '';
|
|
||||||
form.description = '';
|
|
||||||
uploadError.value = null;
|
|
||||||
uploadProgress.value = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startUpload = async () => {
|
|
||||||
if (!file.value || !form.title) return;
|
|
||||||
|
|
||||||
uploading.value = true;
|
|
||||||
uploadError.value = null;
|
|
||||||
uploadProgress.value = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Get Presigned URL
|
|
||||||
const presignedResponse = await client.videos.uploadUrlCreate({
|
|
||||||
filename: file.value.name,
|
|
||||||
content_type: file.value.type,
|
|
||||||
size: file.value.size
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for data in response
|
|
||||||
const data = (presignedResponse.data as any).data || presignedResponse.data;
|
|
||||||
if (!data || !data.url) {
|
|
||||||
throw new Error('Failed to get upload URL');
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadUrl = data.url;
|
|
||||||
const finalVideoUrl = data.key || uploadUrl.split('?')[0]; // Assuming key is returned or can be derived
|
|
||||||
|
|
||||||
// 2. Upload file to S3 (using XMLHttpRequest for progress)
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('PUT', uploadUrl, true);
|
|
||||||
xhr.setRequestHeader('Content-Type', file.value!.type);
|
|
||||||
|
|
||||||
xhr.upload.onprogress = (e) => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
uploadProgress.value = Math.round((e.loaded / e.total) * 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => reject(new Error('Network error during upload'));
|
|
||||||
xhr.send(file.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Create Video Record
|
|
||||||
await client.videos.videosCreate({
|
|
||||||
title: form.title,
|
|
||||||
description: form.description,
|
|
||||||
size: file.value.size,
|
|
||||||
format: file.value.type,
|
|
||||||
url: finalVideoUrl,
|
|
||||||
// duration is optional, server might process it
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redirect to videos page
|
|
||||||
router.push({ name: 'video' });
|
|
||||||
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
uploadError.value = err.message || 'Upload failed';
|
|
||||||
} finally {
|
|
||||||
uploading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
|
||||||
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];
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="flex-1 flex items-stretch">
|
||||||
|
|
||||||
|
<div class="flex-1 p-8 lg:p-12 overflow-y-auto">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
|
||||||
|
<header class="mb-10">
|
||||||
|
<h1 class="text-3xl font-bold text-slate-900 tracking-tight">Tải lên Video</h1>
|
||||||
|
<p class="text-lg text-slate-500 mt-2">Chọn phương thức tải lên để bắt đầu xử lý.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="inline-flex bg-slate-100 p-1 rounded-2xl mb-8 relative z-0">
|
||||||
|
<div id="pill-bg" class="absolute left-1 top-1 h-[calc(100%-8px)] w-[calc(50%-4px)] bg-white rounded-xl shadow-sm transition-all duration-300 ease-out -z-10"></div>
|
||||||
|
<button onclick="switchView('local')" id="btn-local" class="flex items-center gap-2 px-6 py-3 text-sm font-semibold text-slate-900 rounded-xl transition-colors relative z-10">
|
||||||
|
<hard-drive-upload class="w-5 h-5"/> Từ máy tính
|
||||||
|
</button>
|
||||||
|
<button onclick="switchView('remote')" id="btn-remote" class="flex items-center gap-2 px-6 py-3 text-sm font-medium text-slate-500 hover:text-slate-900 rounded-xl transition-colors relative z-10">
|
||||||
|
<link-icon class="w-5 h-5"/> Remote URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="view-local" class="transition-all duration-500 ease-[cubic-bezier(0.25,0.8,0.25,1)] opacity-100 translate-y-0">
|
||||||
|
<div class="relative group cursor-pointer">
|
||||||
|
<input type="file" multiple class="absolute inset-0 w-full h-full opacity-0 z-20 cursor-pointer" onchange="simulateUploadStart()">
|
||||||
|
|
||||||
|
<div class="bg-gradient-to-tr from-slate-50 to-white rounded-[2rem] p-16 text-center border-2 border-dashed border-slate-200 group-hover:border-success/50 group-hover:shadow-soft transition-all duration-300 relative overflow-hidden">
|
||||||
|
|
||||||
|
<div class="absolute top-0 left-0 w-64 h-64 bg-indigo-100/40 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
|
||||||
|
<div class="absolute bottom-0 right-0 w-64 h-64 bg-blue-100/40 rounded-full blur-3xl translate-x-1/2 translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex flex-col items-center">
|
||||||
|
<div class="w-24 h-24 mb-8 rounded-3xl bg-white shadow-soft flex items-center justify-center text-accent group-hover:scale-110 group-hover:shadow-card-hover transition-all duration-300 ring-4 ring-slate-50 group-hover:ring-indigo-50">
|
||||||
|
<Upload class="w-10 h-10"></Upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold text-slate-900 mb-3">Kéo thả video của bạn vào đây</h3>
|
||||||
|
<p class="text-slate-500 text-base mb-8 max-w-md mx-auto leading-relaxed">Hỗ trợ tải nhiều file cùng lúc. Định dạng MP4, MOV, MKV. Tối đa 10GB mỗi file.</p>
|
||||||
|
|
||||||
|
<span class="px-8 py-3.5 btn-lg btn-primary">
|
||||||
|
<i data-lucide="folder-open" class="w-5 h-5"></i>
|
||||||
|
Duyệt file từ thiết bị
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="view-remote" class="hidden transition-all duration-500 ease-[cubic-bezier(0.25,0.8,0.25,1)] opacity-0 translate-y-4">
|
||||||
|
<div class="bg-white rounded-[2rem] shadow-soft p-10 border border-slate-100/50">
|
||||||
|
<label class="block text-lg font-semibold text-slate-900 mb-4">Nhập đường dẫn Video</label>
|
||||||
|
|
||||||
|
<div class="flex gap-4 items-start">
|
||||||
|
<div class="flex-1 relative group">
|
||||||
|
<div class="absolute left-4 top-4 text-slate-400 group-focus-within:text-accent transition-colors">
|
||||||
|
<i data-lucide="globe" class="w-6 h-6"></i>
|
||||||
|
</div>
|
||||||
|
<textarea placeholder="Dán một hoặc nhiều link (mỗi link một dòng)... https://drive.google.com/file/..."
|
||||||
|
class="w-full pl-12 pr-4 py-4 h-32 bg-slate-50/50 border-2 border-slate-100 rounded-2xl focus:border-accent focus:bg-white focus:ring-0 transition-all resize-none text-slate-800 placeholder:text-slate-400 text-base leading-relaxed font-medium"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2 text-sm text-slate-500 bg-slate-50 px-4 py-2 rounded-full">
|
||||||
|
<i data-lucide="info" class="w-4 h-4"></i> Tự động phát hiện Google Drive, Dropbox
|
||||||
|
</div>
|
||||||
|
<button onclick="simulateUploadStart()" class="px-8 py-3.5 bg-slate-900 hover:bg-black text-white font-medium rounded-xl shadow-xl shadow-slate-200/50 transition-all active:scale-95 flex items-center gap-2">
|
||||||
|
<i data-lucide="sparkles" class="w-5 h-5 text-yellow-300"></i>
|
||||||
|
Phân tích & Tải về
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bulk-actions" class="mt-10 hidden opacity-0 translate-y-4 transition-all duration-500">
|
||||||
|
<div class="p-6 bg-indigo-50/50 rounded-3xl border border-indigo-100/50 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-lg font-semibold text-slate-900">Thiết lập nhanh</h4>
|
||||||
|
<p class="text-slate-500 text-sm">Áp dụng cho 2 file đang chờ</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<select class="px-4 py-2.5 bg-white border-2 border-slate-200 rounded-xl text-sm font-medium text-slate-700 focus:border-accent outline-none transition">
|
||||||
|
<option>Chọn chuyên mục...</option>
|
||||||
|
<option>Học tập</option>
|
||||||
|
<option>Giải trí</option>
|
||||||
|
</select>
|
||||||
|
<select class="px-4 py-2.5 bg-white border-2 border-slate-200 rounded-xl text-sm font-medium text-slate-700 focus:border-accent outline-none transition">
|
||||||
|
<option>Công khai (Public)</option>
|
||||||
|
<option>Riêng tư (Private)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="w-[420px] bg-slate-50 border-l border-slate-100/80 flex flex-col h-[calc(100vh-64px)] sticky top-16">
|
||||||
|
|
||||||
|
<div class="p-6 border-b border-slate-100/80 flex items-center justify-between shrink-0">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold text-slate-900">Hàng chờ tải lên</h2>
|
||||||
|
<p class="text-sm text-slate-500 mt-1" id="queue-status">Chưa có tác vụ nào</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text-slate-400">
|
||||||
|
<i data-lucide="list-video" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto scrollbar-thin p-6 space-y-5 relative" id="queue-list">
|
||||||
|
|
||||||
|
<div id="empty-queue" class="absolute inset-0 flex flex-col items-center justify-center p-8 text-center opacity-40">
|
||||||
|
<img src="https://cdn-icons-png.flaticon.com/512/7486/7486747.png" alt="Empty" class="w-32 h-32 mb-4 grayscale opacity-50 drop-shadow-xl">
|
||||||
|
<p class="text-slate-400 font-medium">Danh sách trống</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="queue-item hidden bg-white rounded-2xl p-5 shadow-soft border border-slate-100/50 relative group overflow-hidden transition-all hover:shadow-md">
|
||||||
|
<div class="flex gap-4 relative z-10">
|
||||||
|
<div class="w-20 h-16 bg-slate-800 rounded-xl shrink-0 relative overflow-hidden shadow-sm">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent z-10"></div>
|
||||||
|
<img src="https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=200&q=80" class="w-full h-full object-cover opacity-80" alt="">
|
||||||
|
<div class="absolute bottom-1 left-2 z-20">
|
||||||
|
<i data-lucide="play-circle" class="w-4 h-4 text-white/90"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0 py-0.5 flex flex-col justify-between">
|
||||||
|
<div class="flex justify-between items-start gap-2">
|
||||||
|
<h4 class="text-sm font-bold text-slate-800 truncate">Introduction_to_React_v18.mp4</h4>
|
||||||
|
<button class="text-slate-300 hover:text-red-500 transition p-1 -mr-2 -mt-2 opacity-0 group-hover:opacity-100">
|
||||||
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs text-slate-500 mb-1.5 font-medium">
|
||||||
|
<span class="flex items-center gap-1.5"><span class="w-2 h-2 rounded-full bg-accent animate-pulse"></span> Uploading...</span>
|
||||||
|
<span class="text-accent font-bold">72%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 w-full bg-slate-100 rounded-full overflow-hidden relative">
|
||||||
|
<div class="absolute inset-0 bg-accent/20 animate-pulse w-full"></div>
|
||||||
|
<div class="h-full bg-accent rounded-full w-[72%] relative z-10 shadow-[0_0_12px_rgba(99,102,241,0.6)] transition-all duration-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-2 text-[11px] text-slate-400 font-medium">
|
||||||
|
<span>345 MB of 520 MB</span>
|
||||||
|
<span>4.2 MB/s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="queue-item hidden bg-[#F0F3FF] rounded-2xl p-5 shadow-soft border border-indigo-100/50 relative overflow-hidden group transition-all hover:shadow-md">
|
||||||
|
<div class="absolute inset-0 opacity-[0.03] bg-[radial-gradient(#6366F1_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
||||||
|
|
||||||
|
<div class="flex gap-4 relative z-10">
|
||||||
|
<div class="w-20 h-16 bg-indigo-100 rounded-xl shrink-0 flex items-center justify-center text-accent shadow-sm">
|
||||||
|
<i data-lucide="link-2" class="w-8 h-8 opacity-80"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0 py-1 flex flex-col justify-center">
|
||||||
|
<div class="flex justify-between items-start gap-2">
|
||||||
|
<h4 class="text-sm font-bold text-slate-800 truncate">Advanced_NodeJS_Patterns.mkv</h4>
|
||||||
|
<button class="text-slate-400 hover:text-red-500 transition p-1 -mr-2 -mt-2 opacity-0 group-hover:opacity-100">
|
||||||
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 mt-3">
|
||||||
|
<div class="flex items-center gap-2 text-xs font-bold text-indigo-600 bg-white py-1.5 px-3 rounded-lg shadow-sm">
|
||||||
|
<i data-lucide="loader" class="w-3.5 h-3.5 animate-spin"></i>
|
||||||
|
Fetching from Google Drive...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 border-t border-slate-100/80 bg-white shrink-0">
|
||||||
|
<div class="flex items-center justify-between text-sm mb-4 font-medium">
|
||||||
|
<span class="text-slate-500">Tổng dung lượng:</span>
|
||||||
|
<span class="text-slate-900">865 MB</span>
|
||||||
|
</div>
|
||||||
|
<button class="w-full py-4 bg-accent hover:bg-accentHover text-white text-sm font-bold rounded-2xl shadow-xl shadow-indigo-200 transition-transform active:scale-[0.98] flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed" id="btn-publish" disabled>
|
||||||
|
<i data-lucide="check-circle-2" class="w-5 h-5"></i>
|
||||||
|
Hoàn tất & Xuất bản (0)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user