add change language
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { client, type ModelVideo } from '@/api/client';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import NameGradient from './components/NameGradient.vue';
|
||||
import QuickActions from './components/QuickActions.vue';
|
||||
import RecentVideos from './components/RecentVideos.vue';
|
||||
@@ -9,20 +10,19 @@ import StatsOverview from './components/StatsOverview.vue';
|
||||
|
||||
const loading = ref(true);
|
||||
const recentVideos = ref<ModelVideo[]>([]);
|
||||
const { t } = useI18n();
|
||||
|
||||
// Mock stats data (in real app, fetch from API)
|
||||
const stats = ref({
|
||||
totalVideos: 0,
|
||||
totalViews: 0,
|
||||
storageUsed: 0,
|
||||
storageLimit: 10737418240, // 10GB in bytes
|
||||
storageLimit: 10737418240,
|
||||
uploadsThisMonth: 0
|
||||
});
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// Fetch recent videos
|
||||
const response = await client.videos.videosList({ page: 1, limit: 5 });
|
||||
const body = response.data as any;
|
||||
|
||||
@@ -34,7 +34,6 @@ const fetchDashboardData = async () => {
|
||||
stats.value.totalVideos = body.length;
|
||||
}
|
||||
|
||||
// Calculate mock stats
|
||||
stats.value.totalViews = recentVideos.value.reduce((sum, v: any) => sum + (v.views || 0), 0);
|
||||
stats.value.storageUsed = recentVideos.value.reduce((sum, v) => sum + (v.size || 0), 0);
|
||||
stats.value.uploadsThisMonth = recentVideos.value.filter(v => {
|
||||
@@ -52,25 +51,20 @@ const fetchDashboardData = async () => {
|
||||
onMounted(() => {
|
||||
fetchDashboardData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-overview">
|
||||
<PageHeader :title="NameGradient" description="Welcome back, Here's what's happening with your videos." :breadcrumbs="[
|
||||
{ label: 'Dashboard' }
|
||||
<PageHeader :title="NameGradient" :description="t('overview.pageHeaderDescription')" :breadcrumbs="[
|
||||
{ label: t('pageHeader.dashboard') }
|
||||
]" />
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<StatsOverview :loading="loading" :stats="stats" />
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<QuickActions :loading="loading" />
|
||||
|
||||
<!-- Recent Videos -->
|
||||
<RecentVideos :loading="loading" :videos="recentVideos" />
|
||||
|
||||
<!-- Storage Usage -->
|
||||
<!-- <StorageUsage :loading="loading" :stats="stats" /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div class="text-3xl font-bold text-gray-900 mb-1">
|
||||
<span class=":uno: bg-[linear-gradient(130deg,#14a74b_0%,#22c55e_35%,#10b981_65%,#06b6d4_100%)] bg-clip-text text-transparent">Hello, {{ auth.user?.username }}</span>
|
||||
<span class=":uno: bg-[linear-gradient(130deg,#14a74b_0%,#22c55e_35%,#10b981_65%,#06b6d4_100%)] bg-clip-text text-transparent">{{ t('overview.nameGradient.hello', { name: auth.user?.username || t('app.name') }) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const auth = useAuthStore()
|
||||
</script>
|
||||
const auth = useAuthStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Chart from '@/components/icons/Chart.vue';
|
||||
import Credit from '@/components/icons/Credit.vue';
|
||||
import Upload from '@/components/icons/Upload.vue';
|
||||
@@ -6,6 +8,7 @@ import Video from '@/components/icons/Video.vue';
|
||||
import { useUIState } from '@/stores/uiState';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Referral from './Referral.vue';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
}
|
||||
@@ -14,33 +17,34 @@ defineProps<Props>();
|
||||
|
||||
const uiState = useUIState();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const quickActions = [
|
||||
const quickActions = computed(() => [
|
||||
{
|
||||
title: 'Upload Video',
|
||||
description: 'Upload a new video to your library',
|
||||
title: t('overview.quickActions.uploadVideo.title'),
|
||||
description: t('overview.quickActions.uploadVideo.description'),
|
||||
icon: Upload,
|
||||
onClick: () => uiState.toggleUploadDialog()
|
||||
},
|
||||
{
|
||||
title: 'Video Library',
|
||||
description: 'Browse all your videos',
|
||||
title: t('overview.quickActions.videoLibrary.title'),
|
||||
description: t('overview.quickActions.videoLibrary.description'),
|
||||
icon: Video,
|
||||
onClick: () => router.push('/video')
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
description: 'Track performance & insights',
|
||||
title: t('overview.quickActions.analytics.title'),
|
||||
description: t('overview.quickActions.analytics.description'),
|
||||
icon: Chart,
|
||||
onClick: () => { }
|
||||
},
|
||||
{
|
||||
title: 'Manage Plan',
|
||||
description: 'Upgrade or change your plan',
|
||||
title: t('overview.quickActions.managePlan.title'),
|
||||
description: t('overview.quickActions.managePlan.description'),
|
||||
icon: Credit,
|
||||
onClick: () => router.push('/payments-and-plans')
|
||||
},
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,7 +67,7 @@ const quickActions = [
|
||||
</div>
|
||||
|
||||
<div v-else class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Quick Actions</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">{{ t('overview.quickActions.title') }}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<button v-for="action in quickActions" :key="action.title" @click="action.onClick" :class="[
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ModelVideo } from '@/api/client';
|
||||
import EmptyState from '@/components/dashboard/EmptyState.vue';
|
||||
import { formatBytes, formatDate, formatDuration } from '@/lib/utils';
|
||||
import { formatDate, formatDuration } from '@/lib/utils';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
interface Props {
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const getStatusClass = (status?: string) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
@@ -45,17 +47,17 @@ const getStatusClass = (status?: string) => {
|
||||
|
||||
<div v-else>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold">Recent Videos</h2>
|
||||
<h2 class="text-xl font-semibold">{{ t('overview.recentVideos.title') }}</h2>
|
||||
<router-link to="/video"
|
||||
class="text-sm text-primary hover:underline font-medium flex items-center gap-1">
|
||||
View all
|
||||
{{ t('overview.recentVideos.viewAll') }}
|
||||
<span class="i-heroicons-arrow-right w-4 h-4" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<EmptyState v-if="videos.length === 0" title="No videos found"
|
||||
description="You haven't uploaded any videos yet. Start by uploading your first video!"
|
||||
imageUrl="https://cdn-icons-png.flaticon.com/512/7486/7486747.png" actionLabel="Upload Video"
|
||||
<EmptyState v-if="videos.length === 0" :title="t('overview.recentVideos.emptyTitle')"
|
||||
:description="t('overview.recentVideos.emptyDescription')"
|
||||
imageUrl="https://cdn-icons-png.flaticon.com/512/7486/7486747.png" :actionLabel="t('overview.recentVideos.emptyAction')"
|
||||
:onAction="() => router.push('/upload')" />
|
||||
|
||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
@@ -65,19 +67,19 @@ const getStatusClass = (status?: string) => {
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Video</th>
|
||||
{{ t('overview.recentVideos.table.video') }}</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status</th>
|
||||
{{ t('overview.recentVideos.table.status') }}</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Duration</th>
|
||||
{{ t('overview.recentVideos.table.duration') }}</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Upload Date</th>
|
||||
{{ t('overview.recentVideos.table.uploadDate') }}</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions</th>
|
||||
{{ t('overview.recentVideos.table.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@@ -94,14 +96,14 @@ const getStatusClass = (status?: string) => {
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-gray-900 truncate">{{ video.title }}</p>
|
||||
<p class="text-sm text-gray-500 truncate">
|
||||
{{ video.description || 'No description' }}</p>
|
||||
{{ video.description || t('overview.recentVideos.noDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
:class="['px-2 py-1 text-xs font-medium rounded-full whitespace-nowrap', getStatusClass(video.status)]">
|
||||
{{ video.status || 'Unknown' }}
|
||||
{{ video.status || t('overview.recentVideos.unknownStatus') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
@@ -112,13 +114,13 @@ const getStatusClass = (status?: string) => {
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Edit">
|
||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" :title="t('overview.recentVideos.actionEdit')">
|
||||
<span class="i-heroicons-pencil w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Share">
|
||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" :title="t('overview.recentVideos.actionShare')">
|
||||
<span class="i-heroicons-share w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-red-100 rounded transition-colors" title="Delete">
|
||||
<button class="p-1.5 hover:bg-red-100 rounded transition-colors" :title="t('overview.recentVideos.actionDelete')">
|
||||
<span class="i-heroicons-trash w-4 h-4 text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-surface">
|
||||
<div class="flex flex-col space-y-1.5 p-6">
|
||||
<h3 class="text-lg font-semibold leading-none tracking-tight">Referral Link</h3>
|
||||
<h3 class="text-lg font-semibold leading-none tracking-tight">{{ t('overview.referral.title') }}</h3>
|
||||
</div>
|
||||
<div class="p-6 pt-0 space-y-4">
|
||||
<p class="text-sm text-gray-600 font-medium">Share your referral link and earn commissions from
|
||||
referred users!</p>
|
||||
<p class="text-sm text-gray-600 font-medium">{{ t('overview.referral.subtitle') }}</p>
|
||||
<div class="flex gap-2">
|
||||
<AppInput class="w-full" readonly type="text" :modelValue="url" @click="copyToClipboard" />
|
||||
<button class="btn btn-primary" @click="copyToClipboard" :disabled="isCopied">
|
||||
<button class="btn btn-primary" @click="copyToClipboard" :disabled="isCopied" :aria-label="t('common.copy')">
|
||||
<svg v-if="!isCopied" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true">
|
||||
@@ -28,19 +27,25 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { ref } from 'vue';
|
||||
const auth = useAuthStore()
|
||||
const isCopied = ref(false)
|
||||
const url = location.origin + '/ref/' + auth.user?.username
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const isCopied = ref(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
const url = computed(() => `${location.origin}/ref/${auth.user?.username || ''}`);
|
||||
|
||||
const copyToClipboard = ($event: MouseEvent) => {
|
||||
// ($event.target as HTMLInputElement)?.select
|
||||
if ($event.target instanceof HTMLInputElement) {
|
||||
$event.target.select()
|
||||
$event.target.select();
|
||||
}
|
||||
navigator.clipboard.writeText(url)
|
||||
isCopied.value = true
|
||||
|
||||
navigator.clipboard.writeText(url.value);
|
||||
isCopied.value = true;
|
||||
|
||||
setTimeout(() => {
|
||||
isCopied.value = false
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
isCopied.value = false;
|
||||
}, 3000);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import StatsCard from '@/components/dashboard/StatsCard.vue';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
|
||||
@@ -14,6 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -30,15 +32,15 @@ defineProps<Props>();
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatsCard title="Total Videos" :value="stats.totalVideos" :trend="{ value: 12, isPositive: true }" />
|
||||
<StatsCard :title="t('overview.stats.totalVideos')" :value="stats.totalVideos" :trend="{ value: 12, isPositive: true }" />
|
||||
|
||||
<StatsCard title="Total Views" :value="stats.totalViews.toLocaleString()"
|
||||
<StatsCard :title="t('overview.stats.totalViews')" :value="stats.totalViews.toLocaleString()"
|
||||
:trend="{ value: 8, isPositive: true }" />
|
||||
|
||||
<StatsCard title="Storage Used"
|
||||
<StatsCard :title="t('overview.stats.storageUsed')"
|
||||
:value="`${formatBytes(stats.storageUsed)} / ${formatBytes(stats.storageLimit)}`" color="warning" />
|
||||
|
||||
<StatsCard title="Uploads This Month" :value="stats.uploadsThisMonth" color="success"
|
||||
<StatsCard :title="t('overview.stats.uploadsThisMonth')" :value="stats.uploadsThisMonth" color="success"
|
||||
:trend="{ value: 25, isPositive: true }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { t } = useI18n();
|
||||
|
||||
const storagePercentage = computed(() => {
|
||||
return Math.round((props.stats.storageUsed / props.stats.storageLimit) * 100);
|
||||
@@ -24,21 +26,21 @@ const storageBreakdown = computed(() => {
|
||||
const total = videoSize + thumbSize + otherSize;
|
||||
|
||||
return [
|
||||
{ label: 'Videos', size: videoSize, percentage: (videoSize / (total || 1)) * 100, color: 'bg-primary' },
|
||||
{ label: 'Thumbnails & Assets', size: thumbSize, percentage: (thumbSize / (total || 1)) * 100, color: 'bg-blue-500' },
|
||||
{ label: 'Other Files', size: otherSize, percentage: (otherSize / (total || 1)) * 100, color: 'bg-gray-400' },
|
||||
{ label: t('overview.storage.breakdown.videos'), size: videoSize, percentage: (videoSize / (total || 1)) * 100, color: 'bg-primary' },
|
||||
{ label: t('overview.storage.breakdown.thumbnails'), size: thumbSize, percentage: (thumbSize / (total || 1)) * 100, color: 'bg-blue-500' },
|
||||
{ label: t('overview.storage.breakdown.other'), size: otherSize, percentage: (otherSize / (total || 1)) * 100, color: 'bg-gray-400' },
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!loading" class="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Storage Usage</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">{{ t('overview.storage.title') }}</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
{{ formatBytes(stats.storageUsed) }} of {{ formatBytes(stats.storageLimit) }} used
|
||||
{{ t('overview.storage.usedOfLimit', { used: formatBytes(stats.storageUsed), limit: formatBytes(stats.storageLimit) }) }}
|
||||
</span>
|
||||
<span class="text-sm font-medium" :class="storagePercentage > 80 ? 'text-danger' : 'text-gray-700'">
|
||||
{{ storagePercentage }}%
|
||||
@@ -66,10 +68,10 @@ const storageBreakdown = computed(() => {
|
||||
<div class="flex gap-2">
|
||||
<span class="i-heroicons-exclamation-triangle w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-yellow-800">Storage running low</p>
|
||||
<p class="text-sm font-medium text-yellow-800">{{ t('overview.storage.lowStorage.title') }}</p>
|
||||
<p class="text-sm text-yellow-700 mt-1">
|
||||
Consider upgrading your plan to get more storage.
|
||||
<router-link to="/plans" class="underline font-medium">View plans</router-link>
|
||||
{{ t('overview.storage.lowStorage.message') }}
|
||||
<router-link to="/plans" class="underline font-medium">{{ t('overview.storage.lowStorage.viewPlans') }}</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gradient-to-r to-success/20 p-4 sm:p-6 md:p-8 rounded-xl border-2 border-success/30 mb-8">
|
||||
<h1 class="text-2xl sm:text-3xl md:text-4xl font-extrabold text-foreground mb-2">Welcome back, {{
|
||||
auth.user?.username }}! 👋
|
||||
<h1 class="text-2xl sm:text-3xl md:text-4xl font-extrabold text-foreground mb-2">
|
||||
{{ t('overview.welcome.title', { name: auth.user?.username || t('app.name') }) }}
|
||||
</h1>
|
||||
<p class="text-sm sm:text-base text-gray-600 font-medium">Here's what's happening with your content
|
||||
today.</p>
|
||||
<p class="text-sm sm:text-base text-gray-600 font-medium">{{ t('overview.welcome.subtitle') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user