develop-updateui #1

Merged
lethdat merged 78 commits from develop-updateui into master 2026-04-02 05:59:23 +00:00
4 changed files with 206 additions and 178 deletions
Showing only changes of commit f805bac0e6 - Show all commits

4
components.d.ts vendored
View File

@@ -22,11 +22,13 @@ declare module 'vue' {
DashboardSidebar: typeof import('./src/components/dashboard/DashboardSidebar.vue')['default']
DashboardTopbar: typeof import('./src/components/dashboard/DashboardTopbar.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']
IconField: typeof import('primevue/iconfield')['default']
InputIcon: typeof import('primevue/inputicon')['default']
InputText: typeof import('primevue/inputtext')['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']
OverlayPanel: typeof import('primevue/overlaypanel')['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 DashboardTopbar: typeof import('./src/components/dashboard/DashboardTopbar.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 IconField: typeof import('primevue/iconfield')['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']
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
const Message: typeof import('primevue/message')['default']
const OverlayPanel: typeof import('primevue/overlaypanel')['default']
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']

View 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>

View 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>

View File

@@ -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">
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { client } from '@/api/client';
import Upload from '@/components/icons/Upload.vue';
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>
<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ử .</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)...&#10;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 (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 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>