From 7cdc72b6e1dc86ee3c3c32ff47691f31378dfbe8 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Fri, 6 Mar 2026 16:21:48 +0100 Subject: [PATCH] feat(backoffice): redesign UI with enhanced background and glassmorphism effects - Add vibrant radial gradient backgrounds with multiple color zones - Implement glassmorphism effects on header and cards - Add subtle grain texture overlay - Update card hover effects with smooth transitions - Improve dark mode background visibility --- apps/backoffice/app/books/[id]/page.tsx | 64 ++--- apps/backoffice/app/books/page.tsx | 79 +++--- apps/backoffice/app/components/BookCard.tsx | 28 +-- .../backoffice/app/components/JobProgress.tsx | 8 +- apps/backoffice/app/components/JobRow.tsx | 8 +- .../app/components/JobsIndicator.tsx | 159 +++++++----- apps/backoffice/app/components/JobsList.tsx | 26 +- .../app/components/LibraryActions.tsx | 18 +- .../app/components/LibrarySubPageHeader.tsx | 111 +++++++++ .../app/components/MonitoringForm.tsx | 10 +- apps/backoffice/app/components/ui/Badge.tsx | 104 ++++++-- apps/backoffice/app/components/ui/Button.tsx | 87 ++++++- apps/backoffice/app/components/ui/Card.tsx | 135 +++++++++- apps/backoffice/app/components/ui/Form.tsx | 107 +++++++- apps/backoffice/app/components/ui/Input.tsx | 178 +++++++++++-- .../app/components/ui/Pagination.tsx | 77 +++--- .../app/components/ui/ProgressBar.tsx | 114 ++++++++- apps/backoffice/app/components/ui/StatBox.tsx | 16 +- apps/backoffice/app/components/ui/index.ts | 24 +- apps/backoffice/app/globals.css | 233 ++++++++++++++---- apps/backoffice/app/jobs/[id]/page.tsx | 153 +++++++----- apps/backoffice/app/jobs/page.tsx | 92 ++++--- apps/backoffice/app/layout.tsx | 87 ++++--- .../app/libraries/[id]/books/page.tsx | 54 ++-- .../app/libraries/[id]/series/page.tsx | 46 ++-- apps/backoffice/app/libraries/page.tsx | 198 ++++++++------- apps/backoffice/app/page.tsx | 50 ++-- apps/backoffice/app/theme-toggle.tsx | 115 ++++++++- apps/backoffice/app/tokens/page.tsx | 94 ++++--- apps/backoffice/next-env.d.ts | 2 +- 30 files changed, 1783 insertions(+), 694 deletions(-) create mode 100644 apps/backoffice/app/components/LibrarySubPageHeader.tsx diff --git a/apps/backoffice/app/books/[id]/page.tsx b/apps/backoffice/app/books/[id]/page.tsx index c04afe5..be817f4 100644 --- a/apps/backoffice/app/books/[id]/page.tsx +++ b/apps/backoffice/app/books/[id]/page.tsx @@ -33,14 +33,14 @@ export default async function BookDetailPage({ return ( <>
- + ← Back to books
-
+
{`Cover
-
+

{book.title}

{book.author && ( -

by {book.author}

+

by {book.author}

)} {book.series && ( -

+

{book.series} - {book.volume && Volume {book.volume}} + {book.volume && Volume {book.volume}}

)}
-
- Format: +
+ Format: {book.kind.toUpperCase()}
{book.volume && ( -
- Volume: +
+ Volume: {book.volume}
)} {book.language && ( -
- Language: +
+ Language: {book.language.toUpperCase()}
)} {book.page_count && ( -
- Pages: +
+ Pages: {book.page_count}
)} -
- Library: +
+ Library: {library?.name || book.library_id}
{book.series && ( -
- Series: +
+ Series: {book.series}
)} {book.file_format && ( -
- File Format: +
+ File Format: {book.file_format.toUpperCase()}
)} {book.file_parse_status && ( -
- Parse Status: +
+ Parse Status: {book.file_parse_status} @@ -131,25 +131,25 @@ export default async function BookDetailPage({ )} {book.file_path && ( -
- File Path: +
+ File Path: {book.file_path}
)} -
- Book ID: +
+ Book ID: {book.id}
-
- Library ID: +
+ Library ID: {book.library_id}
{book.updated_at && (
- Updated: + Updated: {new Date(book.updated_at).toLocaleString()}
)} diff --git a/apps/backoffice/app/books/page.tsx b/apps/backoffice/app/books/page.tsx index 005b93c..4996f55 100644 --- a/apps/backoffice/app/books/page.tsx +++ b/apps/backoffice/app/books/page.tsx @@ -1,6 +1,6 @@ import { fetchBooks, searchBooks, fetchLibraries, BookDto, LibraryDto, getBookCoverUrl } from "../../lib/api"; import { BooksGrid, EmptyState } from "../components/BookCard"; -import { Card, Button, FormField, FormInput, FormSelect, FormRow, CursorPagination } from "../components/ui"; +import { Card, CardContent, Button, FormField, FormInput, FormSelect, FormRow, CursorPagination } from "../components/ui"; import Link from "next/link"; export const dynamic = "force-dynamic"; @@ -26,7 +26,6 @@ export default async function BooksPage({ let totalHits: number | null = null; if (searchQuery) { - // Mode recherche const searchResponse = await searchBooks(searchQuery, libraryId, limit).catch(() => null); if (searchResponse) { searchResults = searchResponse.hits.map(hit => ({ @@ -47,7 +46,6 @@ export default async function BooksPage({ totalHits = searchResponse.estimated_total_hits; } } else { - // Mode liste avec pagination const booksPage = await fetchBooks(libraryId, undefined, cursor, limit).catch(() => ({ items: [] as BookDto[], next_cursor: null, @@ -55,7 +53,6 @@ export default async function BooksPage({ })); books = booksPage.items; nextCursor = booksPage.next_cursor; - // Note: L'API ne supporte pas encore prev_cursor, on gère ça côté UI } const displayBooks = (searchResults || books).map(book => ({ @@ -64,27 +61,34 @@ export default async function BooksPage({ })); const hasNextPage = !!nextCursor; - const hasPrevPage = !!cursor; // Si on a un cursor, on peut revenir en arrière (simplifié) + const hasPrevPage = !!cursor; return ( <> -

- - Books -

+
+

+ + + + Books +

+
- {/* Filtres et recherche */} + {/* Search Bar - Style compact et propre */} -
- - + + + + - + + {libraries.map((lib) => ( @@ -94,22 +98,40 @@ export default async function BooksPage({ ))} - - {searchQuery && ( - - ✕ Clear - - )} - -
+
+ + {searchQuery && ( + + Clear + + )} +
+ +
- {/* Résultats de recherche */} + {/* Résultats */} {searchQuery && totalHits !== null && ( -

+

Found {totalHits} result{totalHits !== 1 ? 's' : ''} for "{searchQuery}"

)} @@ -119,7 +141,6 @@ export default async function BooksPage({ <> - {/* Pagination */} {!searchQuery && ( +
{/* Skeleton */}
-
-
+ /> {/* Image */} {alt} {book.author && ( -

{book.author}

+

{book.author}

)} {book.series && ( -

+

{book.series} {book.volume && #{book.volume}}

@@ -76,14 +74,14 @@ export function BookCard({ book }: BookCardProps) {
{book.kind} {book.language && ( - + {book.language} )} @@ -114,12 +112,12 @@ interface EmptyStateProps { export function EmptyState({ message }: EmptyStateProps) { return (
-
+
-

{message}

+

{message}

); } diff --git a/apps/backoffice/app/components/JobProgress.tsx b/apps/backoffice/app/components/JobProgress.tsx index 48b8066..e12f5f3 100644 --- a/apps/backoffice/app/components/JobProgress.tsx +++ b/apps/backoffice/app/components/JobProgress.tsx @@ -70,7 +70,7 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { if (error) { return ( -
+
Error: {error}
); @@ -78,7 +78,7 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { if (!progress) { return ( -
+
Loading progress...
); @@ -89,7 +89,7 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { const total = progress.total_files ?? 0; return ( -
+
{isComplete && ( @@ -99,7 +99,7 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { -
+
{processed} / {total} files {progress.current_file && ( diff --git a/apps/backoffice/app/components/JobRow.tsx b/apps/backoffice/app/components/JobRow.tsx index ebd36ae..55b46df 100644 --- a/apps/backoffice/app/components/JobRow.tsx +++ b/apps/backoffice/app/components/JobRow.tsx @@ -62,7 +62,7 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo return ( <> - + - + {duration} - + {formatDate(job.created_at)} @@ -143,7 +143,7 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo {showProgress && (job.status === "running" || job.status === "pending") && ( - + ( + + + + +); + +const SpinnerIcon = ({ className }: { className?: string }) => ( + + + + +); + +const ChevronIcon = ({ className }: { className?: string }) => ( + + + +); + export function JobsIndicator() { const [activeJobs, setActiveJobs] = useState([]); const [isOpen, setIsOpen] = useState(false); @@ -67,13 +91,18 @@ export function JobsIndicator() { return ( - - - - + ); } @@ -81,56 +110,61 @@ export function JobsIndicator() { return (
- {/* Popin/Dropdown */} + {/* Popin/Dropdown with glassmorphism */} {isOpen && ( -
-
+
+ {/* Header */} +
- 📊 + 📊

Active Jobs

-

+

{runningJobs.length > 0 ? `${runningJobs.length} running, ${pendingJobs.length} pending` : `${pendingJobs.length} job${pendingJobs.length !== 1 ? 's' : ''} pending` @@ -149,33 +183,29 @@ export function JobsIndicator() { {/* Overall progress bar if running */} {runningJobs.length > 0 && ( -

+
- Overall Progress + Overall Progress {Math.round(totalProgress)}%
-
-
-
+
)} -
+ {/* Job List */} +
{activeJobs.length === 0 ? ( -
+

No active jobs

) : ( -
    +
      {activeJobs.map(job => (
    • setIsOpen(false)} >
      @@ -186,37 +216,30 @@ export function JobsIndicator() {
      - {job.id.slice(0, 8)} - + {job.id.slice(0, 8)} + {job.type} - +
      {job.status === "running" && job.progress_percent !== null && (
      -
      -
      -
      - {job.progress_percent}% + + {job.progress_percent}%
      )} {job.current_file && ( -

      +

      📄 {job.current_file}

      )} {job.stats_json && ( -
      +
      ✓ {job.stats_json.indexed_files} {job.stats_json.errors > 0 && ( - ⚠ {job.stats_json.errors} + ⚠ {job.stats_json.errors} )}
      )} @@ -230,11 +253,23 @@ export function JobsIndicator() {
      {/* Footer */} -
      -

      Auto-refreshing every 2s

      +
      +

      Auto-refreshing every 2s

      )}
      ); } + +// Mini progress bar for dropdown +function MiniProgressBar({ value }: { value: number }) { + return ( +
      +
      +
      + ); +} diff --git a/apps/backoffice/app/components/JobsList.tsx b/apps/backoffice/app/components/JobsList.tsx index 1e977d2..704d239 100644 --- a/apps/backoffice/app/components/JobsList.tsx +++ b/apps/backoffice/app/components/JobsList.tsx @@ -2,7 +2,6 @@ import { useState, useEffect } from "react"; import { JobRow } from "./JobRow"; -import { MiniProgressBar } from "./ui"; interface Job { id: string; @@ -45,18 +44,15 @@ function formatDate(dateStr: string): string { const now = new Date(); const diff = now.getTime() - date.getTime(); - // Less than 1 hour: show relative if (diff < 3600000) { const mins = Math.floor(diff / 60000); if (mins < 1) return "Just now"; return `${mins}m ago`; } - // Less than 24 hours: show hours if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours}h ago`; } - // Otherwise: show date return date.toLocaleDateString(); } @@ -105,22 +101,22 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro }; return ( -
      +
      - - - - - - - - - + + + + + + + + + - + {jobs.map((job) => ( setIsOpen(!isOpen)} - className={isOpen ? "bg-muted/10" : ""} + className={isOpen ? "bg-accent" : ""} > - ⚙️ + + + + {isOpen && ( -
      +
      @@ -86,7 +88,7 @@ export function LibraryActions({ name="monitor_enabled" value="true" defaultChecked={monitorEnabled} - className="w-4 h-4 rounded border-line text-primary focus:ring-primary" + className="w-4 h-4 rounded border-border text-primary focus:ring-ring" /> Auto Scan @@ -99,7 +101,7 @@ export function LibraryActions({ name="watcher_enabled" value="true" defaultChecked={watcherEnabled} - className="w-4 h-4 rounded border-line text-primary focus:ring-primary" + className="w-4 h-4 rounded border-border text-primary focus:ring-ring" /> File Watcher ⚡ @@ -110,7 +112,7 @@ export function LibraryActions({ Auto @@ -58,14 +58,14 @@ export function MonitoringForm({ libraryId, monitorEnabled, scanMode, watcherEna isPending ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary' - } ${watcherEnabled ? 'bg-warning-soft border-warning text-warning' : 'bg-card border-line text-muted'}`}> + } ${watcherEnabled ? 'bg-warning/10 border-warning text-warning' : 'bg-card border-border text-muted-foreground'}`}> @@ -74,7 +74,7 @@ export function MonitoringForm({ libraryId, monitorEnabled, scanMode, watcherEna name="scan_mode" defaultValue={scanMode} disabled={isPending} - className="px-3 py-1.5 text-sm rounded-lg border border-line bg-card text-foreground focus:ring-2 focus:ring-primary focus:border-primary disabled:opacity-50" + className="px-3 py-1.5 text-sm rounded-lg border border-border bg-card text-foreground focus:ring-2 focus:ring-primary focus:border-primary disabled:opacity-50" > diff --git a/apps/backoffice/app/components/ui/Badge.tsx b/apps/backoffice/app/components/ui/Badge.tsx index 6d037ce..b467049 100644 --- a/apps/backoffice/app/components/ui/Badge.tsx +++ b/apps/backoffice/app/components/ui/Badge.tsx @@ -1,61 +1,113 @@ -type BadgeVariant = "default" | "primary" | "success" | "warning" | "error" | "muted"; +import { ReactNode } from "react"; + +type BadgeVariant = + | "default" + | "primary" + | "secondary" + | "destructive" + | "outline" + | "success" + | "warning" + | "error" + | "muted" + | "unread" + | "in-progress" + | "completed"; interface BadgeProps { - children: React.ReactNode; + children: ReactNode; variant?: BadgeVariant; className?: string; } const variantStyles: Record = { - default: "bg-muted/20 text-muted", - primary: "bg-primary-soft text-primary", - success: "bg-success-soft text-success", - warning: "bg-warning-soft text-warning", - error: "bg-error-soft text-error", - muted: "bg-muted/10 text-muted", + // shadcn/ui compatible + default: "bg-primary/90 text-primary-foreground border-transparent hover:bg-primary/80 backdrop-blur-md", + secondary: "bg-secondary/80 text-secondary-foreground border-transparent hover:bg-secondary/60 backdrop-blur-md", + destructive: "bg-destructive/90 text-destructive-foreground border-transparent hover:bg-destructive/80 backdrop-blur-md", + outline: "text-foreground border-border bg-background/50", + + // Legacy + Additional variants + primary: "bg-primary/90 text-primary-foreground backdrop-blur-md", + success: "bg-success/90 text-success-foreground backdrop-blur-md", + warning: "bg-warning/90 text-white backdrop-blur-md", + error: "bg-destructive/90 text-destructive-foreground backdrop-blur-md", + muted: "bg-muted/60 text-muted-foreground backdrop-blur-md", + + // Status badges from StripStream + unread: "badge-unread backdrop-blur-md", + "in-progress": "badge-in-progress backdrop-blur-md", + completed: "badge-completed backdrop-blur-md", }; export function Badge({ children, variant = "default", className = "" }: BadgeProps) { return ( - + {children} ); } -type StatusVariant = "running" | "success" | "failed" | "cancelled" | "pending"; +// Status badge for jobs/tasks +const statusVariants: Record = { + running: "in-progress", + success: "completed", + completed: "completed", + failed: "error", + cancelled: "muted", + pending: "warning", + unread: "unread", +}; interface StatusBadgeProps { status: string; className?: string; } -const statusVariants: Record = { - running: "primary", - success: "success", - failed: "error", - cancelled: "muted", - pending: "warning", -}; - export function StatusBadge({ status, className = "" }: StatusBadgeProps) { - const variant = statusVariants[status as StatusVariant] || "default"; + const variant = statusVariants[status.toLowerCase()] || "default"; return {status}; } -type JobTypeVariant = "rebuild" | "full_rebuild"; +// Job type badge +const jobTypeVariants: Record = { + rebuild: "primary", + full_rebuild: "warning", +}; interface JobTypeBadgeProps { type: string; className?: string; } -const jobTypeVariants: Record = { - rebuild: "primary", - full_rebuild: "warning", -}; - export function JobTypeBadge({ type, className = "" }: JobTypeBadgeProps) { - const variant = jobTypeVariants[type as JobTypeVariant] || "default"; + const variant = jobTypeVariants[type.toLowerCase()] || "default"; return {type}; } + +// Progress badge (shows percentage) +interface ProgressBadgeProps { + progress: number; + className?: string; +} + +export function ProgressBadge({ progress, className = "" }: ProgressBadgeProps) { + let variant: BadgeVariant = "unread"; + if (progress === 100) variant = "completed"; + else if (progress > 0) variant = "in-progress"; + + return ( + + {progress}% + + ); +} diff --git a/apps/backoffice/app/components/ui/Button.tsx b/apps/backoffice/app/components/ui/Button.tsx index f05e79f..3e11387 100644 --- a/apps/backoffice/app/components/ui/Button.tsx +++ b/apps/backoffice/app/components/ui/Button.tsx @@ -1,6 +1,15 @@ import { ButtonHTMLAttributes, ReactNode } from "react"; -type ButtonVariant = "primary" | "secondary" | "danger" | "warning" | "ghost"; +type ButtonVariant = + | "default" + | "destructive" + | "outline" + | "secondary" + | "ghost" + | "link" + | "primary" + | "danger" + | "warning"; interface ButtonProps extends ButtonHTMLAttributes { children: ReactNode; @@ -9,22 +18,29 @@ interface ButtonProps extends ButtonHTMLAttributes { } const variantStyles: Record = { - primary: "bg-primary text-white hover:bg-primary/90", - secondary: "border border-line text-muted hover:bg-muted/5", - danger: "bg-error text-white hover:bg-error/90", - warning: "bg-warning text-white hover:bg-warning/90", - ghost: "text-muted hover:text-foreground hover:bg-muted/5", + // shadcn/ui compatible variants + default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/85 shadow-sm", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + + // Legacy variants (mapped to new ones for compatibility) + primary: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md", + danger: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm", + warning: "bg-warning text-white hover:bg-warning/90 shadow-sm", }; const sizeStyles: Record = { - sm: "h-8 px-3 text-xs", - md: "h-10 px-4 text-sm", - lg: "h-12 px-6 text-base", + sm: "h-9 px-3 text-xs rounded-md", + md: "h-10 px-4 py-2 text-sm rounded-md", + lg: "h-11 px-8 text-base rounded-md", }; export function Button({ children, - variant = "primary", + variant = "default", size = "md", className = "", disabled, @@ -33,8 +49,12 @@ export function Button({ return ( + ); +} diff --git a/apps/backoffice/app/components/ui/Card.tsx b/apps/backoffice/app/components/ui/Card.tsx index c3cd8c3..91230e9 100644 --- a/apps/backoffice/app/components/ui/Card.tsx +++ b/apps/backoffice/app/components/ui/Card.tsx @@ -3,25 +3,146 @@ import { ReactNode } from "react"; interface CardProps { children: ReactNode; className?: string; + hover?: boolean; } -export function Card({ children, className = "" }: CardProps) { +export function Card({ children, className = "", hover = true }: CardProps) { return ( -
      +
      {children}
      ); } interface CardHeaderProps { - title: string; + children: ReactNode; className?: string; } -export function CardHeader({ title, className = "" }: CardHeaderProps) { +export function CardHeader({ children, className = "" }: CardHeaderProps) { return ( -

      - {title} -

      +
      + {children} +
      + ); +} + +interface CardTitleProps { + children: ReactNode; + className?: string; +} + +export function CardTitle({ children, className = "" }: CardTitleProps) { + return ( +

      + {children} +

      + ); +} + +interface CardDescriptionProps { + children: ReactNode; + className?: string; +} + +export function CardDescription({ children, className = "" }: CardDescriptionProps) { + return ( +

      + {children} +

      + ); +} + +interface CardContentProps { + children: ReactNode; + className?: string; +} + +export function CardContent({ children, className = "" }: CardContentProps) { + return ( +
      + {children} +
      + ); +} + +interface CardFooterProps { + children: ReactNode; + className?: string; +} + +export function CardFooter({ children, className = "" }: CardFooterProps) { + return ( +
      + {children} +
      + ); +} + +// Glass Card variant for special sections +interface GlassCardProps { + children: ReactNode; + className?: string; +} + +export function GlassCard({ children, className = "" }: GlassCardProps) { + return ( +
      + {children} +
      + ); +} + +// Simple card with header shortcut +interface SimpleCardProps { + title?: string; + description?: string; + children: ReactNode; + className?: string; + footer?: ReactNode; +} + +export function SimpleCard({ + title, + description, + children, + className = "", + footer +}: SimpleCardProps) { + return ( + + {(title || description) && ( + + {title && {title}} + {description && {description}} + + )} + + {children} + + {footer && ( + + {footer} + + )} + ); } diff --git a/apps/backoffice/app/components/ui/Form.tsx b/apps/backoffice/app/components/ui/Form.tsx index 444e388..a7f075f 100644 --- a/apps/backoffice/app/components/ui/Form.tsx +++ b/apps/backoffice/app/components/ui/Form.tsx @@ -1,45 +1,81 @@ import { ReactNode, LabelHTMLAttributes, InputHTMLAttributes, SelectHTMLAttributes } from "react"; +// Form Field Container interface FormFieldProps { children: ReactNode; className?: string; } export function FormField({ children, className = "" }: FormFieldProps) { - return
      {children}
      ; + return
      {children}
      ; } +// Form Label interface FormLabelProps extends LabelHTMLAttributes { children: ReactNode; + required?: boolean; } -export function FormLabel({ children, className = "", ...props }: FormLabelProps) { +export function FormLabel({ children, required, className = "", ...props }: FormLabelProps) { return ( -
      IDLibraryTypeStatusFilesDurationCreatedActions
      IDLibraryTypeStatusFilesDurationCreatedActions