From 482bd9b0d2550a1bc60f75003b7c2c7e0915a33f Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 17 Oct 2025 11:49:28 +0200 Subject: [PATCH] feat: refactor UI components to utilize new Container, Section, and StatusBadge components for improved layout and styling consistency across the application --- .../[libraryId]/ClientLibraryPage.tsx | 60 +++---- src/components/admin/UsersTable.tsx | 37 ++--- src/components/auth/LoginForm.tsx | 53 ++---- src/components/auth/RegisterForm.tsx | 47 ++---- src/components/common/CompactModeButton.tsx | 20 ++- src/components/common/UnreadFilterButton.tsx | 17 +- src/components/home/HomeContent.tsx | 10 +- src/components/home/MediaRow.tsx | 84 +++------- src/components/layout/Header.tsx | 16 +- src/components/layout/Sidebar.tsx | 112 ++++++------- .../library/PaginatedSeriesGrid.tsx | 5 +- .../reader/components/ControlButtons.tsx | 114 ++++++------- src/components/series/SeriesHeader.tsx | 29 ++-- .../settings/BackgroundSettings.tsx | 21 +-- src/components/settings/CacheSettings.tsx | 23 +-- src/components/settings/DisplaySettings.tsx | 154 +++++++++--------- src/components/settings/KomgaSettings.tsx | 23 +-- src/components/ui/container.tsx | 47 ++++++ src/components/ui/icon-button.tsx | 33 ++++ src/components/ui/nav-button.tsx | 39 +++++ src/components/ui/scroll-container.tsx | 105 ++++++++++++ src/components/ui/section.tsx | 45 +++++ src/components/ui/status-badge.tsx | 44 +++++ 23 files changed, 669 insertions(+), 469 deletions(-) create mode 100644 src/components/ui/container.tsx create mode 100644 src/components/ui/icon-button.tsx create mode 100644 src/components/ui/nav-button.tsx create mode 100644 src/components/ui/scroll-container.tsx create mode 100644 src/components/ui/section.tsx create mode 100644 src/components/ui/status-badge.tsx diff --git a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx index 0a2c519..b0c8a85 100644 --- a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx +++ b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx @@ -9,6 +9,8 @@ import { OptimizedSkeleton } from "@/components/skeletons/OptimizedSkeletons"; import type { LibraryResponse } from "@/types/library"; import type { KomgaSeries, KomgaLibrary } from "@/types/komga"; import type { UserPreferences } from "@/types/preferences"; +import { Container } from "@/components/ui/container"; +import { Section } from "@/components/ui/section"; interface ClientLibraryPageProps { currentPage: number; @@ -148,7 +150,7 @@ export function ClientLibraryPage({ if (loading) { return ( -
+
@@ -158,49 +160,49 @@ export function ClientLibraryPage({ ))}
-
+ ); } if (error) { return ( -
-
-

- {library?.name || t("series.empty")} -

- -
+ +
} + /> -
+ ); } if (!library || !series) { return ( -
+ -
+ ); } return ( -
-
-

{library.name}

-
- {series.totalElements > 0 && ( -

- {t("series.display.showing", { - start: ((currentPage - 1) * effectivePageSize) + 1, - end: Math.min(currentPage * effectivePageSize, series.totalElements), - total: series.totalElements, - })} -

- )} - -
-
+ +
+ {series.totalElements > 0 && ( +

+ {t("series.display.showing", { + start: ((currentPage - 1) * effectivePageSize) + 1, + end: Math.min(currentPage * effectivePageSize, series.totalElements), + total: series.totalElements, + })} +

+ )} + +
+ } + /> - + ); } diff --git a/src/components/admin/UsersTable.tsx b/src/components/admin/UsersTable.tsx index 7b2f4c0..de0b70a 100644 --- a/src/components/admin/UsersTable.tsx +++ b/src/components/admin/UsersTable.tsx @@ -16,6 +16,7 @@ import type { AdminUserData } from "@/lib/services/admin.service"; import { EditUserDialog } from "./EditUserDialog"; import { DeleteUserDialog } from "./DeleteUserDialog"; import { ResetPasswordDialog } from "./ResetPasswordDialog"; +import { StatusBadge } from "@/components/ui/status-badge"; interface UsersTableProps { users: AdminUserData[]; @@ -29,7 +30,7 @@ export function UsersTable({ users, onUserUpdated }: UsersTableProps) { return ( <> -
+
@@ -67,28 +68,24 @@ export function UsersTable({ users, onUserUpdated }: UsersTableProps) { {user.hasKomgaConfig ? ( -
- - Configuré -
+ + Configuré + ) : ( -
- - Non configuré -
+ + Non configuré + )}
{user.hasPreferences ? ( -
- - Oui -
+ + Oui + ) : ( -
- - Non -
+ + Non + )}
{user._count?.favorites || 0} @@ -134,7 +131,7 @@ export function UsersTable({ users, onUserUpdated }: UsersTableProps) { !open && setEditingUser(null)} + onOpenChange={(open: boolean) => !open && setEditingUser(null)} onSuccess={() => { setEditingUser(null); onUserUpdated(); @@ -146,7 +143,7 @@ export function UsersTable({ users, onUserUpdated }: UsersTableProps) { !open && setResettingPasswordUser(null)} + onOpenChange={(open: boolean) => !open && setResettingPasswordUser(null)} onSuccess={() => { setResettingPasswordUser(null); }} @@ -157,7 +154,7 @@ export function UsersTable({ users, onUserUpdated }: UsersTableProps) { !open && setDeletingUser(null)} + onOpenChange={(open: boolean) => !open && setDeletingUser(null)} onSuccess={() => { setDeletingUser(null); onUserUpdated(); diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 096d359..918a27c 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -4,6 +4,10 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { signIn } from "next-auth/react"; import { useTranslate } from "@/hooks/useTranslate"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; interface LoginFormProps { from?: string; @@ -49,66 +53,37 @@ export function LoginForm({ from }: LoginFormProps) { return (
- - {t("login.form.email")} +
- - {t("login.form.password")} +
- -
- {error && ( -
- {error} -
- )} - + ); } diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index 576a2d5..0c3e1e3 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -6,6 +6,9 @@ import { signIn } from "next-auth/react"; import { ErrorMessage } from "@/components/ui/ErrorMessage"; import { useTranslate } from "@/hooks/useTranslate"; import type { AppErrorType } from "@/types/global"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; interface RegisterFormProps { from?: string; @@ -88,61 +91,33 @@ export function RegisterForm({ from: _from }: RegisterFormProps) { return (
- - + +
- - {t("login.form.password")} +
- - {t("login.form.confirmPassword")} +
{error && } - + ); } diff --git a/src/components/common/CompactModeButton.tsx b/src/components/common/CompactModeButton.tsx index 9f68272..ca645de 100644 --- a/src/components/common/CompactModeButton.tsx +++ b/src/components/common/CompactModeButton.tsx @@ -1,6 +1,7 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences"; import { useTranslate } from "@/hooks/useTranslate"; import { LayoutGrid, LayoutTemplate } from "lucide-react"; +import { Button } from "@/components/ui/button"; interface CompactModeButtonProps { onToggle?: (isCompact: boolean) => void; @@ -16,16 +17,19 @@ export function CompactModeButton({ onToggle }: CompactModeButtonProps) { onToggle?.(newCompactState); }; + const Icon = isCompact ? LayoutTemplate : LayoutGrid; + const label = isCompact ? t("series.filters.normal") : t("series.filters.compact"); + return ( - + + {label} + ); } diff --git a/src/components/common/UnreadFilterButton.tsx b/src/components/common/UnreadFilterButton.tsx index 37936c4..9e15c09 100644 --- a/src/components/common/UnreadFilterButton.tsx +++ b/src/components/common/UnreadFilterButton.tsx @@ -2,6 +2,7 @@ import { useTranslate } from "@/hooks/useTranslate"; import { Filter } from "lucide-react"; +import { Button } from "@/components/ui/button"; interface UnreadFilterButtonProps { showOnlyUnread: boolean; @@ -11,16 +12,18 @@ interface UnreadFilterButtonProps { export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterButtonProps) { const { t } = useTranslate(); + const label = showOnlyUnread ? t("series.filters.showAll") : t("series.filters.unread"); + return ( - + {label} + ); } diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx index 45b1874..11df128 100644 --- a/src/components/home/HomeContent.tsx +++ b/src/components/home/HomeContent.tsx @@ -81,7 +81,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) { } + icon={LibraryBig} /> )} @@ -89,7 +89,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) { } + icon={BookOpen} /> )} @@ -97,7 +97,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) { } + icon={Clock} /> )} @@ -105,7 +105,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) { } + icon={Sparkles} /> )} @@ -113,7 +113,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) { } + icon={History} /> )} diff --git a/src/components/home/MediaRow.tsx b/src/components/home/MediaRow.tsx index 18f2901..aac0f5c 100644 --- a/src/components/home/MediaRow.tsx +++ b/src/components/home/MediaRow.tsx @@ -1,12 +1,14 @@ "use client"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import type { KomgaBook, KomgaSeries } from "@/types/komga"; import { BookCover } from "../ui/book-cover"; import { SeriesCover } from "../ui/series-cover"; import { useTranslate } from "@/hooks/useTranslate"; +import { ScrollContainer } from "@/components/ui/scroll-container"; +import { Section } from "@/components/ui/section"; +import type { LucideIcon } from "lucide-react"; +import { Card } from "@/components/ui/card"; interface BaseItem { id: string; @@ -36,13 +38,10 @@ interface OptimizedBook extends BaseItem { interface MediaRowProps { title: string; items: (OptimizedSeries | OptimizedBook)[]; - icon?: React.ReactNode; + icon?: LucideIcon; } export function MediaRow({ title, items, icon }: MediaRowProps) { - const scrollContainerRef = useRef(null); - const [showLeftArrow, setShowLeftArrow] = useState(false); - const [showRightArrow, setShowRightArrow] = useState(true); const router = useRouter(); const { t } = useTranslate(); @@ -51,64 +50,21 @@ export function MediaRow({ title, items, icon }: MediaRowProps) { router.push(path); }; - const handleScroll = () => { - if (!scrollContainerRef.current) return; - - const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current; - setShowLeftArrow(scrollLeft > 0); - setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 10); - }; - - const scroll = (direction: "left" | "right") => { - if (!scrollContainerRef.current) return; - - const scrollAmount = direction === "left" ? -400 : 400; - scrollContainerRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" }); - }; - if (!items.length) return null; return ( -
-
- {icon} -

{title}

-
-
- {/* Bouton de défilement gauche */} - {showLeftArrow && ( - - )} - - {/* Conteneur défilant */} -
- {items.map((item) => ( - onItemClick?.(item)} /> - ))} -
- - {/* Bouton de défilement droit */} - {showRightArrow && ( - - )} -
-
+
+ + {items.map((item) => ( + onItemClick?.(item)} /> + ))} + +
); } @@ -128,9 +84,9 @@ function MediaCard({ item, onClick }: MediaCardProps) { : ""); return ( -
{isSeries ? ( @@ -154,6 +110,6 @@ function MediaCard({ item, onClick }: MediaCardProps) { )}
-
+ ); } diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index cc5819a..b35422b 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -2,6 +2,7 @@ import { Menu, Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import LanguageSelector from "@/components/LanguageSelector"; import { useTranslation } from "react-i18next"; +import { IconButton } from "@/components/ui/icon-button"; interface HeaderProps { onToggleSidebar: () => void; @@ -18,16 +19,15 @@ export function Header({ onToggleSidebar }: HeaderProps) { return (
- + />
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index af634f3..1a1f928 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -12,6 +12,8 @@ import { ERROR_CODES } from "@/constants/errorCodes"; import { getErrorMessage } from "@/utils/errors"; import { useToast } from "@/components/ui/use-toast"; import { useTranslate } from "@/hooks/useTranslate"; +import { NavButton } from "@/components/ui/nav-button"; +import { IconButton } from "@/components/ui/icon-button"; interface SidebarProps { isOpen: boolean; @@ -180,17 +182,13 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites, u {t("sidebar.navigation")} {mainNavItems.map((item) => ( - + /> ))}
@@ -213,17 +211,14 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites, u ) : ( favorites.map((series) => ( - + className="[&_svg]:fill-yellow-400 [&_svg]:text-yellow-400" + /> )) )} @@ -235,14 +230,16 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites, u

{t("sidebar.libraries.title")}

- + tooltip={t("sidebar.libraries.refresh")} + iconClassName={cn(isRefreshing && "animate-spin")} + className="h-8 w-8" + /> {isRefreshing ? (
@@ -254,17 +251,13 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites, u
) : ( libraries.map((library) => ( - + /> )) )} @@ -275,50 +268,37 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites, u

{t("sidebar.settings.title")}

- - + /> {userIsAdmin && ( - + /> )}
- + className="text-destructive hover:bg-destructive/10 hover:text-destructive" + />
); diff --git a/src/components/library/PaginatedSeriesGrid.tsx b/src/components/library/PaginatedSeriesGrid.tsx index 03afa06..d1aa0cc 100644 --- a/src/components/library/PaginatedSeriesGrid.tsx +++ b/src/components/library/PaginatedSeriesGrid.tsx @@ -11,6 +11,7 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences"; import { PageSizeSelect } from "@/components/common/PageSizeSelect"; import { CompactModeButton } from "@/components/common/CompactModeButton"; import { UnreadFilterButton } from "@/components/common/UnreadFilterButton"; +import { Container } from "@/components/ui/container"; interface PaginatedSeriesGridProps { series: KomgaSeries[]; @@ -97,7 +98,7 @@ export function PaginatedSeriesGrid({ }; return ( -
+
@@ -124,6 +125,6 @@ export function PaginatedSeriesGrid({ className="order-1 sm:order-2" />
-
+ ); } diff --git a/src/components/reader/components/ControlButtons.tsx b/src/components/reader/components/ControlButtons.tsx index a522fec..5c2fc42 100644 --- a/src/components/reader/components/ControlButtons.tsx +++ b/src/components/reader/components/ControlButtons.tsx @@ -14,6 +14,7 @@ import { import { cn } from "@/lib/utils"; import { PageInput } from "./PageInput"; import { useTranslation } from "react-i18next"; +import { IconButton } from "@/components/ui/icon-button"; export const ControlButtons = ({ showControls, @@ -40,7 +41,7 @@ export const ControlButtons = ({ {/* Boutons de contrôle */}
{ @@ -48,72 +49,69 @@ export const ControlButtons = ({ onToggleControls(); }} > - - - - -
e.stopPropagation()}> + iconClassName="h-6 w-6" + className={cn("rounded-full", showThumbnails && "ring-2 ring-primary")} + /> +
e.stopPropagation()}> { e.stopPropagation(); onClose(currentPage); }} + tooltip={t("reader.controls.close")} + iconClassName="h-6 w-6" className={cn( - "absolute top-4 right-4 p-2 rounded-full bg-background/70 backdrop-blur-md hover:bg-background/80 transition-all duration-300 z-30", + "absolute top-4 right-4 rounded-full bg-background/70 backdrop-blur-md hover:bg-background/80 transition-all duration-300 z-30", showControls ? "opacity-100" : "opacity-0 pointer-events-none" )} - aria-label={t("reader.controls.close")} - > - - + /> )} {/* Bouton précédent */} {currentPage > 1 && ( - + /> )} {/* Bouton suivant */} {currentPage < totalPages && ( - + /> )} ); diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx index 5304ab4..9e762c6 100644 --- a/src/components/series/SeriesHeader.tsx +++ b/src/components/series/SeriesHeader.tsx @@ -3,7 +3,6 @@ import { Book, BookOpen, BookMarked, Star, StarOff } from "lucide-react"; import type { KomgaSeries } from "@/types/komga"; import { useState, useEffect } from "react"; -import { Button } from "../ui/button"; import { useToast } from "@/components/ui/use-toast"; import { RefreshButton } from "@/components/library/RefreshButton"; import { AppError } from "@/utils/errors"; @@ -11,6 +10,8 @@ import { ERROR_CODES } from "@/constants/errorCodes"; import { getErrorMessage } from "@/utils/errors"; import { useTranslate } from "@/hooks/useTranslate"; import { SeriesCover } from "@/components/ui/series-cover"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { IconButton } from "@/components/ui/icon-button"; interface SeriesHeaderProps { series: KomgaSeries; @@ -93,7 +94,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => { if (booksReadCount === booksCount) { return { label: t("series.header.status.read"), - className: "bg-green-500/10 text-green-500", + status: "success" as const, icon: BookMarked, }; } @@ -104,14 +105,14 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => { read: booksReadCount, total: booksCount, }), - className: "bg-blue-500/10 text-blue-500", + status: "reading" as const, icon: BookOpen, }; } return { label: t("series.header.status.unread"), - className: "bg-yellow-500/10 text-yellow-500", + status: "unread" as const, icon: Book, }; }; @@ -151,30 +152,24 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {

)}
- - + {statusInfo.label} - + {series.booksCount === 1 ? t("series.header.books", { count: series.booksCount }) : t("series.header.books_plural", { count: series.booksCount }) } - + iconClassName={isFavorite ? "fill-yellow-400 text-yellow-400" : ""} + />
diff --git a/src/components/settings/BackgroundSettings.tsx b/src/components/settings/BackgroundSettings.tsx index 8fd9227..d6c58a9 100644 --- a/src/components/settings/BackgroundSettings.tsx +++ b/src/components/settings/BackgroundSettings.tsx @@ -11,6 +11,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { GRADIENT_PRESETS } from "@/types/preferences"; import type { BackgroundType } from "@/types/preferences"; import { Check } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; export function BackgroundSettings() { const { t } = useTranslate(); @@ -94,16 +95,12 @@ export function BackgroundSettings() { }; return ( -
-
-
-

- {t("settings.background.title")} -

-

- {t("settings.background.description")} -

-
+ + + {t("settings.background.title")} + {t("settings.background.description")} + +
{/* Type de background */} @@ -188,8 +185,8 @@ export function BackgroundSettings() {
)}
-
-
+ + ); } diff --git a/src/components/settings/CacheSettings.tsx b/src/components/settings/CacheSettings.tsx index e2fe977..995ed12 100644 --- a/src/components/settings/CacheSettings.tsx +++ b/src/components/settings/CacheSettings.tsx @@ -7,6 +7,7 @@ import { Trash2, Loader2, HardDrive } from "lucide-react"; import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch"; import { Label } from "@/components/ui/label"; import type { TTLConfigData } from "@/types/komga"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; interface CacheSettingsProps { initialTTLConfig: TTLConfigData | null; @@ -186,15 +187,15 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { }; return ( -
-
-
-

- - {t("settings.cache.title")} -

-

{t("settings.cache.description")}

-
+ + + + + {t("settings.cache.title")} + + {t("settings.cache.description")} + +
@@ -370,7 +371,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
-
-
+ + ); } diff --git a/src/components/settings/DisplaySettings.tsx b/src/components/settings/DisplaySettings.tsx index 78eb334..0322190 100644 --- a/src/components/settings/DisplaySettings.tsx +++ b/src/components/settings/DisplaySettings.tsx @@ -3,6 +3,7 @@ import { usePreferences } from "@/contexts/PreferencesContext"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { useToast } from "@/components/ui/use-toast"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; export function DisplaySettings() { const { t } = useTranslate(); @@ -27,87 +28,82 @@ export function DisplaySettings() { }; return ( -
-
-
-

- {t("settings.display.title")} -

-

{t("settings.display.description")}

+ + + {t("settings.display.title")} + {t("settings.display.description")} + + +
+
+ +

+ {t("settings.display.thumbnails.description")} +

+
+
- -
-
-
- -

- {t("settings.display.thumbnails.description")} -

-
- -
-
-
- -

- {t("settings.display.unreadFilter.description")} -

-
- { - try { - await updatePreferences({ showOnlyUnread: checked }); - toast({ - title: t("settings.title"), - description: t("settings.komga.messages.configSaved"), - }); - } catch (error) { - console.error("Erreur détaillée:", error); - toast({ - variant: "destructive", - title: t("settings.error.title"), - description: t("settings.error.message"), - }); - } - }} - /> -
-
-
- -

- {t("settings.display.debugMode.description")} -

-
- { - try { - await updatePreferences({ debug: checked }); - toast({ - title: t("settings.title"), - description: t("settings.komga.messages.configSaved"), - }); - } catch (error) { - console.error("Erreur détaillée:", error); - toast({ - variant: "destructive", - title: t("settings.error.title"), - description: t("settings.error.message"), - }); - } - }} - /> +
+
+ +

+ {t("settings.display.unreadFilter.description")} +

+ { + try { + await updatePreferences({ showOnlyUnread: checked }); + toast({ + title: t("settings.title"), + description: t("settings.komga.messages.configSaved"), + }); + } catch (error) { + console.error("Erreur détaillée:", error); + toast({ + variant: "destructive", + title: t("settings.error.title"), + description: t("settings.error.message"), + }); + } + }} + />
-
-
+
+
+ +

+ {t("settings.display.debugMode.description")} +

+
+ { + try { + await updatePreferences({ debug: checked }); + toast({ + title: t("settings.title"), + description: t("settings.komga.messages.configSaved"), + }); + } catch (error) { + console.error("Erreur détaillée:", error); + toast({ + variant: "destructive", + title: t("settings.error.title"), + description: t("settings.error.message"), + }); + } + }} + /> +
+
+
); } diff --git a/src/components/settings/KomgaSettings.tsx b/src/components/settings/KomgaSettings.tsx index 47e7275..6f64620 100644 --- a/src/components/settings/KomgaSettings.tsx +++ b/src/components/settings/KomgaSettings.tsx @@ -5,6 +5,7 @@ import { useTranslate } from "@/hooks/useTranslate"; import { useToast } from "@/components/ui/use-toast"; import { Network, Loader2 } from "lucide-react"; import type { KomgaConfig } from "@/types/komga"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; interface KomgaSettingsProps { initialConfig: KomgaConfig | null; @@ -144,15 +145,15 @@ export function KomgaSettings({ initialConfig }: KomgaSettingsProps) { }; return ( -
-
-
-

- - {t("settings.komga.title")} -

-

{t("settings.komga.description")}

-
+ + + + + {t("settings.komga.title")} + + {t("settings.komga.description")} + + {!shouldShowForm ? (
@@ -274,7 +275,7 @@ export function KomgaSettings({ initialConfig }: KomgaSettingsProps) {
)} -
-
+ + ); } diff --git a/src/components/ui/container.tsx b/src/components/ui/container.tsx new file mode 100644 index 0000000..8d0f2fd --- /dev/null +++ b/src/components/ui/container.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const containerVariants = cva("mx-auto px-4 sm:px-6 lg:px-8", { + variants: { + size: { + default: "max-w-screen-2xl", + narrow: "max-w-4xl", + wide: "max-w-screen-3xl", + full: "max-w-full", + }, + spacing: { + none: "", + sm: "py-4", + default: "py-8", + lg: "py-12", + }, + }, + defaultVariants: { + size: "default", + spacing: "default", + }, +}); + +export interface ContainerProps + extends React.HTMLAttributes, + VariantProps { + as?: React.ElementType; +} + +const Container = React.forwardRef( + ({ className, size, spacing, as: Component = "div", ...props }, ref) => { + return ( + + ); + } +); + +Container.displayName = "Container"; + +export { Container, containerVariants }; + diff --git a/src/components/ui/icon-button.tsx b/src/components/ui/icon-button.tsx new file mode 100644 index 0000000..3a79c0f --- /dev/null +++ b/src/components/ui/icon-button.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { type LucideIcon } from "lucide-react"; +import { Button, type ButtonProps } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export interface IconButtonProps extends Omit { + icon: LucideIcon; + label?: string; + tooltip?: string; + iconClassName?: string; +} + +const IconButton = React.forwardRef( + ({ icon: Icon, label, tooltip, iconClassName, className, ...props }, ref) => { + return ( + + ); + } +); + +IconButton.displayName = "IconButton"; + +export { IconButton }; + diff --git a/src/components/ui/nav-button.tsx b/src/components/ui/nav-button.tsx new file mode 100644 index 0000000..68dcf47 --- /dev/null +++ b/src/components/ui/nav-button.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import { type LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface NavButtonProps extends React.ButtonHTMLAttributes { + icon: LucideIcon; + label: string; + active?: boolean; + count?: number; +} + +const NavButton = React.forwardRef( + ({ icon: Icon, label, active, count, className, ...props }, ref) => { + return ( + + ); + } +); + +NavButton.displayName = "NavButton"; + +export { NavButton }; + diff --git a/src/components/ui/scroll-container.tsx b/src/components/ui/scroll-container.tsx new file mode 100644 index 0000000..3849b25 --- /dev/null +++ b/src/components/ui/scroll-container.tsx @@ -0,0 +1,105 @@ +"use client"; + +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface ScrollContainerProps extends React.HTMLAttributes { + children: React.ReactNode; + showArrows?: boolean; + scrollAmount?: number; + arrowLeftLabel?: string; + arrowRightLabel?: string; +} + +const ScrollContainer = React.forwardRef( + ( + { + children, + className, + showArrows = true, + scrollAmount = 400, + arrowLeftLabel = "Scroll left", + arrowRightLabel = "Scroll right", + ...props + }, + ref + ) => { + const scrollContainerRef = React.useRef(null); + const [showLeftArrow, setShowLeftArrow] = React.useState(false); + const [showRightArrow, setShowRightArrow] = React.useState(true); + + const handleScroll = React.useCallback(() => { + if (!scrollContainerRef.current) return; + + const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current; + setShowLeftArrow(scrollLeft > 0); + setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 10); + }, []); + + const scroll = React.useCallback( + (direction: "left" | "right") => { + if (!scrollContainerRef.current) return; + + const scrollValue = direction === "left" ? -scrollAmount : scrollAmount; + scrollContainerRef.current.scrollBy({ left: scrollValue, behavior: "smooth" }); + }, + [scrollAmount] + ); + + React.useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + handleScroll(); + + const resizeObserver = new ResizeObserver(handleScroll); + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, [handleScroll]); + + return ( +
+ {showArrows && showLeftArrow && ( + + )} + +
+ {children} +
+ + {showArrows && showRightArrow && ( + + )} +
+ ); + } +); + +ScrollContainer.displayName = "ScrollContainer"; + +export { ScrollContainer }; + diff --git a/src/components/ui/section.tsx b/src/components/ui/section.tsx new file mode 100644 index 0000000..ce907e2 --- /dev/null +++ b/src/components/ui/section.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { type LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface SectionProps extends React.HTMLAttributes { + title?: string; + icon?: LucideIcon; + description?: string; + actions?: React.ReactNode; + headerClassName?: string; +} + +const Section = React.forwardRef( + ( + { title, icon: Icon, description, actions, children, className, headerClassName, ...props }, + ref + ) => { + return ( +
+ {(title || actions) && ( +
+ {title && ( +
+ {Icon && } +
+

{title}

+ {description && ( +

{description}

+ )} +
+
+ )} + {actions &&
{actions}
} +
+ )} + {children} +
+ ); + } +); + +Section.displayName = "Section"; + +export { Section }; + diff --git a/src/components/ui/status-badge.tsx b/src/components/ui/status-badge.tsx new file mode 100644 index 0000000..551a11c --- /dev/null +++ b/src/components/ui/status-badge.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { type LucideIcon } from "lucide-react"; +import { Badge, type BadgeProps } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { cva, type VariantProps } from "class-variance-authority"; + +const statusBadgeVariants = cva("flex items-center gap-1", { + variants: { + status: { + success: "bg-green-500/10 text-green-500 border-green-500/20", + warning: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", + error: "bg-red-500/10 text-red-500 border-red-500/20", + info: "bg-blue-500/10 text-blue-500 border-blue-500/20", + reading: "bg-blue-500/10 text-blue-500 border-blue-500/20", + unread: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", + }, + }, + defaultVariants: { + status: "info", + }, +}); + +export interface StatusBadgeProps + extends Omit, + VariantProps { + icon?: LucideIcon; + children: React.ReactNode; +} + +const StatusBadge = ({ status, icon: Icon, children, className, ...props }: StatusBadgeProps) => { + return ( + + {Icon && } + {children} + + ); +}; + +export { StatusBadge, statusBadgeVariants }; +