theme draft 2 (done?)

This commit is contained in:
2026-03-22 13:36:02 -04:00
parent 6c2c4f6b8b
commit b13f9b362c
18 changed files with 257 additions and 447 deletions

View File

@@ -9,13 +9,11 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated, isCheckingAuth } = useAuth(); const { isAuthenticated, isCheckingAuth } = useAuth();
const location = useLocation(); const location = useLocation();
// Show loading while checking authentication status
if (isCheckingAuth) { if (isCheckingAuth) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
if (!isAuthenticated) { if (!isAuthenticated) {
// Redirect to login with the current location saved
return <Navigate to="/login" state={{ from: location }} replace />; return <Navigate to="/login" state={{ from: location }} replace />;
} }

View File

@@ -9,7 +9,7 @@ interface FieldProps {
export function Field({ label, children, isEditing: _isEditing = false }: FieldProps) { export function Field({ label, children, isEditing: _isEditing = false }: FieldProps) {
return ( return (
<div className="relative rounded"> <div className="relative rounded">
<div className="relative inline-flex gap-2 text-gray-500">{label}</div> <div className="relative inline-flex gap-2 text-content-muted">{label}</div>
{children} {children}
</div> </div>
); );

View File

@@ -90,7 +90,7 @@ export default function Layout() {
{isUserDropdownOpen && ( {isUserDropdownOpen && (
<div className="absolute right-4 top-16 z-20 pt-4 transition duration-200"> <div className="absolute right-4 top-16 z-20 pt-4 transition duration-200">
<div className="w-64 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 dark:shadow-gray-800"> <div className="w-64 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-border/30">
<div <div
className="border-b border-border px-4 py-3" className="border-b border-border px-4 py-3"
role="group" role="group"

View File

@@ -124,10 +124,7 @@ export function getSVGGraphData(
const itemY = svgHeight - itemSize; const itemY = svgHeight - itemSize;
const lineX = (idx + 1) * blockOffset; const lineX = (idx + 1) * blockOffset;
linePoints.push({ linePoints.push({ x: lineX, y: itemY });
x: lineX,
y: itemY,
});
if (lineX > maxBX) { if (lineX > maxBX) {
maxBX = lineX; maxBX = lineX;
@@ -164,8 +161,8 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps)
if (!data || data.length < 2) { if (!data || data.length < 2) {
return ( return (
<div className="relative flex h-24 items-center justify-center bg-gray-100 dark:bg-gray-600"> <div className="relative flex h-24 items-center justify-center bg-surface-muted">
<p className="text-gray-400 dark:text-gray-300">No data available</p> <p className="text-content-subtle">No data available</p>
</div> </div>
); );
} }
@@ -175,8 +172,8 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps)
return ( return (
<div className="relative"> <div className="relative">
<svg viewBox={`26 0 755 ${svgHeight}`} preserveAspectRatio="none" width="100%" height="6em"> <svg viewBox={`26 0 755 ${svgHeight}`} preserveAspectRatio="none" width="100%" height="6em">
<path fill="#316BBE" fillOpacity="0.5" stroke="none" d={`${BezierPath} ${BezierFill}`} /> <path fill="rgb(var(--secondary-600))" fillOpacity="0.5" stroke="none" d={`${BezierPath} ${BezierFill}`} />
<path fill="none" stroke="#316BBE" d={BezierPath} /> <path fill="none" stroke="rgb(var(--secondary-600))" d={BezierPath} />
</svg> </svg>
<div <div
className="absolute top-0 flex size-full" className="absolute top-0 flex size-full"
@@ -196,11 +193,10 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps)
}} }}
> >
<div <div
className="pointer-events-none absolute top-3 flex flex-col items-center rounded p-2 text-xs dark:text-white" className="pointer-events-none absolute top-3 flex flex-col items-center rounded bg-surface/80 p-2 text-xs text-content"
style={{ style={{
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
left: '50%', left: '50%',
backgroundColor: 'rgba(128, 128, 128, 0.2)',
}} }}
> >
<span>{formatDate(point.date)}</span> <span>{formatDate(point.date)}</span>

View File

@@ -15,7 +15,7 @@ export function Skeleton({
height, height,
animation = 'pulse', animation = 'pulse',
}: SkeletonProps) { }: SkeletonProps) {
const baseClasses = 'bg-gray-200 dark:bg-gray-600'; const baseClasses = 'bg-surface-strong';
const variantClasses = { const variantClasses = {
default: 'rounded', default: 'rounded',
@@ -184,7 +184,7 @@ export function PageLoader({ message = 'Loading...', className = '' }: PageLoade
return ( return (
<div className={cn('flex min-h-[400px] flex-col items-center justify-center gap-4', className)}> <div className={cn('flex min-h-[400px] flex-col items-center justify-center gap-4', className)}>
<div className="relative"> <div className="relative">
<div className="size-12 animate-spin rounded-full border-4 border-gray-200 border-t-secondary-500 dark:border-gray-600" /> <div className="size-12 animate-spin rounded-full border-4 border-surface-strong border-t-secondary-500" />
</div> </div>
<p className="text-sm font-medium text-content-muted">{message}</p> <p className="text-sm font-medium text-content-muted">{message}</p>
</div> </div>
@@ -206,7 +206,7 @@ export function InlineLoader({ size = 'md', className = '' }: InlineLoaderProps)
return ( return (
<div className={cn('flex items-center justify-center', className)}> <div className={cn('flex items-center justify-center', className)}>
<div <div
className={`${sizeMap[size]} animate-spin rounded-full border-gray-200 border-t-secondary-500 dark:border-gray-600`} className={`${sizeMap[size]} animate-spin rounded-full border-surface-strong border-t-secondary-500`}
/> />
</div> </div>
); );

View File

@@ -13,10 +13,7 @@ export default function ActivityPage() {
key: 'document_id' as const, key: 'document_id' as const,
header: 'Document', header: 'Document',
render: (_value, row) => ( render: (_value, row) => (
<Link <Link to={`/documents/${row.document_id}`} className="text-secondary-600 hover:underline">
to={`/documents/${row.document_id}`}
className="text-blue-600 hover:underline dark:text-blue-400"
>
{row.author || 'Unknown'} - {row.title || 'Unknown'} {row.author || 'Unknown'} - {row.title || 'Unknown'}
</Link> </Link>
), ),
@@ -29,9 +26,7 @@ export default function ActivityPage() {
{ {
key: 'duration' as const, key: 'duration' as const,
header: 'Duration', header: 'Duration',
render: value => { render: value => formatDuration(typeof value === 'number' ? value : 0),
return formatDuration(typeof value === 'number' ? value : 0);
},
}, },
{ {
key: 'end_percentage' as const, key: 'end_percentage' as const,

View File

@@ -48,7 +48,6 @@ export default function AdminImportPage() {
{ {
onSuccess: _response => { onSuccess: _response => {
showInfo('Import completed successfully'); showInfo('Import completed successfully');
// Redirect to import results page after a short delay
setTimeout(() => { setTimeout(() => {
window.location.href = '/admin/import-results'; window.location.href = '/admin/import-results';
}, 1500); }, 1500);
@@ -65,22 +64,22 @@ export default function AdminImportPage() {
}; };
if (isLoading && !currentPath) { if (isLoading && !currentPath) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
if (selectedDirectory) { if (selectedDirectory) {
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow"> <div className="inline-block min-w-full overflow-hidden rounded shadow">
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"> <div className="flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
<p className="text-lg font-semibold text-gray-500">Selected Import Directory</p> <p className="text-lg font-semibold text-content">Selected Import Directory</p>
<form className="flex flex-col gap-4" onSubmit={handleImport}> <form className="flex flex-col gap-4" onSubmit={handleImport}>
<div className="flex w-full justify-between gap-4"> <div className="flex w-full justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 text-content">
<FolderOpenIcon size={20} /> <FolderOpenIcon size={20} />
<p className="break-all text-lg font-medium">{selectedDirectory}</p> <p className="break-all text-lg font-medium">{selectedDirectory}</p>
</div> </div>
<div className="mr-4 flex flex-col justify-around gap-2"> <div className="mr-4 flex flex-col justify-around gap-2 text-content">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<input <input
type="radio" type="radio"
@@ -124,20 +123,20 @@ export default function AdminImportPage() {
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow"> <div className="inline-block min-w-full overflow-hidden rounded shadow">
<table className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700"> <table className="min-w-full bg-surface text-sm leading-normal text-content">
<thead className="text-gray-800 dark:text-gray-400"> <thead className="text-content-muted">
<tr> <tr>
<th className="w-12 border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"></th> <th className="w-12 border-b border-border p-3 text-left font-normal"></th>
<th className="break-all border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"> <th className="break-all border-b border-border p-3 text-left font-normal">
{currentPath} {currentPath}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="text-black dark:text-white"> <tbody>
{currentPath !== '/' && ( {currentPath !== '/' && (
<tr> <tr>
<td className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"></td> <td className="border-b border-border p-3 text-content-muted"></td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<button onClick={handleNavigateUp}> <button onClick={handleNavigateUp}>
<p>../</p> <p>../</p>
</button> </button>
@@ -153,12 +152,12 @@ export default function AdminImportPage() {
) : ( ) : (
directories.map((item: DirectoryItem) => ( directories.map((item: DirectoryItem) => (
<tr key={item.name}> <tr key={item.name}>
<td className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"> <td className="border-b border-border p-3 text-content-muted">
<button onClick={() => item.name && handleSelectDirectory(item.name)}> <button onClick={() => item.name && handleSelectDirectory(item.name)}>
<FolderOpenIcon size={20} /> <FolderOpenIcon size={20} />
</button> </button>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<button onClick={() => item.name && handleSelectDirectory(item.name)}> <button onClick={() => item.name && handleSelectDirectory(item.name)}>
<p>{item.name ?? ''}</p> <p>{item.name ?? ''}</p>
</button> </button>

View File

@@ -8,27 +8,27 @@ export default function AdminImportResultsPage() {
resultsData?.status === 200 ? (resultsData.data as ImportResultsResponse).results || [] : []; resultsData?.status === 200 ? (resultsData.data as ImportResultsResponse).results || [] : [];
if (isLoading) { if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow"> <div className="inline-block min-w-full overflow-hidden rounded shadow">
<table className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700"> <table className="min-w-full bg-surface text-sm leading-normal text-content">
<thead className="text-gray-800 dark:text-gray-400"> <thead className="text-content-muted">
<tr> <tr>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Document Document
</th> </th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Status Status
</th> </th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Error Error
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="text-black dark:text-white"> <tbody>
{results.length === 0 ? ( {results.length === 0 ? (
<tr> <tr>
<td className="p-3 text-center" colSpan={3}> <td className="p-3 text-center" colSpan={3}>
@@ -39,22 +39,24 @@ export default function AdminImportResultsPage() {
results.map((result: ImportResult, index: number) => ( results.map((result: ImportResult, index: number) => (
<tr key={index}> <tr key={index}>
<td <td
className="grid border-b border-gray-200 p-3" className="grid border-b border-border p-3"
style={{ gridTemplateColumns: '4rem auto' }} style={{ gridTemplateColumns: '4rem auto' }}
> >
<span className="text-gray-800 dark:text-gray-400">Name:</span> <span className="text-content-muted">Name:</span>
{result.id ? ( {result.id ? (
<Link to={`/documents/${result.id}`}>{result.name}</Link> <Link to={`/documents/${result.id}`} className="text-secondary-600 hover:underline">
{result.name}
</Link>
) : ( ) : (
<span>N/A</span> <span>N/A</span>
)} )}
<span className="text-gray-800 dark:text-gray-400">File:</span> <span className="text-content-muted">File:</span>
<span>{result.path}</span> <span>{result.path}</span>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{result.status}</p> <p>{result.status}</p>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{result.error || ''}</p> <p>{result.error || ''}</p>
</td> </td>
</tr> </tr>

View File

@@ -26,18 +26,18 @@ export default function AdminLogsPage() {
return ( return (
<div> <div>
<div className="mb-4 flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"> <div className="mb-4 flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}> <form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
<div className="flex w-full grow flex-col"> <div className="flex w-full grow flex-col">
<div className="relative flex"> <div className="relative flex">
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"> <span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<Search2Icon size={15} hoverable={false} /> <Search2Icon size={15} hoverable={false} />
</span> </span>
<input <input
type="text" type="text"
value={filter} value={filter}
onChange={e => setFilter(e.target.value)} onChange={e => setFilter(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white p-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600" className="w-full flex-1 appearance-none rounded-none border border-border bg-surface p-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
placeholder="JQ Filter" placeholder="JQ Filter"
/> />
</div> </div>
@@ -51,7 +51,7 @@ export default function AdminLogsPage() {
</div> </div>
<div <div
className="flex w-full flex-col-reverse overflow-scroll text-black dark:text-white" className="flex w-full flex-col-reverse overflow-scroll text-content"
style={{ fontFamily: 'monospace' }} style={{ fontFamily: 'monospace' }}
> >
{isLoading ? ( {isLoading ? (

View File

@@ -42,19 +42,14 @@ export default function AdminPage() {
const filename = `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`; const filename = `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`;
// Stream the response directly to disk using File System Access API
// This avoids loading multi-GB files into browser memory
if ('showSaveFilePicker' in window && typeof window.showSaveFilePicker === 'function') { if ('showSaveFilePicker' in window && typeof window.showSaveFilePicker === 'function') {
try { try {
// Modern browsers: Use File System Access API for direct disk writes
const handle = await window.showSaveFilePicker({ const handle = await window.showSaveFilePicker({
suggestedName: filename, suggestedName: filename,
types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }], types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }],
}); });
const writable = await handle.createWritable(); const writable = await handle.createWritable();
// Stream response body directly to file without buffering
const reader = response.body?.getReader(); const reader = response.body?.getReader();
if (!reader) throw new Error('Unable to read response'); if (!reader) throw new Error('Unable to read response');
@@ -67,13 +62,11 @@ export default function AdminPage() {
await writable.close(); await writable.close();
showInfo('Backup completed successfully'); showInfo('Backup completed successfully');
} catch (err) { } catch (err) {
// User cancelled or error
if ((err as Error).name !== 'AbortError') { if ((err as Error).name !== 'AbortError') {
showError('Backup failed: ' + (err as Error).message); showError('Backup failed: ' + (err as Error).message);
} }
} }
} else { } else {
// Fallback for older browsers
showError( showError(
'Your browser does not support large file downloads. Please use Chrome, Edge, or Safari.' 'Your browser does not support large file downloads. Please use Chrome, Edge, or Safari.'
); );
@@ -113,52 +106,34 @@ export default function AdminPage() {
const handleMetadataMatch = () => { const handleMetadataMatch = () => {
postAdminAction.mutate( postAdminAction.mutate(
{ data: { action: 'METADATA_MATCH' } },
{ {
data: { onSuccess: () => showInfo('Metadata matching started'),
action: 'METADATA_MATCH', onError: error => showError('Metadata matching failed: ' + getErrorMessage(error)),
},
},
{
onSuccess: () => {
showInfo('Metadata matching started');
},
onError: error => {
showError('Metadata matching failed: ' + getErrorMessage(error));
},
} }
); );
}; };
const handleCacheTables = () => { const handleCacheTables = () => {
postAdminAction.mutate( postAdminAction.mutate(
{ data: { action: 'CACHE_TABLES' } },
{ {
data: { onSuccess: () => showInfo('Cache tables started'),
action: 'CACHE_TABLES', onError: error => showError('Cache tables failed: ' + getErrorMessage(error)),
},
},
{
onSuccess: () => {
showInfo('Cache tables started');
},
onError: error => {
showError('Cache tables failed: ' + getErrorMessage(error));
},
} }
); );
}; };
if (isLoading) { if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
return ( return (
<div className="flex w-full grow flex-col gap-4"> <div className="flex w-full grow flex-col gap-4">
{/* Backup & Restore Card */} <div className="flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"> <p className="mb-2 text-lg font-semibold text-content">Backup & Restore</p>
<p className="mb-2 text-lg font-semibold">Backup & Restore</p>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Backup Form */} <form className="flex justify-between text-content" onSubmit={handleBackupSubmit}>
<form className="flex justify-between" onSubmit={handleBackupSubmit}>
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<div> <div>
<input <input
@@ -186,8 +161,7 @@ export default function AdminPage() {
</div> </div>
</form> </form>
{/* Restore Form */} <form onSubmit={handleRestoreSubmit} className="flex grow justify-between text-content">
<form onSubmit={handleRestoreSubmit} className="flex grow justify-between">
<div className="flex w-1/2 items-center"> <div className="flex w-1/2 items-center">
<input <input
type="file" type="file"
@@ -205,11 +179,10 @@ export default function AdminPage() {
</div> </div>
</div> </div>
{/* Tasks Card */} <div className="flex grow flex-col rounded bg-surface p-4 text-content-muted shadow-lg">
<div className="flex grow flex-col rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"> <p className="text-lg font-semibold text-content">Tasks</p>
<p className="text-lg font-semibold">Tasks</p> <table className="min-w-full bg-surface text-sm text-content">
<table className="min-w-full bg-white text-sm dark:bg-gray-700"> <tbody>
<tbody className="text-black dark:text-white">
<tr> <tr>
<td className="pl-0"> <td className="pl-0">
<p>Metadata Matching</p> <p>Metadata Matching</p>

View File

@@ -39,9 +39,7 @@ export default function AdminUsersPage() {
setNewIsAdmin(false); setNewIsAdmin(false);
refetch(); refetch();
}, },
onError: error => { onError: error => showError('Failed to create user: ' + getErrorMessage(error)),
showError('Failed to create user: ' + getErrorMessage(error));
},
} }
); );
}; };
@@ -49,19 +47,14 @@ export default function AdminUsersPage() {
const handleDeleteUser = (userId: string) => { const handleDeleteUser = (userId: string) => {
updateUser.mutate( updateUser.mutate(
{ {
data: { data: { operation: 'DELETE', user: userId },
operation: 'DELETE',
user: userId,
},
}, },
{ {
onSuccess: () => { onSuccess: () => {
showInfo('User deleted successfully'); showInfo('User deleted successfully');
refetch(); refetch();
}, },
onError: error => { onError: error => showError('Failed to delete user: ' + getErrorMessage(error)),
showError('Failed to delete user: ' + getErrorMessage(error));
},
} }
); );
}; };
@@ -71,20 +64,14 @@ export default function AdminUsersPage() {
updateUser.mutate( updateUser.mutate(
{ {
data: { data: { operation: 'UPDATE', user: userId, password },
operation: 'UPDATE',
user: userId,
password,
},
}, },
{ {
onSuccess: () => { onSuccess: () => {
showInfo('Password updated successfully'); showInfo('Password updated successfully');
refetch(); refetch();
}, },
onError: error => { onError: error => showError('Failed to update password: ' + getErrorMessage(error)),
showError('Failed to update password: ' + getErrorMessage(error));
},
} }
); );
}; };
@@ -92,50 +79,40 @@ export default function AdminUsersPage() {
const handleToggleAdmin = (userId: string, isAdmin: boolean) => { const handleToggleAdmin = (userId: string, isAdmin: boolean) => {
updateUser.mutate( updateUser.mutate(
{ {
data: { data: { operation: 'UPDATE', user: userId, is_admin: isAdmin },
operation: 'UPDATE',
user: userId,
is_admin: isAdmin,
},
}, },
{ {
onSuccess: () => { onSuccess: () => {
const role = isAdmin ? 'admin' : 'user'; showInfo(`User permissions updated to ${isAdmin ? 'admin' : 'user'}`);
showInfo(`User permissions updated to ${role}`);
refetch(); refetch();
}, },
onError: error => { onError: error => showError('Failed to update admin status: ' + getErrorMessage(error)),
showError('Failed to update admin status: ' + getErrorMessage(error));
},
} }
); );
}; };
if (isLoading) { if (isLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
return ( return (
<div className="relative h-full overflow-x-auto"> <div className="relative h-full overflow-x-auto">
{showAddForm && ( {showAddForm && (
<div className="absolute left-10 top-10 rounded bg-gray-200 p-3 shadow-lg shadow-gray-500 transition-all duration-200 dark:bg-gray-600 dark:shadow-gray-900"> <div className="absolute left-10 top-10 rounded bg-surface-strong p-3 shadow-lg transition-all duration-200">
<form <form onSubmit={handleCreateUser} className="flex flex-col gap-2 text-sm text-content">
onSubmit={handleCreateUser}
className="flex flex-col gap-2 text-sm text-black dark:text-white"
>
<input <input
type="text" type="text"
value={newUsername} value={newUsername}
onChange={e => setNewUsername(e.target.value)} onChange={e => setNewUsername(e.target.value)}
placeholder="Username" placeholder="Username"
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white" className="bg-surface p-2 text-content"
/> />
<input <input
type="password" type="password"
value={newPassword} value={newPassword}
onChange={e => setNewPassword(e.target.value)} onChange={e => setNewPassword(e.target.value)}
placeholder="Password" placeholder="Password"
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white" className="bg-surface p-2 text-content"
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -147,7 +124,7 @@ export default function AdminUsersPage() {
<label htmlFor="new_is_admin">Admin</label> <label htmlFor="new_is_admin">Admin</label>
</div> </div>
<button <button
className="bg-gray-500 px-2 py-1 font-medium text-white hover:bg-gray-800 dark:text-gray-800 dark:hover:bg-gray-100" className="bg-primary-500 px-2 py-1 font-medium text-primary-foreground hover:bg-primary-700"
type="submit" type="submit"
> >
Create Create
@@ -157,29 +134,25 @@ export default function AdminUsersPage() {
)} )}
<div className="min-w-full overflow-scroll rounded shadow"> <div className="min-w-full overflow-scroll rounded shadow">
<table className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700"> <table className="min-w-full bg-surface text-sm leading-normal text-content">
<thead className="text-gray-800 dark:text-gray-400"> <thead className="text-content-muted">
<tr> <tr>
<th className="w-12 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="w-12 border-b border-border p-3 text-left font-normal uppercase">
<button onClick={() => setShowAddForm(!showAddForm)}> <button onClick={() => setShowAddForm(!showAddForm)}>
<AddIcon size={20} /> <AddIcon size={20} />
</button> </button>
</th> </th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">User</th>
User <th className="border-b border-border p-3 text-left font-normal uppercase">Password</th>
</th> <th className="border-b border-border p-3 text-center font-normal uppercase">
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
Password
</th>
<th className="border-b border-gray-200 p-3 text-center font-normal uppercase dark:border-gray-800">
Permissions Permissions
</th> </th>
<th className="w-48 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="w-48 border-b border-border p-3 text-left font-normal uppercase">
Created Created
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="text-black dark:text-white"> <tbody>
{users.length === 0 ? ( {users.length === 0 ? (
<tr> <tr>
<td className="p-3 text-center" colSpan={5}> <td className="p-3 text-center" colSpan={5}>
@@ -189,33 +162,33 @@ export default function AdminUsersPage() {
) : ( ) : (
users.map((user: User) => ( users.map((user: User) => (
<tr key={user.id}> <tr key={user.id}>
<td className="relative cursor-pointer border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"> <td className="relative cursor-pointer border-b border-border p-3 text-content-muted">
<button onClick={() => handleDeleteUser(user.id)}> <button onClick={() => handleDeleteUser(user.id)}>
<DeleteIcon size={20} /> <DeleteIcon size={20} />
</button> </button>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{user.id}</p> <p>{user.id}</p>
</td> </td>
<td className="border-b border-gray-200 px-3"> <td className="border-b border-border px-3">
<button <button
onClick={() => { onClick={() => {
const password = prompt(`Enter new password for ${user.id}`); const password = prompt(`Enter new password for ${user.id}`);
if (password) handleUpdatePassword(user.id, password); if (password) handleUpdatePassword(user.id, password);
}} }}
className="bg-gray-500 px-2 py-1 font-medium text-white hover:bg-gray-800 dark:text-gray-800 dark:hover:bg-gray-100" className="bg-primary-500 px-2 py-1 font-medium text-primary-foreground hover:bg-primary-700"
> >
Reset Reset
</button> </button>
</td> </td>
<td className="flex min-w-40 justify-center gap-2 border-b border-gray-200 p-3 text-center"> <td className="flex min-w-40 justify-center gap-2 border-b border-border p-3 text-center">
<button <button
onClick={() => handleToggleAdmin(user.id, true)} onClick={() => handleToggleAdmin(user.id, true)}
disabled={user.admin} disabled={user.admin}
className={`rounded-md px-2 py-1 text-white dark:text-black ${ className={`rounded-md px-2 py-1 ${
user.admin user.admin
? 'cursor-default bg-gray-800 dark:bg-gray-100' ? 'cursor-default bg-content text-content-inverse'
: 'cursor-pointer bg-gray-400 dark:bg-gray-600' : 'cursor-pointer bg-surface-strong text-content'
}`} }`}
> >
admin admin
@@ -223,16 +196,16 @@ export default function AdminUsersPage() {
<button <button
onClick={() => handleToggleAdmin(user.id, false)} onClick={() => handleToggleAdmin(user.id, false)}
disabled={!user.admin} disabled={!user.admin}
className={`rounded-md px-2 py-1 text-white dark:text-black ${ className={`rounded-md px-2 py-1 ${
!user.admin !user.admin
? 'cursor-default bg-gray-800 dark:bg-gray-100' ? 'cursor-default bg-content text-content-inverse'
: 'cursor-pointer bg-gray-400 dark:bg-gray-600' : 'cursor-pointer bg-surface-strong text-content'
}`} }`}
> >
user user
</button> </button>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{user.created_at}</p> <p>{user.created_at}</p>
</td> </td>
</tr> </tr>

View File

@@ -38,17 +38,16 @@ export default function ComponentDemoPage() {
}; };
return ( return (
<div className="space-y-8 p-4"> <div className="space-y-8 p-4 text-content">
<h1 className="text-2xl font-bold dark:text-white">UI Components Demo</h1> <h1 className="text-2xl font-bold">UI Components Demo</h1>
{/* Toast Demos */} <section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700"> <h2 className="mb-4 text-xl font-semibold">Toast Notifications</h2>
<h2 className="mb-4 text-xl font-semibold dark:text-white">Toast Notifications</h2>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<button <button
onClick={handleDemoClick} onClick={handleDemoClick}
disabled={isLoading} disabled={isLoading}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50" className="rounded bg-secondary-500 px-4 py-2 text-secondary-foreground hover:bg-secondary-600 disabled:cursor-not-allowed disabled:opacity-50"
> >
{isLoading ? <InlineLoader size="sm" /> : 'Show Info Toast'} {isLoading ? <InlineLoader size="sm" /> : 'Show Info Toast'}
</button> </button>
@@ -66,21 +65,19 @@ export default function ComponentDemoPage() {
</button> </button>
<button <button
onClick={handleCustomToast} onClick={handleCustomToast}
className="rounded bg-purple-500 px-4 py-2 text-white hover:bg-purple-600" className="rounded bg-primary-500 px-4 py-2 text-primary-foreground hover:bg-primary-600"
> >
Show Custom Toast Show Custom Toast
</button> </button>
</div> </div>
</section> </section>
{/* Skeleton Demos */} <section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700"> <h2 className="mb-4 text-xl font-semibold">Skeleton Loading Components</h2>
<h2 className="mb-4 text-xl font-semibold dark:text-white">Skeleton Loading Components</h2>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2"> <div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{/* Basic Skeletons */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium dark:text-gray-300">Basic Skeletons</h3> <h3 className="text-lg font-medium text-content-muted">Basic Skeletons</h3>
<div className="space-y-2"> <div className="space-y-2">
<Skeleton className="h-8 w-full" /> <Skeleton className="h-8 w-full" />
<Skeleton variant="text" className="w-3/4" /> <Skeleton variant="text" className="w-3/4" />
@@ -92,16 +89,14 @@ export default function ComponentDemoPage() {
</div> </div>
</div> </div>
{/* Skeleton Text */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium dark:text-gray-300">Skeleton Text</h3> <h3 className="text-lg font-medium text-content-muted">Skeleton Text</h3>
<SkeletonText lines={3} /> <SkeletonText lines={3} />
<SkeletonText lines={5} className="max-w-md" /> <SkeletonText lines={5} className="max-w-md" />
</div> </div>
{/* Skeleton Avatar */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium dark:text-gray-300">Skeleton Avatar</h3> <h3 className="text-lg font-medium text-content-muted">Skeleton Avatar</h3>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<SkeletonAvatar size="sm" /> <SkeletonAvatar size="sm" />
<SkeletonAvatar size="md" /> <SkeletonAvatar size="md" />
@@ -110,9 +105,8 @@ export default function ComponentDemoPage() {
</div> </div>
</div> </div>
{/* Skeleton Button */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium dark:text-gray-300">Skeleton Button</h3> <h3 className="text-lg font-medium text-content-muted">Skeleton Button</h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<SkeletonButton width={120} /> <SkeletonButton width={120} />
<SkeletonButton className="w-full max-w-xs" /> <SkeletonButton className="w-full max-w-xs" />
@@ -121,9 +115,8 @@ export default function ComponentDemoPage() {
</div> </div>
</section> </section>
{/* Skeleton Card Demo */} <section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700"> <h2 className="mb-4 text-xl font-semibold">Skeleton Cards</h2>
<h2 className="mb-4 text-xl font-semibold dark:text-white">Skeleton Cards</h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<SkeletonCard /> <SkeletonCard />
<SkeletonCard showAvatar /> <SkeletonCard showAvatar />
@@ -131,33 +124,30 @@ export default function ComponentDemoPage() {
</div> </div>
</section> </section>
{/* Skeleton Table Demo */} <section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700"> <h2 className="mb-4 text-xl font-semibold">Skeleton Table</h2>
<h2 className="mb-4 text-xl font-semibold dark:text-white">Skeleton Table</h2>
<SkeletonTable rows={5} columns={4} /> <SkeletonTable rows={5} columns={4} />
</section> </section>
{/* Page Loader Demo */} <section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700"> <h2 className="mb-4 text-xl font-semibold">Page Loader</h2>
<h2 className="mb-4 text-xl font-semibold dark:text-white">Page Loader</h2>
<PageLoader message="Loading demo content..." /> <PageLoader message="Loading demo content..." />
</section> </section>
{/* Inline Loader Demo */} <section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700"> <h2 className="mb-4 text-xl font-semibold">Inline Loader</h2>
<h2 className="mb-4 text-xl font-semibold dark:text-white">Inline Loader</h2>
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<div className="text-center"> <div className="text-center">
<InlineLoader size="sm" /> <InlineLoader size="sm" />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">Small</p> <p className="mt-2 text-sm text-content-muted">Small</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<InlineLoader size="md" /> <InlineLoader size="md" />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">Medium</p> <p className="mt-2 text-sm text-content-muted">Medium</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<InlineLoader size="lg" /> <InlineLoader size="lg" />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">Large</p> <p className="mt-2 text-sm text-content-muted">Large</p>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,4 +1,6 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { import {
useGetDocument, useGetDocument,
useEditDocument, useEditDocument,
@@ -6,7 +8,6 @@ import {
} from '../generated/anthoLumeAPIV1'; } from '../generated/anthoLumeAPIV1';
import { Document } from '../generated/model/document'; import { Document } from '../generated/model/document';
import { Progress } from '../generated/model/progress'; import { Progress } from '../generated/model/progress';
import { useQueryClient } from '@tanstack/react-query';
import { formatDuration } from '../utils/formatters'; import { formatDuration } from '../utils/formatters';
import { import {
DeleteIcon, DeleteIcon,
@@ -18,9 +19,14 @@ import {
CloseIcon, CloseIcon,
CheckIcon, CheckIcon,
} from '../icons'; } from '../icons';
import { useState } from 'react';
import { Field, FieldLabel, FieldValue, FieldActions } from '../components'; import { Field, FieldLabel, FieldValue, FieldActions } from '../components';
const iconButtonClassName = 'cursor-pointer text-content-muted hover:text-content';
const popupClassName = 'rounded bg-surface-strong p-3 text-content shadow-lg transition-all duration-200';
const popupInputClassName = 'rounded bg-surface p-2 text-content';
const editInputClassName =
'w-full rounded border border-secondary-200 bg-secondary-50 p-2 text-lg font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500';
export default function DocumentPage() { export default function DocumentPage() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -35,18 +41,16 @@ export default function DocumentPage() {
const [isEditingDescription, setIsEditingDescription] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false);
const [showTimeReadInfo, setShowTimeReadInfo] = useState(false); const [showTimeReadInfo, setShowTimeReadInfo] = useState(false);
// Edit values - initialized after document is loaded
const [editTitle, setEditTitle] = useState(''); const [editTitle, setEditTitle] = useState('');
const [editAuthor, setEditAuthor] = useState(''); const [editAuthor, setEditAuthor] = useState('');
const [editDescription, setEditDescription] = useState(''); const [editDescription, setEditDescription] = useState('');
if (docLoading) { if (docLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
// Check for successful response (status 200)
if (!docData || docData.status !== 200) { if (!docData || docData.status !== 200) {
return <div className="text-gray-500 dark:text-white">Document not found</div>; return <div className="text-content-muted">Document not found</div>;
} }
const document = docData.data.document as Document; const document = docData.data.document as Document;
@@ -54,7 +58,7 @@ export default function DocumentPage() {
docData?.status === 200 ? (docData.data.progress as Progress | undefined) : undefined; docData?.status === 200 ? (docData.data.progress as Progress | undefined) : undefined;
if (!document) { if (!document) {
return <div className="text-gray-500 dark:text-white">Document not found</div>; return <div className="text-content-muted">Document not found</div>;
} }
const percentage = const percentage =
@@ -62,24 +66,18 @@ export default function DocumentPage() {
const secondsPerPercent = document.seconds_per_percent || 0; const secondsPerPercent = document.seconds_per_percent || 0;
const totalTimeLeftSeconds = Math.round((100 - percentage) * secondsPerPercent); const totalTimeLeftSeconds = Math.round((100 - percentage) * secondsPerPercent);
// Helper to start editing
const startEditing = (field: 'title' | 'author' | 'description') => { const startEditing = (field: 'title' | 'author' | 'description') => {
if (field === 'title') setEditTitle(document.title); if (field === 'title') setEditTitle(document.title);
if (field === 'author') setEditAuthor(document.author); if (field === 'author') setEditAuthor(document.author);
if (field === 'description') setEditDescription(document.description || ''); if (field === 'description') setEditDescription(document.description || '');
}; };
// Save edit handlers
const saveTitle = () => { const saveTitle = () => {
editMutation.mutate( editMutation.mutate(
{ { id: document.id, data: { title: editTitle } },
id: document.id,
data: { title: editTitle },
},
{ {
onSuccess: response => { onSuccess: response => {
setIsEditingTitle(false); setIsEditingTitle(false);
// Update cache with the response data (no refetch needed)
queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); queryClient.setQueryData(getGetDocumentQueryKey(document.id), response);
}, },
onError: () => setIsEditingTitle(false), onError: () => setIsEditingTitle(false),
@@ -89,14 +87,10 @@ export default function DocumentPage() {
const saveAuthor = () => { const saveAuthor = () => {
editMutation.mutate( editMutation.mutate(
{ { id: document.id, data: { author: editAuthor } },
id: document.id,
data: { author: editAuthor },
},
{ {
onSuccess: response => { onSuccess: response => {
setIsEditingAuthor(false); setIsEditingAuthor(false);
// Update cache with the response data (no refetch needed)
queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); queryClient.setQueryData(getGetDocumentQueryKey(document.id), response);
}, },
onError: () => setIsEditingAuthor(false), onError: () => setIsEditingAuthor(false),
@@ -106,14 +100,10 @@ export default function DocumentPage() {
const saveDescription = () => { const saveDescription = () => {
editMutation.mutate( editMutation.mutate(
{ { id: document.id, data: { description: editDescription } },
id: document.id,
data: { description: editDescription },
},
{ {
onSuccess: response => { onSuccess: response => {
setIsEditingDescription(false); setIsEditingDescription(false);
// Update cache with the response data (no refetch needed)
queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); queryClient.setQueryData(getGetDocumentQueryKey(document.id), response);
}, },
onError: () => setIsEditingDescription(false), onError: () => setIsEditingDescription(false),
@@ -123,10 +113,8 @@ export default function DocumentPage() {
return ( return (
<div className="relative size-full"> <div className="relative size-full">
<div className="size-full overflow-scroll rounded bg-white p-4 shadow-lg dark:bg-gray-700 dark:text-white"> <div className="size-full overflow-scroll rounded bg-surface p-4 text-content shadow-lg">
{/* Document Info - Left Column */}
<div className="relative float-left mb-2 mr-4 flex w-44 flex-col gap-2 md:w-60 lg:w-80"> <div className="relative float-left mb-2 mr-4 flex w-44 flex-col gap-2 md:w-60 lg:w-80">
{/* Cover Image with Edit Label */}
<label className="z-10 cursor-pointer" htmlFor="edit-cover-checkbox"> <label className="z-10 cursor-pointer" htmlFor="edit-cover-checkbox">
<img <img
className="w-full rounded object-fill" className="w-full rounded object-fill"
@@ -135,31 +123,27 @@ export default function DocumentPage() {
/> />
</label> </label>
{/* Read Button - Only if file exists */}
{document.filepath && ( {document.filepath && (
<a <a
href={`/reader#id=${document.id}&type=REMOTE`} href={`/reader#id=${document.id}&type=REMOTE`}
className="z-10 mt-2 w-full rounded bg-blue-700 py-1 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700" className="z-10 mt-2 w-full rounded bg-secondary-700 py-1 text-center text-sm font-medium text-white hover:bg-secondary-800 focus:outline-none focus:ring-4 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700"
> >
Read Read
</a> </a>
)} )}
{/* Action Buttons Container */}
<div className="relative z-20 flex flex-wrap-reverse justify-between gap-2"> <div className="relative z-20 flex flex-wrap-reverse justify-between gap-2">
{/* ISBN Info */}
<div className="min-w-[50%] md:mr-2"> <div className="min-w-[50%] md:mr-2">
<div className="flex gap-1 text-sm"> <div className="flex gap-1 text-sm">
<p className="text-gray-500">ISBN-10:</p> <p className="text-content-muted">ISBN-10:</p>
<p className="font-medium">{document.isbn10 || 'N/A'}</p> <p className="font-medium">{document.isbn10 || 'N/A'}</p>
</div> </div>
<div className="flex gap-1 text-sm"> <div className="flex gap-1 text-sm">
<p className="text-gray-500">ISBN-13:</p> <p className="text-content-muted">ISBN-13:</p>
<p className="font-medium">{document.isbn13 || 'N/A'}</p> <p className="font-medium">{document.isbn13 || 'N/A'}</p>
</div> </div>
</div> </div>
{/* Edit Cover Dropdown */}
<div className="relative"> <div className="relative">
<input <input
type="checkbox" type="checkbox"
@@ -169,35 +153,24 @@ export default function DocumentPage() {
onChange={e => setShowEditCover(e.target.checked)} onChange={e => setShowEditCover(e.target.checked)}
/> />
<div <div
className={`absolute left-0 top-0 z-30 flex flex-col gap-2 rounded bg-gray-200 p-3 shadow-lg transition-all duration-200 dark:bg-gray-600 ${ className={`absolute left-0 top-0 z-30 flex flex-col gap-2 ${popupClassName} ${
showEditCover ? 'opacity-100' : 'pointer-events-none opacity-0' showEditCover ? 'opacity-100' : 'pointer-events-none opacity-0'
}`} }`}
> >
<form className="flex w-72 flex-col gap-2 text-sm text-black dark:text-white"> <form className="flex w-72 flex-col gap-2 text-sm">
<input <input type="file" id="cover_file" name="cover_file" className={popupInputClassName} />
type="file"
id="cover_file"
name="cover_file"
className="bg-gray-300 p-2"
/>
<button <button
type="submit" type="submit"
className="rounded bg-blue-700 px-2 py-1 text-sm font-medium text-white hover:bg-blue-800 dark:bg-blue-600" className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
> >
Upload Cover Upload Cover
</button> </button>
</form> </form>
<form className="flex w-72 flex-col gap-2 text-sm text-black dark:text-white"> <form className="flex w-72 flex-col gap-2 text-sm">
<input <input type="checkbox" checked id="remove_cover" name="remove_cover" className="hidden" />
type="checkbox"
checked
id="remove_cover"
name="remove_cover"
className="hidden"
/>
<button <button
type="submit" type="submit"
className="rounded bg-blue-700 px-2 py-1 text-sm font-medium text-white hover:bg-blue-800 dark:bg-blue-600" className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
> >
Remove Cover Remove Cover
</button> </button>
@@ -205,24 +178,22 @@ export default function DocumentPage() {
</div> </div>
</div> </div>
{/* Icons Container */} <div className="relative my-auto flex grow justify-between text-content-muted">
<div className="relative my-auto flex grow justify-between text-gray-500 dark:text-gray-500">
{/* Delete Button */}
<div className="relative"> <div className="relative">
<button <button
type="button" type="button"
onClick={() => setShowDelete(!showDelete)} onClick={() => setShowDelete(!showDelete)}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
aria-label="Delete" aria-label="Delete"
> >
<DeleteIcon size={28} /> <DeleteIcon size={28} />
</button> </button>
<div <div
className={`absolute bottom-7 left-5 z-30 rounded bg-gray-200 p-3 shadow-lg transition-all duration-200 dark:bg-gray-600 ${ className={`absolute bottom-7 left-5 z-30 ${popupClassName} ${
showDelete ? 'opacity-100' : 'pointer-events-none opacity-0' showDelete ? 'opacity-100' : 'pointer-events-none opacity-0'
}`} }`}
> >
<form className="w-24 text-sm text-black dark:text-white"> <form className="w-24 text-sm">
<button <button
type="submit" type="submit"
className="rounded bg-red-600 px-2 py-1 text-sm font-medium text-white hover:bg-red-700" className="rounded bg-red-600 px-2 py-1 text-sm font-medium text-white hover:bg-red-700"
@@ -233,38 +204,36 @@ export default function DocumentPage() {
</div> </div>
</div> </div>
{/* Activity Button */}
<a <a
href={`/activity?document=${document.id}`} href={`/activity?document=${document.id}`}
aria-label="Activity" aria-label="Activity"
className="hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
> >
<ActivityIcon size={28} /> <ActivityIcon size={28} />
</a> </a>
{/* Identify/Search Button */}
<div className="relative"> <div className="relative">
<button <button
type="button" type="button"
onClick={() => setShowIdentify(!showIdentify)} onClick={() => setShowIdentify(!showIdentify)}
aria-label="Identify" aria-label="Identify"
className="hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
> >
<SearchIcon size={28} /> <SearchIcon size={28} />
</button> </button>
<div <div
className={`absolute bottom-7 left-5 z-30 rounded bg-gray-200 p-3 shadow-lg transition-all duration-200 dark:bg-gray-600 ${ className={`absolute bottom-7 left-5 z-30 ${popupClassName} ${
showIdentify ? 'opacity-100' : 'pointer-events-none opacity-0' showIdentify ? 'opacity-100' : 'pointer-events-none opacity-0'
}`} }`}
> >
<form className="flex flex-col gap-2 text-sm text-black dark:text-white"> <form className="flex flex-col gap-2 text-sm">
<input <input
type="text" type="text"
id="title" id="title"
name="title" name="title"
placeholder="Title" placeholder="Title"
defaultValue={document.title} defaultValue={document.title}
className="rounded bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white" className={popupInputClassName}
/> />
<input <input
type="text" type="text"
@@ -272,7 +241,7 @@ export default function DocumentPage() {
name="author" name="author"
placeholder="Author" placeholder="Author"
defaultValue={document.author} defaultValue={document.author}
className="rounded bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white" className={popupInputClassName}
/> />
<input <input
type="text" type="text"
@@ -280,11 +249,11 @@ export default function DocumentPage() {
name="isbn" name="isbn"
placeholder="ISBN 10 / ISBN 13" placeholder="ISBN 10 / ISBN 13"
defaultValue={document.isbn13 || document.isbn10} defaultValue={document.isbn13 || document.isbn10}
className="rounded bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white" className={popupInputClassName}
/> />
<button <button
type="submit" type="submit"
className="rounded bg-blue-700 px-2 py-1 text-sm font-medium text-white hover:bg-blue-800 dark:bg-blue-600" className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
> >
Identify Identify
</button> </button>
@@ -292,17 +261,16 @@ export default function DocumentPage() {
</div> </div>
</div> </div>
{/* Download Button */}
{document.filepath ? ( {document.filepath ? (
<a <a
href={`/api/v1/documents/${document.id}/file`} href={`/api/v1/documents/${document.id}/file`}
aria-label="Download" aria-label="Download"
className="hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
> >
<DownloadIcon size={28} /> <DownloadIcon size={28} />
</a> </a>
) : ( ) : (
<span className="text-gray-200 dark:text-gray-600"> <span className="text-content-subtle">
<DownloadIcon size={28} disabled /> <DownloadIcon size={28} disabled />
</span> </span>
)} )}
@@ -310,9 +278,7 @@ export default function DocumentPage() {
</div> </div>
</div> </div>
{/* Document Details Grid */}
<div className="grid justify-between gap-4 pb-4 sm:grid-cols-2"> <div className="grid justify-between gap-4 pb-4 sm:grid-cols-2">
{/* Title - Editable */}
<Field <Field
isEditing={isEditingTitle} isEditing={isEditingTitle}
label={ label={
@@ -321,20 +287,10 @@ export default function DocumentPage() {
<FieldActions> <FieldActions>
{isEditingTitle ? ( {isEditingTitle ? (
<div className="flex gap-1"> <div className="flex gap-1">
<button <button type="button" onClick={() => setIsEditingTitle(false)} className={iconButtonClassName} aria-label="Cancel edit">
type="button"
onClick={() => setIsEditingTitle(false)}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
aria-label="Cancel edit"
>
<CloseIcon size={18} /> <CloseIcon size={18} />
</button> </button>
<button <button type="button" onClick={saveTitle} className={iconButtonClassName} aria-label="Confirm edit">
type="button"
onClick={saveTitle}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
aria-label="Confirm edit"
>
<CheckIcon size={18} /> <CheckIcon size={18} />
</button> </button>
</div> </div>
@@ -345,7 +301,7 @@ export default function DocumentPage() {
startEditing('title'); startEditing('title');
setIsEditingTitle(true); setIsEditingTitle(true);
}} }}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
aria-label="Edit title" aria-label="Edit title"
> >
<EditIcon size={18} /> <EditIcon size={18} />
@@ -357,19 +313,13 @@ export default function DocumentPage() {
> >
{isEditingTitle ? ( {isEditingTitle ? (
<div className="relative mt-1 flex gap-2"> <div className="relative mt-1 flex gap-2">
<input <input type="text" value={editTitle} onChange={e => setEditTitle(e.target.value)} className={editInputClassName} />
type="text"
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
className="w-full rounded border border-blue-200 bg-blue-50 p-2 text-lg font-medium text-black focus:outline-none focus:ring-2 focus:ring-blue-400 dark:border-blue-700 dark:bg-blue-900/20 dark:text-white dark:focus:ring-blue-500"
/>
</div> </div>
) : ( ) : (
<FieldValue>{document.title}</FieldValue> <FieldValue>{document.title}</FieldValue>
)} )}
</Field> </Field>
{/* Author - Editable */}
<Field <Field
isEditing={isEditingAuthor} isEditing={isEditingAuthor}
label={ label={
@@ -378,20 +328,10 @@ export default function DocumentPage() {
<FieldActions> <FieldActions>
{isEditingAuthor ? ( {isEditingAuthor ? (
<> <>
<button <button type="button" onClick={() => setIsEditingAuthor(false)} className={iconButtonClassName} aria-label="Cancel edit">
type="button"
onClick={() => setIsEditingAuthor(false)}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
aria-label="Cancel edit"
>
<CloseIcon size={18} /> <CloseIcon size={18} />
</button> </button>
<button <button type="button" onClick={saveAuthor} className={iconButtonClassName} aria-label="Confirm edit">
type="button"
onClick={saveAuthor}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
aria-label="Confirm edit"
>
<CheckIcon size={18} /> <CheckIcon size={18} />
</button> </button>
</> </>
@@ -402,7 +342,7 @@ export default function DocumentPage() {
startEditing('author'); startEditing('author');
setIsEditingAuthor(true); setIsEditingAuthor(true);
}} }}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
aria-label="Edit author" aria-label="Edit author"
> >
<EditIcon size={18} /> <EditIcon size={18} />
@@ -414,19 +354,13 @@ export default function DocumentPage() {
> >
{isEditingAuthor ? ( {isEditingAuthor ? (
<div className="relative mt-1 flex gap-2"> <div className="relative mt-1 flex gap-2">
<input <input type="text" value={editAuthor} onChange={e => setEditAuthor(e.target.value)} className={editInputClassName} />
type="text"
value={editAuthor}
onChange={e => setEditAuthor(e.target.value)}
className="w-full rounded border border-blue-200 bg-blue-50 p-2 text-lg font-medium text-black focus:outline-none focus:ring-2 focus:ring-blue-400 dark:border-blue-700 dark:bg-blue-900/20 dark:text-white dark:focus:ring-blue-500"
/>
</div> </div>
) : ( ) : (
<FieldValue>{document.author}</FieldValue> <FieldValue>{document.author}</FieldValue>
)} )}
</Field> </Field>
{/* Time Read with Info Dropdown */}
<Field <Field
label={ label={
<> <>
@@ -434,31 +368,27 @@ export default function DocumentPage() {
<button <button
type="button" type="button"
onClick={() => setShowTimeReadInfo(!showTimeReadInfo)} onClick={() => setShowTimeReadInfo(!showTimeReadInfo)}
className="my-auto cursor-pointer hover:text-gray-800 dark:hover:text-gray-100" className={`${iconButtonClassName} my-auto`}
aria-label="Show time read info" aria-label="Show time read info"
> >
<InfoIcon size={18} /> <InfoIcon size={18} />
</button> </button>
<div <div
className={`absolute right-0 top-7 z-30 rounded bg-gray-200 p-3 shadow-lg transition-all duration-200 dark:bg-gray-600 ${ className={`absolute right-0 top-7 z-30 ${popupClassName} ${
showTimeReadInfo ? 'opacity-100' : 'pointer-events-none opacity-0' showTimeReadInfo ? 'opacity-100' : 'pointer-events-none opacity-0'
}`} }`}
> >
<div className="flex text-xs"> <div className="flex text-xs">
<p className="w-32 text-gray-400">Seconds / Percent</p> <p className="w-32 text-content-subtle">Seconds / Percent</p>
<p className="font-medium dark:text-white"> <p className="font-medium">{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}</p>
{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}
</p>
</div> </div>
<div className="flex text-xs"> <div className="flex text-xs">
<p className="w-32 text-gray-400">Words / Minute</p> <p className="w-32 text-content-subtle">Words / Minute</p>
<p className="font-medium dark:text-white"> <p className="font-medium">{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}</p>
{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}
</p>
</div> </div>
<div className="flex text-xs"> <div className="flex text-xs">
<p className="w-32 text-gray-400">Est. Time Left</p> <p className="w-32 text-content-subtle">Est. Time Left</p>
<p className="whitespace-nowrap font-medium dark:text-white"> <p className="whitespace-nowrap font-medium">
{totalTimeLeftSeconds > 0 ? formatDuration(totalTimeLeftSeconds) : 'N/A'} {totalTimeLeftSeconds > 0 ? formatDuration(totalTimeLeftSeconds) : 'N/A'}
</p> </p>
</div> </div>
@@ -473,13 +403,11 @@ export default function DocumentPage() {
</FieldValue> </FieldValue>
</Field> </Field>
{/* Progress */}
<Field label={<FieldLabel>Progress</FieldLabel>}> <Field label={<FieldLabel>Progress</FieldLabel>}>
<FieldValue>{`${percentage.toFixed(2)}%`}</FieldValue> <FieldValue>{`${percentage.toFixed(2)}%`}</FieldValue>
</Field> </Field>
</div> </div>
{/* Description - Editable */}
<Field <Field
isEditing={isEditingDescription} isEditing={isEditingDescription}
label={ label={
@@ -488,20 +416,10 @@ export default function DocumentPage() {
<FieldActions> <FieldActions>
{isEditingDescription ? ( {isEditingDescription ? (
<> <>
<button <button type="button" onClick={() => setIsEditingDescription(false)} className={iconButtonClassName} aria-label="Cancel edit">
type="button"
onClick={() => setIsEditingDescription(false)}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
aria-label="Cancel edit"
>
<CloseIcon size={18} /> <CloseIcon size={18} />
</button> </button>
<button <button type="button" onClick={saveDescription} className={iconButtonClassName} aria-label="Confirm edit">
type="button"
onClick={saveDescription}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
aria-label="Confirm edit"
>
<CheckIcon size={18} /> <CheckIcon size={18} />
</button> </button>
</> </>
@@ -512,7 +430,7 @@ export default function DocumentPage() {
startEditing('description'); startEditing('description');
setIsEditingDescription(true); setIsEditingDescription(true);
}} }}
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100" className={iconButtonClassName}
aria-label="Edit description" aria-label="Edit description"
> >
<EditIcon size={18} /> <EditIcon size={18} />
@@ -527,14 +445,12 @@ export default function DocumentPage() {
<textarea <textarea
value={editDescription} value={editDescription}
onChange={e => setEditDescription(e.target.value)} onChange={e => setEditDescription(e.target.value)}
className="h-32 w-full grow rounded border border-blue-200 bg-blue-50 p-2 font-medium text-black focus:outline-none focus:ring-2 focus:ring-blue-400 dark:border-blue-700 dark:bg-blue-900/20 dark:text-white dark:focus:ring-blue-500" className="h-32 w-full grow rounded border border-secondary-200 bg-secondary-50 p-2 font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500"
rows={5} rows={5}
/> />
</div> </div>
) : ( ) : (
<FieldValue className="hyphens-auto text-justify"> <FieldValue className="hyphens-auto text-justify">{document.description || 'N/A'}</FieldValue>
{document.description || 'N/A'}
</FieldValue>
)} )}
</Field> </Field>
</div> </div>

View File

@@ -17,29 +17,24 @@ interface InfoCardProps {
} }
function InfoCard({ title, size, link }: InfoCardProps) { function InfoCard({ title, size, link }: InfoCardProps) {
const content = (
<div className="flex w-full gap-4 rounded bg-surface p-4 shadow-lg">
<div className="flex w-full flex-col justify-around text-sm text-content">
<p className="text-2xl font-bold">{size}</p>
<p className="text-sm text-content-subtle">{title}</p>
</div>
</div>
);
if (link) { if (link) {
return ( return (
<Link to={link} className="w-full"> <Link to={link} className="w-full">
<div className="flex w-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700"> {content}
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
<p className="text-sm text-gray-400">{title}</p>
</div>
</div>
</Link> </Link>
); );
} }
return ( return <div className="w-full">{content}</div>;
<div className="w-full">
<div className="flex w-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
<p className="text-sm text-gray-400">{title}</p>
</div>
</div>
</div>
);
} }
interface StreakCardProps { interface StreakCardProps {
@@ -63,18 +58,18 @@ function StreakCard({
}: StreakCardProps) { }: StreakCardProps) {
return ( return (
<div className="w-full"> <div className="w-full">
<div className="relative w-full rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700"> <div className="relative w-full rounded bg-surface px-4 py-6 text-content shadow-lg">
<p className="w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white"> <p className="w-max border-b border-border text-sm font-semibold text-content-muted">
{window === 'WEEK' ? 'Weekly Read Streak' : 'Daily Read Streak'} {window === 'WEEK' ? 'Weekly Read Streak' : 'Daily Read Streak'}
</p> </p>
<div className="my-6 flex items-end space-x-2"> <div className="my-6 flex items-end space-x-2">
<p className="text-5xl font-bold text-black dark:text-white">{currentStreak}</p> <p className="text-5xl font-bold">{currentStreak}</p>
</div> </div>
<div className="dark:text-white"> <div>
<div className="mb-2 flex items-center justify-between border-b border-gray-200 pb-2 text-sm"> <div className="mb-2 flex items-center justify-between border-b border-border pb-2 text-sm">
<div> <div>
<p>{window === 'WEEK' ? 'Current Weekly Streak' : 'Current Daily Streak'}</p> <p>{window === 'WEEK' ? 'Current Weekly Streak' : 'Current Daily Streak'}</p>
<div className="flex items-end text-sm text-gray-400"> <div className="flex items-end text-sm text-content-subtle">
{currentStreakStartDate} {currentStreakEndDate} {currentStreakStartDate} {currentStreakEndDate}
</div> </div>
</div> </div>
@@ -83,7 +78,7 @@ function StreakCard({
<div className="mb-2 flex items-center justify-between pb-2 text-sm"> <div className="mb-2 flex items-center justify-between pb-2 text-sm">
<div> <div>
<p>{window === 'WEEK' ? 'Best Weekly Streak' : 'Best Daily Streak'}</p> <p>{window === 'WEEK' ? 'Best Weekly Streak' : 'Best Daily Streak'}</p>
<div className="flex items-end text-sm text-gray-400"> <div className="flex items-end text-sm text-content-subtle">
{maxStreakStartDate} {maxStreakEndDate} {maxStreakStartDate} {maxStreakEndDate}
</div> </div>
</div> </div>
@@ -120,67 +115,47 @@ function LeaderboardCard({ name, data }: LeaderboardCardProps) {
const currentData = data[selectedPeriod]; const currentData = data[selectedPeriod];
const handlePeriodChange = (period: TimePeriod) => { const getPeriodClassName = (period: TimePeriod) =>
setSelectedPeriod(period); `cursor-pointer ${selectedPeriod === period ? 'text-content' : 'text-content-subtle hover:text-content'}`;
};
return ( return (
<div className="w-full"> <div className="w-full">
<div className="flex size-full flex-col justify-between rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700"> <div className="flex size-full flex-col justify-between rounded bg-surface px-4 py-6 text-content shadow-lg">
<div> <div>
<div className="flex justify-between"> <div className="flex justify-between">
<p className="w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white"> <p className="w-max border-b border-border text-sm font-semibold text-content-muted">
{name} Leaderboard {name} Leaderboard
</p> </p>
<div className="flex items-center gap-2 text-xs text-gray-400"> <div className="flex items-center gap-2 text-xs">
<button <button type="button" onClick={() => setSelectedPeriod('all')} className={getPeriodClassName('all')}>
type="button"
onClick={() => handlePeriodChange('all')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'all' ? '!text-black dark:!text-white' : ''}`}
>
all all
</button> </button>
<button <button type="button" onClick={() => setSelectedPeriod('year')} className={getPeriodClassName('year')}>
type="button"
onClick={() => handlePeriodChange('year')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'year' ? '!text-black dark:!text-white' : ''}`}
>
year year
</button> </button>
<button <button type="button" onClick={() => setSelectedPeriod('month')} className={getPeriodClassName('month')}>
type="button"
onClick={() => handlePeriodChange('month')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'month' ? '!text-black dark:!text-white' : ''}`}
>
month month
</button> </button>
<button <button type="button" onClick={() => setSelectedPeriod('week')} className={getPeriodClassName('week')}>
type="button"
onClick={() => handlePeriodChange('week')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'week' ? '!text-black dark:!text-white' : ''}`}
>
week week
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Current period data */}
<div className="my-6 flex items-end space-x-2"> <div className="my-6 flex items-end space-x-2">
{currentData?.length === 0 ? ( {currentData?.length === 0 ? (
<p className="text-5xl font-bold text-black dark:text-white">N/A</p> <p className="text-5xl font-bold">N/A</p>
) : ( ) : (
<p className="text-5xl font-bold text-black dark:text-white"> <p className="text-5xl font-bold">{currentData[0]?.user_id || 'N/A'}</p>
{currentData[0]?.user_id || 'N/A'}
</p>
)} )}
</div> </div>
<div className="dark:text-white"> <div>
{currentData?.slice(0, 3).map((item: LeaderboardEntry, index: number) => ( {currentData?.slice(0, 3).map((item: LeaderboardEntry, index: number) => (
<div <div
key={index} key={index}
className={`flex items-center justify-between py-2 text-sm ${index > 0 ? 'border-t border-gray-200' : ''}`} className={`flex items-center justify-between py-2 text-sm ${index > 0 ? 'border-t border-border' : ''}`}
> >
<div> <div>
<p>{item.user_id}</p> <p>{item.user_id}</p>
@@ -204,22 +179,20 @@ export default function HomePage() {
const userStats = homeResponse?.user_statistics; const userStats = homeResponse?.user_statistics;
if (homeLoading) { if (homeLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>; return <div className="text-content-muted">Loading...</div>;
} }
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Daily Read Totals Graph */}
<div className="w-full"> <div className="w-full">
<div className="relative w-full rounded bg-white shadow-lg dark:bg-gray-700"> <div className="relative w-full rounded bg-surface shadow-lg">
<p className="absolute left-5 top-3 w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white"> <p className="absolute left-5 top-3 w-max border-b border-border text-sm font-semibold text-content-muted">
Daily Read Totals Daily Read Totals
</p> </p>
<ReadingHistoryGraph data={graphData || []} /> <ReadingHistoryGraph data={graphData || []} />
</div> </div>
</div> </div>
{/* Info Cards */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-4"> <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<InfoCard title="Documents" size={dbInfo?.documents_size || 0} link="./documents" /> <InfoCard title="Documents" size={dbInfo?.documents_size || 0} link="./documents" />
<InfoCard title="Activity Records" size={dbInfo?.activity_size || 0} link="./activity" /> <InfoCard title="Activity Records" size={dbInfo?.activity_size || 0} link="./activity" />
@@ -227,7 +200,6 @@ export default function HomePage() {
<InfoCard title="Devices" size={dbInfo?.devices_size || 0} /> <InfoCard title="Devices" size={dbInfo?.devices_size || 0} />
</div> </div>
{/* Streak Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{streaks?.map((streak: UserStreak, index: number) => ( {streaks?.map((streak: UserStreak, index: number) => (
<StreakCard <StreakCard
@@ -243,7 +215,6 @@ export default function HomePage() {
))} ))}
</div> </div>
{/* Leaderboard Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<LeaderboardCard <LeaderboardCard
name="WPM" name="WPM"

View File

@@ -12,10 +12,7 @@ export default function ProgressPage() {
key: 'document_id' as const, key: 'document_id' as const,
header: 'Document', header: 'Document',
render: (_value, row) => ( render: (_value, row) => (
<Link <Link to={`/documents/${row.document_id}`} className="text-secondary-600 hover:underline">
to={`/documents/${row.document_id}`}
className="text-blue-600 hover:underline dark:text-blue-400"
>
{row.author || 'Unknown'} - {row.title || 'Unknown'} {row.author || 'Unknown'} - {row.title || 'Unknown'}
</Link> </Link>
), ),

View File

@@ -49,7 +49,7 @@ export default function RegisterPage() {
}; };
return ( return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-800 dark:text-white"> <div className="min-h-screen bg-canvas text-content">
<div className="flex w-full flex-wrap"> <div className="flex w-full flex-wrap">
<div className="flex w-full flex-col md:w-1/2"> <div className="flex w-full flex-col md:w-1/2">
<div className="my-auto flex flex-col justify-center px-8 pt-8 md:justify-start md:px-24 md:pt-0 lg:px-32"> <div className="my-auto flex flex-col justify-center px-8 pt-8 md:justify-start md:px-24 md:pt-0 lg:px-32">
@@ -61,7 +61,7 @@ export default function RegisterPage() {
type="text" type="text"
value={username} value={username}
onChange={e => setUsername(e.target.value)} onChange={e => setUsername(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600" className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
placeholder="Username" placeholder="Username"
required required
disabled={isLoading || isLoadingInfo || !registrationEnabled} disabled={isLoading || isLoadingInfo || !registrationEnabled}
@@ -74,7 +74,7 @@ export default function RegisterPage() {
type="password" type="password"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600" className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
placeholder="Password" placeholder="Password"
required required
disabled={isLoading || isLoadingInfo || !registrationEnabled} disabled={isLoading || isLoadingInfo || !registrationEnabled}
@@ -106,8 +106,8 @@ export default function RegisterPage() {
</div> </div>
</div> </div>
<div className="relative hidden h-screen w-1/2 shadow-2xl md:block"> <div className="relative hidden h-screen w-1/2 shadow-2xl md:block">
<div className="left-0 top-0 flex h-screen w-full items-center justify-center bg-gray-300 object-cover ease-in-out"> <div className="left-0 top-0 flex h-screen w-full items-center justify-center bg-surface-strong object-cover ease-in-out">
<span className="text-gray-500">AnthoLume</span> <span className="text-content-muted">AnthoLume</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -49,30 +49,30 @@ export function SearchPageView({
return ( return (
<div className="flex w-full flex-col gap-4 md:flex-row"> <div className="flex w-full flex-col gap-4 md:flex-row">
<div className="flex grow flex-col gap-4"> <div className="flex grow flex-col gap-4">
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"> <div className="flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={onSubmit}> <form className="flex flex-col gap-4 lg:flex-row" onSubmit={onSubmit}>
<div className="flex w-full grow flex-col"> <div className="flex w-full grow flex-col">
<div className="relative flex"> <div className="relative flex">
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"> <span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<Search2Icon size={15} hoverable={false} /> <Search2Icon size={15} hoverable={false} />
</span> </span>
<input <input
type="text" type="text"
value={query} value={query}
onChange={e => onQueryChange(e.target.value)} onChange={e => onQueryChange(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600" className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
placeholder="Query" placeholder="Query"
/> />
</div> </div>
</div> </div>
<div className="relative flex min-w-[12em]"> <div className="relative flex min-w-[12em]">
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"> <span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<BookIcon size={15} /> <BookIcon size={15} />
</span> </span>
<select <select
value={source} value={source}
onChange={e => onSourceChange(e.target.value as GetSearchSource)} onChange={e => onSourceChange(e.target.value as GetSearchSource)}
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600" className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
> >
<option value={GetSearchSource.LibGen}>Library Genesis</option> <option value={GetSearchSource.LibGen}>Library Genesis</option>
<option value={GetSearchSource.Annas_Archive}>Annas Archive</option> <option value={GetSearchSource.Annas_Archive}>Annas Archive</option>
@@ -87,28 +87,28 @@ export function SearchPageView({
</div> </div>
<div className="inline-block min-w-full overflow-hidden rounded shadow"> <div className="inline-block min-w-full overflow-hidden rounded shadow">
<table className="min-w-full bg-white text-sm leading-normal md:text-sm dark:bg-gray-700"> <table className="min-w-full bg-surface text-sm leading-normal text-content md:text-sm">
<thead className="text-gray-800 dark:text-gray-400"> <thead className="text-content-muted">
<tr> <tr>
<th className="w-12 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"></th> <th className="w-12 border-b border-border p-3 text-left font-normal uppercase"></th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Document Document
</th> </th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Series Series
</th> </th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Type Type
</th> </th>
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"> <th className="border-b border-border p-3 text-left font-normal uppercase">
Size Size
</th> </th>
<th className="hidden border-b border-gray-200 p-3 text-left font-normal uppercase md:block dark:border-gray-800"> <th className="hidden border-b border-border p-3 text-left font-normal uppercase md:table-cell">
Date Date
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="text-black dark:text-white"> <tbody>
{isLoading && ( {isLoading && (
<tr> <tr>
<td className="p-3 text-center" colSpan={6}> <td className="p-3 text-center" colSpan={6}>
@@ -126,24 +126,24 @@ export function SearchPageView({
{!isLoading && {!isLoading &&
results.map(item => ( results.map(item => (
<tr key={item.id}> <tr key={item.id}>
<td className="border-b border-gray-200 p-3 text-gray-500 dark:text-gray-500"> <td className="border-b border-border p-3 text-content-muted">
<button className="hover:text-purple-600" title="Download"> <button className="hover:text-primary-600" title="Download">
<DownloadIcon size={15} /> <DownloadIcon size={15} />
</button> </button>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
{item.author || 'N/A'} - {item.title || 'N/A'} {item.author || 'N/A'} - {item.title || 'N/A'}
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{item.series || 'N/A'}</p> <p>{item.series || 'N/A'}</p>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{item.file_type || 'N/A'}</p> <p>{item.file_type || 'N/A'}</p>
</td> </td>
<td className="border-b border-gray-200 p-3"> <td className="border-b border-border p-3">
<p>{item.file_size || 'N/A'}</p> <p>{item.file_size || 'N/A'}</p>
</td> </td>
<td className="hidden border-b border-gray-200 p-3 md:table-cell"> <td className="hidden border-b border-border p-3 md:table-cell">
<p>{item.upload_date || 'N/A'}</p> <p>{item.upload_date || 'N/A'}</p>
</td> </td>
</tr> </tr>
@@ -172,7 +172,7 @@ export default function SearchPage() {
query: { query: {
enabled: activeQuery.trim().length > 0, enabled: activeQuery.trim().length > 0,
}, },
}, }
); );
const results = getSearchResults(data); const results = getSearchResults(data);

View File

@@ -74,42 +74,42 @@ export default function SettingsPage() {
<div className="flex w-full flex-col gap-4 md:flex-row"> <div className="flex w-full flex-col gap-4 md:flex-row">
<div> <div>
<div className="flex flex-col items-center rounded bg-surface p-4 shadow-lg md:w-60 lg:w-80"> <div className="flex flex-col items-center rounded bg-surface p-4 shadow-lg md:w-60 lg:w-80">
<div className="mb-4 size-16 rounded-full bg-gray-200 dark:bg-gray-600" /> <div className="mb-4 size-16 rounded-full bg-surface-strong" />
<div className="h-6 w-32 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-6 w-32 rounded bg-surface-strong" />
</div> </div>
</div> </div>
<div className="flex grow flex-col gap-4"> <div className="flex grow flex-col gap-4">
<div className="flex flex-col gap-2 rounded bg-surface p-4 shadow-lg"> <div className="flex flex-col gap-2 rounded bg-surface p-4 shadow-lg">
<div className="mb-4 h-6 w-48 rounded bg-gray-200 dark:bg-gray-600" /> <div className="mb-4 h-6 w-48 rounded bg-surface-strong" />
<div className="flex gap-4"> <div className="flex gap-4">
<div className="h-12 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-12 flex-1 rounded bg-surface-strong" />
<div className="h-12 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-12 flex-1 rounded bg-surface-strong" />
<div className="h-10 w-40 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-10 w-40 rounded bg-surface-strong" />
</div> </div>
</div> </div>
<div className="flex flex-col gap-2 rounded bg-surface p-4 shadow-lg"> <div className="flex flex-col gap-2 rounded bg-surface p-4 shadow-lg">
<div className="mb-4 h-6 w-48 rounded bg-gray-200 dark:bg-gray-600" /> <div className="mb-4 h-6 w-48 rounded bg-surface-strong" />
<div className="flex gap-4"> <div className="flex gap-4">
<div className="h-12 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-12 flex-1 rounded bg-surface-strong" />
<div className="h-10 w-40 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-10 w-40 rounded bg-surface-strong" />
</div> </div>
</div> </div>
<div className="flex flex-col gap-2 rounded bg-surface p-4 shadow-lg"> <div className="flex flex-col gap-2 rounded bg-surface p-4 shadow-lg">
<div className="mb-4 h-6 w-48 rounded bg-gray-200 dark:bg-gray-600" /> <div className="mb-4 h-6 w-48 rounded bg-surface-strong" />
<div className="grid gap-3 md:grid-cols-3"> <div className="grid gap-3 md:grid-cols-3">
{themeModes.map(mode => ( {themeModes.map(mode => (
<div key={mode.value} className="h-24 rounded bg-gray-200 dark:bg-gray-600" /> <div key={mode.value} className="h-24 rounded bg-surface-strong" />
))} ))}
</div> </div>
</div> </div>
<div className="flex flex-col rounded bg-surface p-4 shadow-lg"> <div className="flex flex-col rounded bg-surface p-4 shadow-lg">
<div className="mb-4 h-6 w-24 rounded bg-gray-200 dark:bg-gray-600" /> <div className="mb-4 h-6 w-24 rounded bg-surface-strong" />
<div className="mb-4 flex gap-4"> <div className="mb-4 flex gap-4">
<div className="h-6 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-6 flex-1 rounded bg-surface-strong" />
<div className="h-6 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-6 flex-1 rounded bg-surface-strong" />
<div className="h-6 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-6 flex-1 rounded bg-surface-strong" />
</div> </div>
<div className="h-32 flex-1 rounded bg-gray-200 dark:bg-gray-600" /> <div className="h-32 flex-1 rounded bg-surface-strong" />
</div> </div>
</div> </div>
</div> </div>