diff --git a/viewer-frontend/app/HomePageContent.tsx b/viewer-frontend/app/HomePageContent.tsx index 544d702..725787f 100644 --- a/viewer-frontend/app/HomePageContent.tsx +++ b/viewer-frontend/app/HomePageContent.tsx @@ -997,8 +997,8 @@ export function HomePageContent({ initialPhotos, people, tags }: HomePageContent />
{loading ? ( -
- +
+
) : ( <> @@ -1046,9 +1046,9 @@ export function HomePageContent({ initialPhotos, people, tags }: HomePageContent /> {/* Infinite scroll sentinel */} -
+
{loadingMore && ( - + )}
diff --git a/viewer-frontend/app/admin/users/ManageUsersContent.tsx b/viewer-frontend/app/admin/users/ManageUsersContent.tsx index 6254c67..3337fe5 100644 --- a/viewer-frontend/app/admin/users/ManageUsersContent.tsx +++ b/viewer-frontend/app/admin/users/ManageUsersContent.tsx @@ -331,13 +331,13 @@ export function ManageUsersContent() {
{error && ( -
+
{error}
)} {successMessage && ( -
+
{successMessage}
)} @@ -389,6 +389,7 @@ export function ManageUsersContent() { variant="ghost" size="sm" onClick={() => openEditDialog(user)} + aria-label={`Edit ${user.email}`} > @@ -403,6 +404,7 @@ export function ManageUsersContent() { setDeleteConfirmOpen(true); }} className="text-red-600 hover:text-red-700" + aria-label={`Deactivate ${user.email}`} > @@ -553,7 +555,7 @@ export function ManageUsersContent() {
void; @@ -30,38 +31,35 @@ export function ManageUsersPageClient({ onClose }: ManageUsersPageClientProps) { const overlayContent = (
-
- {/* Close button */} -
- -
- +
{/* Header */} -
-
+
+
PunimTag -
+
+ +
-

+

Browse our photo collection

@@ -70,7 +68,7 @@ export function ManageUsersPageClient({ onClose }: ManageUsersPageClientProps) {
-
+
); diff --git a/viewer-frontend/app/error.tsx b/viewer-frontend/app/error.tsx index 77aa86a..251131f 100644 --- a/viewer-frontend/app/error.tsx +++ b/viewer-frontend/app/error.tsx @@ -20,7 +20,7 @@ export default function GlobalError({ }, [error]); return ( -
+

Something went wrong

An unexpected error occurred. You can try again, or head back to the homepage. @@ -33,6 +33,6 @@ export default function GlobalError({ Go home

-
+ ); } diff --git a/viewer-frontend/app/layout.tsx b/viewer-frontend/app/layout.tsx index 952656d..f1d7b0e 100644 --- a/viewer-frontend/app/layout.tsx +++ b/viewer-frontend/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { SessionProviderWrapper } from "@/components/SessionProviderWrapper"; +import { ThemeProviderWrapper } from "@/components/ThemeProviderWrapper"; import "./globals.css"; const inter = Inter({ @@ -19,11 +20,19 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - - {children} - + + Skip to main content + + + + {children} + + ); diff --git a/viewer-frontend/app/login/page.tsx b/viewer-frontend/app/login/page.tsx index e9e605b..8769abe 100644 --- a/viewer-frontend/app/login/page.tsx +++ b/viewer-frontend/app/login/page.tsx @@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import Link from 'next/link'; +import { ThemeToggle } from '@/components/ThemeToggle'; function LoginForm() { const router = useRouter(); @@ -99,13 +100,14 @@ function LoginForm() { }; return ( -
+
+
-

+

Sign in to your account -

-

+ +

Or{' '}

{registered && ( -
-

+

+

Account created successfully! Please check your email to confirm your account before signing in.

)} {verified && ( -
-

+

+

Email verified successfully! You can now sign in.

)} {passwordReset && ( -
-

+

+

Password reset successfully! You can now sign in with your new password.

)} {emailNotVerified && ( -
-

+

+

Please verify your email address before signing in. Check your inbox for a confirmation email.

)} {error && ( -
-

{error}

+
+

{error}

)}
@@ -209,23 +211,23 @@ function LoginForm() {
-
+
); } export default function LoginPage() { return ( +
-

+

Sign in to your account -

-

Loading...

+ +

Loading...

-
+ }> diff --git a/viewer-frontend/app/page.tsx b/viewer-frontend/app/page.tsx index 03e02a7..4a0acce 100644 --- a/viewer-frontend/app/page.tsx +++ b/viewer-frontend/app/page.tsx @@ -222,10 +222,10 @@ export default async function HomePage() { ]); return ( -
+
{error ? ( -
+

Error loading photos

{error}

diff --git a/viewer-frontend/app/register/page.tsx b/viewer-frontend/app/register/page.tsx index 8f64cd9..cde8d8f 100644 --- a/viewer-frontend/app/register/page.tsx +++ b/viewer-frontend/app/register/page.tsx @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import Link from 'next/link'; import { isValidEmail } from '@/lib/utils'; +import { ThemeToggle } from '@/components/ThemeToggle'; export default function RegisterPage() { const router = useRouter(); @@ -74,13 +75,14 @@ export default function RegisterPage() { }; return ( -

+
+
-

+

Create your account -

-

+ +

Or{' '}

{error && ( -
-

{error}

+
+

{error}

)}
@@ -144,7 +146,7 @@ export default function RegisterPage() { className="mt-1" placeholder="••••••••" /> -

+

Must be at least 6 characters

@@ -177,7 +179,7 @@ export default function RegisterPage() {
-
+
); } diff --git a/viewer-frontend/app/reset-password/page.tsx b/viewer-frontend/app/reset-password/page.tsx index 65b39da..6afafdd 100644 --- a/viewer-frontend/app/reset-password/page.tsx +++ b/viewer-frontend/app/reset-password/page.tsx @@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import Link from 'next/link'; +import { ThemeToggle } from '@/components/ThemeToggle'; function ResetPasswordForm() { const router = useRouter(); @@ -71,18 +72,19 @@ function ResetPasswordForm() { if (success) { return ( -
+
+
-

+

Password reset successful -

-

+ +

Your password has been reset successfully. Redirecting to login...

-
-

+

+

You can now sign in with your new password.

@@ -95,25 +97,26 @@ function ResetPasswordForm() {
-
+
); } return ( -
+
+
-

+

Reset your password -

-

+ +

Enter your new password below

{error && ( -
-

{error}

+
+

{error}

)}
@@ -132,7 +135,7 @@ function ResetPasswordForm() { className="mt-1" placeholder="••••••••" /> -

+

Must be at least 6 characters

@@ -173,23 +176,23 @@ function ResetPasswordForm() {
-
+
); } export default function ResetPasswordPage() { return ( +
-

+

Reset your password -

-

Loading...

+ +

Loading...

-
+ }> diff --git a/viewer-frontend/app/search/SearchContent.tsx b/viewer-frontend/app/search/SearchContent.tsx index edd02a6..05d755e 100644 --- a/viewer-frontend/app/search/SearchContent.tsx +++ b/viewer-frontend/app/search/SearchContent.tsx @@ -146,8 +146,8 @@ export function SearchContent({ people, tags }: SearchContentProps) { {/* Results */}
{loading ? ( -
- +
+
) : ( <> @@ -192,11 +192,11 @@ export function SearchContent({ people, tags }: SearchContentProps) { ) : hasActiveFilters ? (
-

No photos found matching your filters

+

No photos found matching your filters

) : (
-

Select filters to search photos

+

Select filters to search photos

)} diff --git a/viewer-frontend/app/search/page.tsx b/viewer-frontend/app/search/page.tsx index 379cddd..dda07c2 100644 --- a/viewer-frontend/app/search/page.tsx +++ b/viewer-frontend/app/search/page.tsx @@ -97,7 +97,7 @@ export default async function SearchPage() { ]); return ( -
+

Search Photos @@ -108,8 +108,8 @@ export default async function SearchPage() {

-
Loading search...
+
+
Loading search...
}> diff --git a/viewer-frontend/app/test-images/page.tsx b/viewer-frontend/app/test-images/page.tsx index 4516386..2a1683f 100644 --- a/viewer-frontend/app/test-images/page.tsx +++ b/viewer-frontend/app/test-images/page.tsx @@ -48,7 +48,7 @@ export default function TestImagesPage() { ]; return ( -
+

Image Source Test Page diff --git a/viewer-frontend/app/upload/UploadContent.tsx b/viewer-frontend/app/upload/UploadContent.tsx index edc8adb..dc3460c 100644 --- a/viewer-frontend/app/upload/UploadContent.tsx +++ b/viewer-frontend/app/upload/UploadContent.tsx @@ -350,7 +350,7 @@ export function UploadContent() {

{allSuccess && ( -
+

diff --git a/viewer-frontend/app/upload/UploadPageClient.tsx b/viewer-frontend/app/upload/UploadPageClient.tsx index fd40515..ab0a39a 100644 --- a/viewer-frontend/app/upload/UploadPageClient.tsx +++ b/viewer-frontend/app/upload/UploadPageClient.tsx @@ -7,6 +7,7 @@ import { UploadContent } from './UploadContent'; import Image from 'next/image'; import Link from 'next/link'; import UserMenu from '@/components/UserMenu'; +import { ThemeToggle } from '@/components/ThemeToggle'; export function UploadPageClient() { const router = useRouter(); @@ -17,38 +18,35 @@ export function UploadPageClient() { return (

-
- {/* Close button */} -
- -
- +
{/* Header */} -
-
+
+
PunimTag -
+
+ +
-

+

Browse our photo collection

@@ -65,7 +63,7 @@ export function UploadPageClient() {
-
+
); } diff --git a/viewer-frontend/components/ForgotPasswordDialog.tsx b/viewer-frontend/components/ForgotPasswordDialog.tsx index 45babe1..596d815 100644 --- a/viewer-frontend/components/ForgotPasswordDialog.tsx +++ b/viewer-frontend/components/ForgotPasswordDialog.tsx @@ -91,7 +91,7 @@ export function ForgotPasswordDialog({ {success ? (
-
+

Password reset email sent! Please check your inbox and follow the instructions to reset your password.

@@ -105,7 +105,7 @@ export function ForgotPasswordDialog({ ) : (
{error && ( -
+

{error}

)} diff --git a/viewer-frontend/components/IdentifyFaceDialog.tsx b/viewer-frontend/components/IdentifyFaceDialog.tsx index f4f9acd..5bc6975 100644 --- a/viewer-frontend/components/IdentifyFaceDialog.tsx +++ b/viewer-frontend/components/IdentifyFaceDialog.tsx @@ -273,7 +273,7 @@ export function IdentifyFaceDialog({
-

+

Please sign in or create an account to continue.

@@ -363,7 +363,7 @@ export function IdentifyFaceDialog({
-

+

Only users with write access can identify faces. Please contact an administrator to request write access.

@@ -485,7 +485,7 @@ export function IdentifyFaceDialog({ const fullName = `${person.firstName} ${person.lastName} ${person.middleName || ''} ${person.maidenName || ''}`.toLowerCase(); return fullName.includes(peopleSearchQuery.toLowerCase()); }).length === 0 ? ( -

No people found

+

No people found

) : (
{people @@ -512,7 +512,7 @@ export function IdentifyFaceDialog({ {person.firstName} {person.lastName}
{(person.middleName || person.maidenName) && ( -
+
{[person.middleName, person.maidenName].filter(Boolean).join(' • ')}
)} diff --git a/viewer-frontend/components/LoginDialog.tsx b/viewer-frontend/components/LoginDialog.tsx index 0604613..1fc5925 100644 --- a/viewer-frontend/components/LoginDialog.tsx +++ b/viewer-frontend/components/LoginDialog.tsx @@ -185,21 +185,21 @@ export function LoginDialog({ {registered && ( -
+

Account created successfully! Please check your email to confirm your account before signing in.

)} {searchParams.get('verified') === 'true' && ( -
+

Email verified successfully! You can now sign in.

)} {emailNotVerified && ( -
+

Please verify your email address before signing in. Check your inbox for a confirmation email.

@@ -214,7 +214,7 @@ export function LoginDialog({
)} {error && ( -
+

{error}

)} diff --git a/viewer-frontend/components/PageHeader.tsx b/viewer-frontend/components/PageHeader.tsx index cdc210a..b948490 100644 --- a/viewer-frontend/components/PageHeader.tsx +++ b/viewer-frontend/components/PageHeader.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import Link from 'next/link'; import UserMenu from '@/components/UserMenu'; import { ActionButtons } from '@/components/ActionButtons'; +import { ThemeToggle } from '@/components/ThemeToggle'; interface PageHeaderProps { photosCount: number; @@ -33,26 +34,27 @@ export function PageHeader({ onToggleSelectionMode, }: PageHeaderProps) { return ( -
-
+
+
PunimTag -
+
+
-

+

Browse our photo collection -

+

setReportDialogComment(event.target.value)} maxLength={REPORT_COMMENT_MAX_LENGTH} - className="mt-2 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900" + className="mt-2 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-secondary px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-100" rows={4} placeholder="Add a short note about why this photo should be reviewed..." /> -
+
{`${reportDialogComment.length}/${REPORT_COMMENT_MAX_LENGTH} characters`} - {reportDialogError && {reportDialogError}} + {reportDialogError && {reportDialogError}}
@@ -836,7 +836,7 @@ export function PhotoGrid({
-

+

Please sign in or create an account to continue.

diff --git a/viewer-frontend/components/PhotoViewer.tsx b/viewer-frontend/components/PhotoViewer.tsx index 6c19370..29fae0a 100644 --- a/viewer-frontend/components/PhotoViewer.tsx +++ b/viewer-frontend/components/PhotoViewer.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; +import { useFocusTrap } from '@/hooks/useFocusTrap'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; @@ -32,6 +33,8 @@ export function PhotoViewer({ photo, previousId, nextId }: PhotoViewerProps) { const [loading, setLoading] = useState(false); const { data: session } = useSession(); const isLoggedIn = Boolean(session); + const dialogRef = useRef(null); + useFocusTrap(dialogRef, true); // Keyboard navigation useEffect(() => { @@ -80,7 +83,14 @@ export function PhotoViewer({ photo, previousId, nextId }: PhotoViewerProps) { const tags = (photo as any).PhotoTagLinkage?.map((pt: any) => pt.Tag.tag_name) || []; return ( -
+
{/* Close Button */} + ); + } + + const isDark = resolvedTheme === 'dark'; + const label = isDark ? 'Switch to light mode' : 'Switch to dark mode'; + + return ( + + + + + +

{label}

+
+
+ ); +} diff --git a/viewer-frontend/components/search/CollapsibleSearch.tsx b/viewer-frontend/components/search/CollapsibleSearch.tsx index e556892..a74e146 100644 --- a/viewer-frontend/components/search/CollapsibleSearch.tsx +++ b/viewer-frontend/components/search/CollapsibleSearch.tsx @@ -36,7 +36,7 @@ export function CollapsibleSearch({ people, tags, filters, onFiltersChange }: Co {isExpanded ? ( <>
- +