Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cdn-dashboard/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy necessary files from builder
COPY --from=builder /app/public ./public
# Create public directory (may be empty)
RUN mkdir -p ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Copy public files if they exist (using wildcard to handle empty dir)
COPY --from=builder /app/public/. ./public/
Comment on lines +30 to +35

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with Docker layer caching and file copying. Line 31 creates a public directory, then line 35 copies from the builder's public directory. If the builder's public directory is empty (which it likely is based on the .gitkeep file), this could fail or create unexpected behavior. Consider using COPY --from=builder /app/public ./public || true or restructuring to avoid copying an empty directory twice.

Copilot uses AI. Check for mistakes.

# Set correct permissions
RUN chown -R nextjs:nodejs /app
Expand Down
Empty file added cdn-dashboard/public/.gitkeep
Empty file.
64 changes: 33 additions & 31 deletions cdn-dashboard/src/app/dashboard/files/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,12 @@ export default function FilesPage() {
};

return (
<PageTransition className="space-y-6">
<PageTransition className="space-y-4 sm:space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-zinc-100">Fichiers</h1>
<p className="text-zinc-500 mt-1">Gérez vos fichiers uploadés</p>
<h1 className="text-xl sm:text-2xl font-bold text-zinc-100">Fichiers</h1>
<p className="text-sm text-zinc-500 mt-1">Gérez vos fichiers uploadés</p>
</div>
<div className="flex items-center gap-2">
{filteredFiles.length > 0 && (
Expand Down Expand Up @@ -390,8 +390,8 @@ export default function FilesPage() {

{/* Search & Filters */}
<AnimatedCard delay={0.1}>
<CardContent className="p-4">
<div className="flex flex-col lg:flex-row gap-4">
<CardContent className="p-3 sm:p-4">
<div className="flex flex-col gap-3 sm:gap-4">
{/* Search */}
<div className="relative flex-1 group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600 group-focus-within:text-indigo-400 transition-colors" />
Expand All @@ -400,30 +400,31 @@ export default function FilesPage() {
placeholder="Rechercher un fichier..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-zinc-800/50 border border-zinc-700/50 text-zinc-100 placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all"
className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-zinc-800/50 border border-zinc-700/50 text-zinc-100 placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all text-sm sm:text-base"
/>
</div>

{/* Type Filter */}
<div className="flex items-center gap-2 overflow-x-auto pb-2 lg:pb-0">
{FILE_TYPE_FILTERS.map((filter) => (
<button
key={filter.value}
onClick={() => setTypeFilter(filter.value)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${
typeFilter === filter.value
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30'
: 'bg-zinc-800/50 text-zinc-400 border border-zinc-700/50 hover:bg-zinc-700/50'
}`}
>
{filter.icon}
{filter.label}
</button>
))}
</div>
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 sm:items-center">
{/* Type Filter */}
<div className="flex items-center gap-2 overflow-x-auto pb-2 sm:pb-0 -mx-1 px-1">
{FILE_TYPE_FILTERS.map((filter) => (
<button
key={filter.value}
onClick={() => setTypeFilter(filter.value)}
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-2 rounded-lg text-xs sm:text-sm font-medium whitespace-nowrap transition-all ${
typeFilter === filter.value
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30'
: 'bg-zinc-800/50 text-zinc-400 border border-zinc-700/50 hover:bg-zinc-700/50'
}`}
>
{filter.icon}
<span className="hidden sm:inline">{filter.label}</span>
</button>
))}
</div>

{/* View Toggle */}
<div className="flex items-center gap-1 bg-zinc-800/50 rounded-lg p-1 border border-zinc-700/50">
<div className="flex items-center gap-1 bg-zinc-800/50 rounded-lg p-1 border border-zinc-700/50 ml-auto sm:ml-0">
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-md transition-all ${
Expand All @@ -441,19 +442,20 @@ export default function FilesPage() {
<Grid className="w-4 h-4" />
</button>
</div>
</div>
</div>
</CardContent>
</AnimatedCard>

{/* Files */}
<AnimatedCard delay={0.2}>
<CardHeader className="flex flex-row items-center justify-between">
<h3 className="text-lg font-semibold text-zinc-100">
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<h3 className="text-base sm:text-lg font-semibold text-zinc-100">
{filteredFiles.length} fichier(s)
</h3>
<div className="flex items-center gap-2 text-sm text-zinc-500">
<Filter className="w-4 h-4" />
<span>Trier par:</span>
<div className="flex items-center gap-2 text-xs sm:text-sm text-zinc-500 overflow-x-auto">
<Filter className="w-4 h-4 hidden sm:block" />
<span className="hidden sm:inline">Trier par:</span>
<button
onClick={() => toggleSort('created_at')}
className={`flex items-center gap-1 px-2 py-1 rounded ${
Expand Down Expand Up @@ -587,7 +589,7 @@ export default function FilesPage() {
</div>
) : (
/* Grid View */
<div className="p-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<div className="p-4 sm:p-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 sm:gap-4">
{filteredFiles.map((file: CDNFile, index: number) => {
const mimeType = file.mime_type || file.mimeType;
const isImage = mimeType?.startsWith('image/');
Expand Down
7 changes: 5 additions & 2 deletions cdn-dashboard/src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Sidebar } from '@/components/sidebar';
import { MobileNav } from '@/components/mobile-nav';
import { useAuthStore } from '@/lib/auth';
import { cn } from '@/lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
Expand Down Expand Up @@ -79,6 +80,7 @@ export default function DashboardLayout({
</div>

<Sidebar />
<MobileNav />

<AnimatePresence mode="wait">
<motion.main
Expand All @@ -89,10 +91,11 @@ export default function DashboardLayout({
transition={{ duration: 0.3 }}
className={cn(
'transition-all duration-300',
'ml-64 min-h-screen relative z-10'
'lg:ml-64 min-h-screen relative z-10',
'pt-16 lg:pt-0'
)}
>
<div className="p-8">
<div className="p-4 sm:p-6 lg:p-8">
{children}
</div>
</motion.main>
Expand Down
66 changes: 33 additions & 33 deletions cdn-dashboard/src/app/dashboard/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,12 @@ export default function LogsPage() {
}, [data?.data]);

return (
<PageTransition className="space-y-6">
<PageTransition className="space-y-4 sm:space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-zinc-100">Logs d&apos;accès</h1>
<p className="text-zinc-500 mt-1">Historique des téléchargements et accès</p>
<h1 className="text-xl sm:text-2xl font-bold text-zinc-100">Logs d&apos;accès</h1>
<p className="text-sm text-zinc-500 mt-1">Historique des téléchargements et accès</p>
</div>
<div className="flex items-center gap-2">
{filteredLogs.length > 0 && (
Expand Down Expand Up @@ -236,55 +236,55 @@ export default function LogsPage() {
</div>

{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<AnimatedCard delay={0}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 flex items-center justify-center">
<Download className="w-5 h-5 text-emerald-400" />
<CardContent className="p-3 sm:p-4">
<div className="flex items-center gap-2 sm:gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-emerald-500/20 flex items-center justify-center">
<Download className="w-4 h-4 sm:w-5 sm:h-5 text-emerald-400" />
</div>
<div>
<p className="text-2xl font-bold text-zinc-100">{stats.downloads}</p>
<p className="text-xs text-zinc-500">Téléchargements</p>
<p className="text-xl sm:text-2xl font-bold text-zinc-100">{stats.downloads}</p>
<p className="text-[10px] sm:text-xs text-zinc-500">Téléchargements</p>
</div>
</div>
</CardContent>
</AnimatedCard>
<AnimatedCard delay={0.05}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-indigo-500/20 flex items-center justify-center">
<Upload className="w-5 h-5 text-indigo-400" />
<CardContent className="p-3 sm:p-4">
<div className="flex items-center gap-2 sm:gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-indigo-500/20 flex items-center justify-center">
<Upload className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-400" />
</div>
<div>
<p className="text-2xl font-bold text-zinc-100">{stats.uploads}</p>
<p className="text-xs text-zinc-500">Uploads</p>
<p className="text-xl sm:text-2xl font-bold text-zinc-100">{stats.uploads}</p>
<p className="text-[10px] sm:text-xs text-zinc-500">Uploads</p>
</div>
</div>
</CardContent>
</AnimatedCard>
<AnimatedCard delay={0.1}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-red-500/20 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-red-400" />
<CardContent className="p-3 sm:p-4">
<div className="flex items-center gap-2 sm:gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-red-500/20 flex items-center justify-center">
<AlertTriangle className="w-4 h-4 sm:w-5 sm:h-5 text-red-400" />
</div>
<div>
<p className="text-2xl font-bold text-zinc-100">{stats.errors}</p>
<p className="text-xs text-zinc-500">Erreurs</p>
<p className="text-xl sm:text-2xl font-bold text-zinc-100">{stats.errors}</p>
<p className="text-[10px] sm:text-xs text-zinc-500">Erreurs</p>
</div>
</div>
</CardContent>
</AnimatedCard>
<AnimatedCard delay={0.15}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center">
<Globe className="w-5 h-5 text-amber-400" />
<CardContent className="p-3 sm:p-4">
<div className="flex items-center gap-2 sm:gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-amber-500/20 flex items-center justify-center">
<Globe className="w-4 h-4 sm:w-5 sm:h-5 text-amber-400" />
</div>
<div>
<p className="text-2xl font-bold text-zinc-100">{stats.uniqueIPs}</p>
<p className="text-xs text-zinc-500">IPs uniques</p>
<p className="text-xl sm:text-2xl font-bold text-zinc-100">{stats.uniqueIPs}</p>
<p className="text-[10px] sm:text-xs text-zinc-500">IPs uniques</p>
</div>
</div>
</CardContent>
Expand All @@ -293,17 +293,17 @@ export default function LogsPage() {

{/* Search & Filters */}
<AnimatedCard delay={0.2}>
<CardContent className="p-4">
<div className="flex flex-col lg:flex-row gap-4">
<CardContent className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
{/* Search */}
<div className="relative flex-1 group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600 group-focus-within:text-indigo-400 transition-colors" />
<input
type="text"
placeholder="Rechercher par fichier, IP, action..."
placeholder="Rechercher..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-zinc-800/50 border border-zinc-700/50 text-zinc-100 placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all"
className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-zinc-800/50 border border-zinc-700/50 text-zinc-100 placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all text-sm"
/>
</div>

Expand Down
26 changes: 13 additions & 13 deletions cdn-dashboard/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ function StatsCard({
}) {
return (
<AnimatedCard delay={delay} glow="indigo">
<div className="p-6">
<div className="p-4 sm:p-6">
<div className="flex items-start justify-between">
<div className="space-y-2">
<p className="text-sm font-medium text-zinc-400">{title}</p>
<div className="text-3xl font-bold text-zinc-100">
<div className="space-y-1 sm:space-y-2">
<p className="text-xs sm:text-sm font-medium text-zinc-400">{title}</p>
<div className="text-2xl sm:text-3xl font-bold text-zinc-100">
<AnimatedNumber value={value} />
</div>
{subtitle && (
<p className="text-sm text-zinc-500">{subtitle}</p>
<p className="text-xs sm:text-sm text-zinc-500">{subtitle}</p>
)}
{trend && (
<div className="flex items-center gap-1.5 mt-2">
Expand Down Expand Up @@ -173,13 +173,13 @@ export default function DashboardPage() {
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="flex items-center justify-between"
className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"
>
<div>
<h1 className="text-2xl font-bold text-zinc-100">Dashboard</h1>
<p className="text-zinc-500 mt-1">Vue d&apos;ensemble de votre CDN</p>
<h1 className="text-xl sm:text-2xl font-bold text-zinc-100">Dashboard</h1>
<p className="text-sm sm:text-base text-zinc-500 mt-1">Vue d&apos;ensemble de votre CDN</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 sm:gap-4 flex-wrap">
<ConnectionIndicator status={status} />
<Button
variant="ghost"
Expand All @@ -189,18 +189,18 @@ export default function DashboardPage() {
disabled={isFetching}
>
<RefreshCw className={`w-4 h-4 ${isFetching ? 'animate-spin' : ''}`} />
{isFetching ? 'Actualisation...' : 'Actualiser'}
<span className="hidden sm:inline">{isFetching ? 'Actualisation...' : 'Actualiser'}</span>
</Button>
{lastUpdate && (
<span className="text-xs text-zinc-500">
<span className="text-xs text-zinc-500 hidden sm:inline">
{formatRelativeTime(lastUpdate.toISOString())}
</span>
)}
</div>
</motion.div>

{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
<StatsCard
title="Fichiers"
value={data?.files.total || 0}
Expand Down Expand Up @@ -236,7 +236,7 @@ export default function DashboardPage() {
</div>

{/* Charts & Activity */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Downloads Chart */}
<AnimatedCard delay={0.2} className="lg:col-span-2">
<div className="p-6">
Expand Down
19 changes: 10 additions & 9 deletions cdn-dashboard/src/app/dashboard/tokens/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,31 +115,32 @@ export default function TokensPage() {
});

return (
<PageTransition className="space-y-6">
<PageTransition className="space-y-4 sm:space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-zinc-100">Tokens d&apos;accès</h1>
<p className="text-zinc-500 mt-1">Gérez les tokens de téléchargement</p>
<h1 className="text-xl sm:text-2xl font-bold text-zinc-100">Tokens d&apos;accès</h1>
<p className="text-sm text-zinc-500 mt-1">Gérez les tokens de téléchargement</p>
</div>
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Nouveau token
<span className="hidden sm:inline">Nouveau token</span>
<span className="sm:hidden">Nouveau</span>
</Button>
</div>

{/* Search & Filters */}
<AnimatedCard delay={0.1}>
<CardContent className="p-4">
<CardContent className="p-3 sm:p-4">
<div className="flex items-center gap-4">
<div className="relative flex-1 group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600 group-focus-within:text-indigo-400 transition-colors" />
<input
type="text"
placeholder="Rechercher par token ou ID fichier..."
placeholder="Rechercher..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-zinc-800/50 border border-zinc-700/50 text-zinc-100 placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all"
className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-zinc-800/50 border border-zinc-700/50 text-zinc-100 placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all text-sm"
/>
</div>
</div>
Expand All @@ -149,7 +150,7 @@ export default function TokensPage() {
{/* Tokens List */}
<AnimatedCard delay={0.2}>
<CardHeader>
<h3 className="text-lg font-semibold text-zinc-100">
<h3 className="text-base sm:text-lg font-semibold text-zinc-100">
{data?.pagination?.total || 0} token(s)
</h3>
</CardHeader>
Expand Down
Loading
Loading