From e760e48fc6269b1d64985044f7edcd07ed1db6b5 Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:03:46 +0300 Subject: [PATCH] refactor(frontend): use @project-graphite/ui for the theme and shared components The design tokens, base styles and generic components now come from the shared graphite-ui package, installed from its v0.1.0 GitHub release. Only the poster, catalogue and cast styles and the tracker-specific components stay here. Shell is built on the package's AppShell, the account menu on Menu, the notification bell on Popover and the year picker on GridListbox. The compiled CSS is rule-for-rule the same, apart from renamed generic classes and values that now come from variables. --- app/frontend/package.json | 3 + app/frontend/src/App.tsx | 110 ++- app/frontend/src/api.ts | 10 +- app/frontend/src/catalogSearch.ts | 3 +- app/frontend/src/components/AccountMenu.tsx | 66 +- app/frontend/src/components/CatalogCard.tsx | 5 +- app/frontend/src/components/ConfirmDialog.tsx | 49 - app/frontend/src/components/Credits.tsx | 2 +- app/frontend/src/components/Dialog.tsx | 50 - app/frontend/src/components/EmptyState.tsx | 16 - app/frontend/src/components/Field.tsx | 58 -- app/frontend/src/components/Icon.tsx | 20 - app/frontend/src/components/LibraryCard.tsx | 2 +- .../src/components/LibraryEntryEditor.tsx | 4 +- .../src/components/NotificationBell.tsx | 123 ++- .../src/components/NotificationList.tsx | 8 +- app/frontend/src/components/Outage.tsx | 95 -- app/frontend/src/components/Pagination.tsx | 40 - .../src/components/PasswordDialog.tsx | 4 +- app/frontend/src/components/Poster.tsx | 2 +- .../src/components/ResendVerification.tsx | 3 +- app/frontend/src/components/ReviewCard.tsx | 3 +- app/frontend/src/components/ReviewEditor.tsx | 4 +- app/frontend/src/components/Shell.tsx | 218 ++--- app/frontend/src/components/Skeleton.tsx | 144 +-- app/frontend/src/components/SmoothImage.tsx | 35 - app/frontend/src/components/TitleReviews.tsx | 2 +- app/frontend/src/components/Toggle.tsx | 40 - app/frontend/src/components/YearPicker.tsx | 122 +-- app/frontend/src/inbox.tsx | 21 +- .../src/pages/AccountSettingsPage.tsx | 5 +- app/frontend/src/pages/AdminPage.tsx | 16 +- app/frontend/src/pages/DiscoverPage.tsx | 8 +- app/frontend/src/pages/ImportBatchPage.tsx | 15 +- app/frontend/src/pages/ImportPage.tsx | 3 +- app/frontend/src/pages/LibraryPage.tsx | 7 +- app/frontend/src/pages/LoginPage.tsx | 3 +- .../src/pages/NotificationSettingsPage.tsx | 3 +- app/frontend/src/pages/NotificationsPage.tsx | 7 +- app/frontend/src/pages/PasswordPages.tsx | 4 +- app/frontend/src/pages/ProfilePage.tsx | 6 +- app/frontend/src/pages/RegisterPage.tsx | 3 +- app/frontend/src/pages/SearchPage.tsx | 3 +- app/frontend/src/pages/SettingsPage.tsx | 14 +- app/frontend/src/pages/SourcesPage.tsx | 3 +- app/frontend/src/pages/TitleDetailsPage.tsx | 5 +- app/frontend/src/pages/TrackGamesPage.tsx | 6 +- app/frontend/src/pages/UnsubscribePage.tsx | 3 +- app/frontend/src/pages/VerifyPage.tsx | 3 +- app/frontend/src/snackbar.tsx | 101 -- app/frontend/src/styles/global.css | 902 +----------------- app/frontend/src/useDismiss.ts | 19 - app/frontend/src/useResource.ts | 3 +- app/frontend/test/skeletons.spec.tsx | 2 +- app/package-lock.json | 19 +- 55 files changed, 350 insertions(+), 2075 deletions(-) delete mode 100644 app/frontend/src/components/ConfirmDialog.tsx delete mode 100644 app/frontend/src/components/Dialog.tsx delete mode 100644 app/frontend/src/components/EmptyState.tsx delete mode 100644 app/frontend/src/components/Field.tsx delete mode 100644 app/frontend/src/components/Icon.tsx delete mode 100644 app/frontend/src/components/Outage.tsx delete mode 100644 app/frontend/src/components/Pagination.tsx delete mode 100644 app/frontend/src/components/SmoothImage.tsx delete mode 100644 app/frontend/src/components/Toggle.tsx delete mode 100644 app/frontend/src/snackbar.tsx delete mode 100644 app/frontend/src/useDismiss.ts diff --git a/app/frontend/package.json b/app/frontend/package.json index 98ee2d4..751c2d8 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -23,5 +23,8 @@ "typescript": "6.0.2", "vite": "8.3.0", "vitest": "5.0.1" + }, + "dependencies": { + "@project-graphite/ui": "https://github.com/project-graphite/graphite-ui/releases/download/v0.1.0/project-graphite-ui-0.1.0.tgz" } } diff --git a/app/frontend/src/App.tsx b/app/frontend/src/App.tsx index 6d60fe5..eed5f0d 100644 --- a/app/frontend/src/App.tsx +++ b/app/frontend/src/App.tsx @@ -1,16 +1,18 @@ import type { ReactNode } from 'react'; -import { Navigate, Route, Routes, useLocation, useParams, type Location } from 'react-router'; -import { useAuth } from './auth'; -import { catalogSections, discoverCategories } from './catalog'; -import { Shell } from './components/Shell'; +import { Link, Navigate, Route, Routes, useLocation, useParams, type Location } from 'react-router'; import { FormPanelSkeleton, FormSkeleton, ListSkeleton, PageSkeleton, - PosterGridSkeleton, TabsSkeleton, -} from './components/Skeleton'; + UiProvider, + type UiLinkProps, +} from '@project-graphite/ui'; +import { useAuth } from './auth'; +import { catalogSections, discoverCategories } from './catalog'; +import { Shell } from './components/Shell'; +import { PosterGridSkeleton } from './components/Skeleton'; import { AccountSettingsPage } from './pages/AccountSettingsPage'; import { AdminPage } from './pages/AdminPage'; import { DiscoverPage } from './pages/DiscoverPage'; @@ -105,53 +107,59 @@ function SignedOutLogin() { return auth.user ? : ; } +function RouterLink({ href, ...props }: UiLinkProps) { + return ; +} + export function App() { return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - - - } - > - - - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + > + + + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + ); } diff --git a/app/frontend/src/api.ts b/app/frontend/src/api.ts index 0724fdb..94e8181 100644 --- a/app/frontend/src/api.ts +++ b/app/frontend/src/api.ts @@ -1,3 +1,5 @@ +import { isAbortError } from '@project-graphite/ui'; + export class ApiError extends Error { constructor( message: string, @@ -35,14 +37,6 @@ function reportOutage() { const gatewayStatuses = [500, 502, 503, 504]; -export function isAbortError(reason: unknown) { - return reason instanceof DOMException && reason.name === 'AbortError'; -} - -export function errorMessage(reason: unknown, fallback: string) { - return reason instanceof Error ? reason.message : fallback; -} - export async function apiRequest( path: string, init: RequestInit = {}, diff --git a/app/frontend/src/catalogSearch.ts b/app/frontend/src/catalogSearch.ts index 60c37fb..319abb3 100644 --- a/app/frontend/src/catalogSearch.ts +++ b/app/frontend/src/catalogSearch.ts @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router'; -import { apiRequest, errorMessage } from './api'; +import { errorMessage } from '@project-graphite/ui'; +import { apiRequest } from './api'; import { titleHref, type CatalogCategory } from './catalog'; export function sourceLink(value: string) { diff --git a/app/frontend/src/components/AccountMenu.tsx b/app/frontend/src/components/AccountMenu.tsx index aa39c4e..7fdceb7 100644 --- a/app/frontend/src/components/AccountMenu.tsx +++ b/app/frontend/src/components/AccountMenu.tsx @@ -1,59 +1,29 @@ -import { useCallback, useRef, useState } from 'react'; -import { Link } from 'react-router'; +import { Menu } from '@project-graphite/ui'; import { useAuth } from '../auth'; -import { useDismiss } from '../useDismiss'; export function AccountMenu({ onSignOut }: { onSignOut: () => void }) { const auth = useAuth(); - const [open, setOpen] = useState(false); - const root = useRef(null); - const close = useCallback(() => setOpen(false), []); - useDismiss(open, root, close); if (!auth.user) return null; const { displayName, handle, role } = auth.user; - const items = [ - [`/users/${handle}`, 'Your profile'], - [`/users/${handle}/settings`, 'Settings'], - ['/notifications', 'Notifications'], - ...(role === 'member' ? [] : [['/admin', 'Admin']]), - ]; return ( -
- - {open && ( -
-
-

{displayName}

-

@{handle}

-
- {items.map(([to, label]) => ( - - {label} - - ))} - + +

{displayName}

+

@{handle}

- )} -
+ } + items={[ + { label: 'Your profile', href: `/users/${handle}` }, + { label: 'Settings', href: `/users/${handle}/settings` }, + { label: 'Notifications', href: '/notifications' }, + ...(role === 'member' ? [] : [{ label: 'Admin', href: '/admin' }]), + { label: 'Sign out', onSelect: onSignOut, separated: true }, + ]} + label="Account" + trigger={displayName.charAt(0).toUpperCase()} + triggerLabel={`Account menu for ${displayName}`} + /> ); } diff --git a/app/frontend/src/components/CatalogCard.tsx b/app/frontend/src/components/CatalogCard.tsx index 7eaf472..ae21d29 100644 --- a/app/frontend/src/components/CatalogCard.tsx +++ b/app/frontend/src/components/CatalogCard.tsx @@ -1,7 +1,7 @@ import { useState, type FormEvent } from 'react'; import { Link, useNavigate } from 'react-router'; +import { actionSkeletonClass, Dialog, errorMessage, Skeleton, useSnackbar } from '@project-graphite/ui'; import { useAdultBlur } from '../adultContent'; -import { errorMessage } from '../api'; import { useAuth } from '../auth'; import { catalogRef, titleHref, type CatalogCandidate } from '../catalog'; import { @@ -10,11 +10,8 @@ import { type LibraryEntry, type LibraryState, } from '../library'; -import { useSnackbar } from '../snackbar'; import { useLibraryStates } from '../useLibraryStates'; -import { Dialog } from './Dialog'; import { Poster, posterGridClass } from './Poster'; -import { actionSkeletonClass, Skeleton } from './Skeleton'; function AddToListDialog({ item, diff --git a/app/frontend/src/components/ConfirmDialog.tsx b/app/frontend/src/components/ConfirmDialog.tsx deleted file mode 100644 index b1161cd..0000000 --- a/app/frontend/src/components/ConfirmDialog.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useState, type ReactNode } from 'react'; -import { errorMessage } from '../api'; -import { Dialog } from './Dialog'; - -export function ConfirmDialog({ - children, - confirmLabel, - eyebrow, - onClose, - onConfirm, - title, -}: { - children: ReactNode; - confirmLabel: string; - eyebrow: string; - onClose: () => void; - onConfirm: () => Promise; - title: string; -}) { - const [busy, setBusy] = useState(false); - const [error, setError] = useState(''); - - async function confirm() { - setBusy(true); - setError(''); - try { - await onConfirm(); - onClose(); - } catch (reason) { - setError(errorMessage(reason, 'Could not finish this')); - setBusy(false); - } - } - - return ( - -
{children}
- {error &&

{error}

} -
- - -
-
- ); -} diff --git a/app/frontend/src/components/Credits.tsx b/app/frontend/src/components/Credits.tsx index 474d7f3..0949eca 100644 --- a/app/frontend/src/components/Credits.tsx +++ b/app/frontend/src/components/Credits.tsx @@ -1,5 +1,5 @@ +import { SmoothImage } from '@project-graphite/ui'; import type { CatalogCategory, CatalogDetails } from '../catalog'; -import { SmoothImage } from './SmoothImage'; const headings: Record = { movie: 'Cast & crew', diff --git a/app/frontend/src/components/Dialog.tsx b/app/frontend/src/components/Dialog.tsx deleted file mode 100644 index 6b316fc..0000000 --- a/app/frontend/src/components/Dialog.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useId, useRef, type ReactNode } from 'react'; - -export function Dialog({ - children, - eyebrow, - onClose, - title, -}: { - children: ReactNode; - eyebrow: string; - onClose: () => void; - title: string; -}) { - const dialog = useRef(null); - const titleId = useId(); - - useEffect(() => { - dialog.current?.showModal(); - }, []); - - return ( - { - event.preventDefault(); - onClose(); - }} - onClick={(event) => { - if (event.target === event.currentTarget) onClose(); - }} - ref={dialog} - > -
-
-
-

{eyebrow}

-

- {title} -

-
- -
- {children} -
-
- ); -} diff --git a/app/frontend/src/components/EmptyState.tsx b/app/frontend/src/components/EmptyState.tsx deleted file mode 100644 index cb29709..0000000 --- a/app/frontend/src/components/EmptyState.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { ReactNode } from 'react'; - -export function EmptyState({ - children, - title, -}: { - children?: ReactNode; - title: string; -}) { - return ( -
-

{title}

- {children} -
- ); -} diff --git a/app/frontend/src/components/Field.tsx b/app/frontend/src/components/Field.tsx deleted file mode 100644 index fc9b9c5..0000000 --- a/app/frontend/src/components/Field.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useId, type InputHTMLAttributes, type ReactNode, type TextareaHTMLAttributes } from 'react'; - -function describedBy(id: string, error?: string, hint?: ReactNode) { - return error ? `${id}-error` : hint ? `${id}-hint` : undefined; -} - -function FieldMessage({ error, hint, id }: { error?: string; hint?: ReactNode; id: string }) { - return error ? ( - - {error} - - ) : hint ? ( - - {hint} - - ) : null; -} - -export function TextField({ - className = '', - error, - hint, - label, - ...input -}: InputHTMLAttributes & { error?: string; hint?: ReactNode; label: ReactNode }) { - const id = useId(); - return ( - - ); -} - -export function TextAreaField({ - error, - hint, - label, - ...textarea -}: TextareaHTMLAttributes & { error?: string; hint?: ReactNode; label: ReactNode }) { - const id = useId(); - return ( -