Merge pull request 'Fix favorite sign-in copy + text contrast' (#88) from fix/favorite-signin-copy-contrast into master
CI / skip-ci-check (push) Successful in 30s
CI / python-lint (push) Successful in 31s
CI / docker-ci (push) Successful in 33s
CI / secret-scan (push) Successful in 36s
CI / e2e (push) Successful in 2m39s
CI / viewer-unit (push) Successful in 2m58s
CI / admin-unit (push) Successful in 3m11s

This commit was merged in pull request #88.
This commit is contained in:
2026-08-05 09:42:57 -05:00
18 changed files with 207 additions and 141 deletions
+5
View File
@@ -98,6 +98,11 @@ Living plan for product quality, auth/email reliability, and automation.
- [x] **Core Web Vitals budget** — LCP + CLS gates on home gallery (`viewer.web-vitals.spec.ts`, `web-vitals-budgets.ts`)
- [x] **Unify `/` and `/search`**`/search` redirects to `/?…` with full query string (`peopleMode`, `tagsMode`, etc.)
#### UX polish (2026-08)
- [x] **Favorite sign-in copy** — logged-out heart opens favorites messaging (not report) via `SignInRequiredDialog`
- [x] **Text contrast** — replace misplaced `text-secondary` (pale wash token) with `text-foreground` / `text-primary` on labels, auth dialogs, menus
## Later
- [x] Proper DEV deploy (`next start` + CI image) instead of long-lived `next dev``scripts/deploy-viewer.sh`, `ecosystem.config.js.example` uses `next start -p 3001`
+1
View File
@@ -70,6 +70,7 @@ not enabled in this CI job yet.
23. Core Web Vitals budget (`viewer.web-vitals.spec.ts`, `web-vitals-budgets.ts`): LCP + CLS gates on anonymous home gallery load
24. Admin review pages smoke (`admin.review-pages.spec.ts`): FastAPI admin login on `punimtagadmindev` → Identify, Auto-Match, Approve Identified load
25. `/search` legacy URLs redirect to `/` with query string preserved (incl. `peopleMode` / `tagsMode`)
26. Logged-out favorite (`viewer.favorite-signin.spec.ts`): Sign In Required dialog uses favorites copy, not report copy
See repo root [`ROADMAP.md`](../ROADMAP.md) for gaps and next steps.
+27
View File
@@ -0,0 +1,27 @@
import { test, expect } from '../fixtures';
test.describe('favorite sign-in copy @smoke', () => {
test('logged-out favorite opens sign-in dialog with favorites messaging', async ({
page,
playkitConfig,
}) => {
await page.goto(`${playkitConfig.baseUrl}/`);
await expect(
page.getByRole('button', { name: /Sign in/i }).or(page.getByLabel('Account menu')).first(),
).toBeVisible({ timeout: 20_000 });
// Open photo viewer (matches the reported bug path) then click the heart.
const photoButton = page.locator('main .aspect-square button').first();
await expect(photoButton).toBeVisible({ timeout: 15_000 });
await photoButton.click();
const viewer = page.getByRole('dialog', { name: /Photo viewer/i });
await expect(viewer).toBeVisible({ timeout: 15_000 });
await viewer.getByRole('button', { name: 'Add to favorites' }).click();
const signInDialog = page.getByRole('dialog').filter({ hasText: 'Sign In Required' });
await expect(signInDialog).toBeVisible({ timeout: 10_000 });
await expect(signInDialog).toContainText(/save favorites|favorite photos/i);
await expect(signInDialog).not.toContainText(/report photos/i);
});
});
+2 -2
View File
@@ -1094,7 +1094,7 @@ export function HomePageContent({ initialPhotos, people, tags }: HomePageContent
<div className="flex items-center gap-2">
<button
onClick={handleSelectAll}
className="p-2 text-secondary hover:text-secondary/80 hover:bg-secondary/10 rounded transition-colors"
className="p-2 text-primary hover:text-primary/80 hover:bg-secondary/10 rounded transition-colors"
title="Select all photos"
aria-label="Select all photos"
>
@@ -1102,7 +1102,7 @@ export function HomePageContent({ initialPhotos, people, tags }: HomePageContent
</button>
<button
onClick={handleClearAll}
className="p-2 text-secondary hover:text-secondary/80 hover:bg-secondary/10 rounded transition-colors"
className="p-2 text-primary hover:text-primary/80 hover:bg-secondary/10 rounded transition-colors"
title="Clear selection"
aria-label="Clear selection"
>
+1 -1
View File
@@ -21,7 +21,7 @@ export default function GlobalError({
return (
<main id="main-content" role="alert" className="flex min-h-screen flex-col items-center justify-center gap-4 bg-background p-6 text-center">
<h1 className="text-xl font-semibold text-secondary">Something went wrong</h1>
<h1 className="text-xl font-semibold text-foreground">Something went wrong</h1>
<p className="max-w-md text-sm text-muted-foreground">
An unexpected error occurred. You can try again, or head back to the homepage.
</p>
+4 -4
View File
@@ -104,7 +104,7 @@ function LoginForm() {
<ThemeToggle className="absolute right-4 top-4" />
<div className="w-full max-w-md space-y-8">
<div>
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-secondary">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">
Sign in to your account
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
@@ -161,7 +161,7 @@ function LoginForm() {
)}
<div className="space-y-4 rounded-md shadow-sm">
<div>
<label htmlFor="email" className="block text-sm font-medium text-secondary">
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email address
</label>
<Input
@@ -183,7 +183,7 @@ function LoginForm() {
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-secondary">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<Input
@@ -221,7 +221,7 @@ export default function LoginPage() {
<main id="main-content" className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-secondary">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">
Sign in to your account
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">Loading...</p>
+5 -5
View File
@@ -79,7 +79,7 @@ export default function RegisterPage() {
<ThemeToggle className="absolute right-4 top-4" />
<div className="w-full max-w-md space-y-8">
<div>
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-secondary">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">
Create your account
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
@@ -100,7 +100,7 @@ export default function RegisterPage() {
)}
<div className="space-y-4 rounded-md shadow-sm">
<div>
<label htmlFor="name" className="block text-sm font-medium text-secondary">
<label htmlFor="name" className="block text-sm font-medium text-foreground">
Name <span className="text-red-500">*</span>
</label>
<Input
@@ -116,7 +116,7 @@ export default function RegisterPage() {
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-secondary">
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email address <span className="text-red-500">*</span>
</label>
<Input
@@ -132,7 +132,7 @@ export default function RegisterPage() {
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-secondary">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<Input
@@ -151,7 +151,7 @@ export default function RegisterPage() {
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-secondary">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
Confirm Password
</label>
<Input
+5 -5
View File
@@ -76,7 +76,7 @@ function ResetPasswordForm() {
<ThemeToggle className="absolute right-4 top-4" />
<div className="w-full max-w-md space-y-8">
<div>
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-secondary">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">
Password reset successful
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
@@ -106,7 +106,7 @@ function ResetPasswordForm() {
<ThemeToggle className="absolute right-4 top-4" />
<div className="w-full max-w-md space-y-8">
<div>
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-secondary">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">
Reset your password
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
@@ -121,7 +121,7 @@ function ResetPasswordForm() {
)}
<div className="space-y-4 rounded-md shadow-sm">
<div>
<label htmlFor="password" className="block text-sm font-medium text-secondary">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
New Password
</label>
<Input
@@ -140,7 +140,7 @@ function ResetPasswordForm() {
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-secondary">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
Confirm Password
</label>
<Input
@@ -186,7 +186,7 @@ export default function ResetPasswordPage() {
<main id="main-content" className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-secondary">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">
Reset your password
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">Loading...</p>
+2 -2
View File
@@ -290,7 +290,7 @@ export function UploadContent() {
}`}
/>
<div>
<span className="text-lg font-medium text-secondary dark:text-gray-50">
<span className="text-lg font-medium text-foreground dark:text-gray-50">
Drop photos and videos here or click to browse
</span>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
@@ -315,7 +315,7 @@ export function UploadContent() {
{files.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-secondary dark:text-gray-50">
<h2 className="text-lg font-semibold text-foreground dark:text-gray-50">
Selected Files ({files.length})
</h2>
{!allSuccess && (
@@ -47,7 +47,7 @@ export function UploadPageClient() {
{/* Upload content */}
<div className="mt-8">
<div className="mb-8">
<h1 className="text-4xl font-bold text-secondary dark:text-gray-50">
<h1 className="text-4xl font-bold text-foreground dark:text-gray-50">
Upload Photos & Videos
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">
@@ -110,7 +110,7 @@ export function ForgotPasswordDialog({
</div>
)}
<div>
<label htmlFor="forgot-email" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="forgot-email" className="block text-sm font-medium text-foreground dark:text-gray-300">
Email address
</label>
<Input
+4 -4
View File
@@ -103,7 +103,7 @@ export function Header() {
<PopoverContent className="w-56 p-2" align="end">
<div className="space-y-1">
<div className="px-2 py-1.5">
<p className="text-sm font-medium text-secondary">
<p className="text-sm font-medium text-foreground">
{session.user.name || 'User'}
</p>
<p className="text-xs text-muted-foreground">
@@ -113,7 +113,7 @@ export function Header() {
<div className="border-t pt-1">
<Button
variant="ghost"
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
className="w-full justify-start text-sm text-primary hover:text-primary/80 hover:bg-secondary/10"
onClick={() => {
setPopoverOpen(false);
router.push('/upload');
@@ -125,7 +125,7 @@ export function Header() {
{session.user.isAdmin && (
<Button
variant="ghost"
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
className="w-full justify-start text-sm text-primary hover:text-primary/80 hover:bg-secondary/10"
onClick={() => {
setPopoverOpen(false);
setManageUsersOpen(true);
@@ -137,7 +137,7 @@ export function Header() {
)}
<Button
variant="ghost"
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
className="w-full justify-start text-sm text-primary hover:text-primary/80 hover:bg-secondary/10"
onClick={() => {
setPopoverOpen(false);
handleSignOut();
+6 -6
View File
@@ -164,7 +164,7 @@ export function LoginDialog({
{onOpenRegister ? (
<button
type="button"
className="font-medium text-secondary hover:text-secondary/80"
className="font-medium text-primary hover:text-primary/80"
onClick={() => {
handleOpenChange(false);
onOpenRegister();
@@ -175,7 +175,7 @@ export function LoginDialog({
) : (
<Link
href="/register"
className="font-medium text-secondary hover:text-secondary/80"
className="font-medium text-primary hover:text-primary/80"
onClick={() => handleOpenChange(false)}
>
create a new account
@@ -220,7 +220,7 @@ export function LoginDialog({
)}
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="email" className="block text-sm font-medium text-foreground dark:text-gray-300">
Email address
</label>
<Input
@@ -243,7 +243,7 @@ export function LoginDialog({
</div>
<div>
<div className="flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="password" className="block text-sm font-medium text-foreground dark:text-gray-300">
Password
</label>
<button
@@ -251,7 +251,7 @@ export function LoginDialog({
onClick={() => {
setForgotPasswordOpen(true);
}}
className="text-sm text-secondary hover:text-secondary/80 font-medium"
className="text-sm text-primary hover:text-primary/80 font-medium"
>
Forgot password?
</button>
@@ -271,7 +271,7 @@ export function LoginDialog({
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
className="absolute right-3 top-1/2 -translate-y-1/2 text-primary hover:text-primary/80 focus:outline-none"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? (
+24 -50
View File
@@ -25,6 +25,10 @@ import { parseFaceLocation, isPointInFace } from '@/lib/face-utils';
import { isUrl, isVideo, getImageSrc } from '@/lib/photo-utils';
import { LoginDialog } from '@/components/LoginDialog';
import { RegisterDialog } from '@/components/RegisterDialog';
import {
SignInRequiredDialog,
type SignInRequiredReason,
} from '@/components/SignInRequiredDialog';
interface FaceWithLocation {
id: number;
@@ -152,7 +156,8 @@ export function PhotoGrid({
const [reportedPhotos, setReportedPhotos] = useState<Map<number, { status: string }>>(new Map());
const [favoritingPhotoId, setFavoritingPhotoId] = useState<number | null>(null);
const [favoritedPhotos, setFavoritedPhotos] = useState<Map<number, boolean>>(new Map());
const [showSignInRequiredDialog, setShowSignInRequiredDialog] = useState(false);
const [signInRequiredReason, setSignInRequiredReason] =
useState<SignInRequiredReason | null>(null);
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [registerDialogOpen, setRegisterDialogOpen] = useState(false);
const [showRegisteredMessage, setShowRegisteredMessage] = useState(false);
@@ -457,7 +462,7 @@ export function PhotoGrid({
e.stopPropagation(); // Prevent photo click from firing
if (!session) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('report');
return;
}
@@ -510,7 +515,7 @@ export function PhotoGrid({
if (!response.ok) {
const error = await response.json().catch(() => null);
if (response.status === 401) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('report');
} else if (response.status === 403) {
alert(error?.error || 'Cannot re-report this photo.');
} else if (response.status === 409) {
@@ -547,7 +552,7 @@ export function PhotoGrid({
e.stopPropagation(); // Prevent photo click from firing
if (!session) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('favorite');
return;
}
@@ -566,7 +571,7 @@ export function PhotoGrid({
if (!response.ok) {
const error = await response.json();
if (response.status === 401) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('favorite');
} else {
alert(error.error || 'Failed to toggle favorite');
}
@@ -642,7 +647,7 @@ export function PhotoGrid({
{isVideoPhoto && (
<div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover:bg-black/30 transition-colors">
<div className="rounded-full bg-white/90 p-3 shadow-lg group-hover:bg-white transition-colors">
<Play className="h-6 w-6 text-secondary fill-secondary ml-1" />
<Play className="h-6 w-6 text-foreground fill-foreground ml-1" />
</div>
</div>
)}
@@ -794,7 +799,7 @@ export function PhotoGrid({
</DialogDescription>
</DialogHeader>
<div className="py-4">
<label htmlFor="report-comment" className="text-sm font-medium text-secondary">
<label htmlFor="report-comment" className="text-sm font-medium text-foreground">
Comment (optional)
</label>
<textarea
@@ -802,7 +807,7 @@ export function PhotoGrid({
value={reportDialogComment}
onChange={(event) => setReportDialogComment(event.target.value)}
maxLength={REPORT_COMMENT_MAX_LENGTH}
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"
className="mt-2 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-foreground 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..."
/>
@@ -834,46 +839,15 @@ export function PhotoGrid({
</DialogContent>
</Dialog>
{/* Sign In Required Dialog for Report */}
<Dialog open={showSignInRequiredDialog} onOpenChange={setShowSignInRequiredDialog}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Sign In Required</DialogTitle>
<DialogDescription>
You need to be signed in to report photos. Your reports will be reviewed by administrators.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Please sign in or create an account to continue.
</p>
<div className="flex gap-2">
<Button
onClick={() => {
setLoginDialogOpen(true);
}}
className="flex-1"
>
Sign in
</Button>
<Button
variant="outline"
onClick={() => {
setRegisterDialogOpen(true);
}}
className="flex-1"
>
Register
</Button>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowSignInRequiredDialog(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<SignInRequiredDialog
open={signInRequiredReason !== null}
reason={signInRequiredReason ?? 'favorite'}
onOpenChange={(open) => {
if (!open) setSignInRequiredReason(null);
}}
onSignIn={() => setLoginDialogOpen(true)}
onRegister={() => setRegisterDialogOpen(true)}
/>
{/* Login Dialog */}
<LoginDialog
@@ -887,7 +861,7 @@ export function PhotoGrid({
onSuccess={async () => {
await update();
router.refresh();
setShowSignInRequiredDialog(false);
setSignInRequiredReason(null);
}}
onOpenRegister={() => {
setLoginDialogOpen(false);
@@ -909,7 +883,7 @@ export function PhotoGrid({
onSuccess={async () => {
await update();
router.refresh();
setShowSignInRequiredDialog(false);
setSignInRequiredReason(null);
}}
onOpenLogin={() => {
setShowRegisteredMessage(true);
@@ -22,6 +22,10 @@ import { isUrl, isVideo, getImageSrc, getVideoSrc, getWebPlaybackStreamUrl } fro
import { IdentifyFaceDialog } from '@/components/IdentifyFaceDialog';
import { LoginDialog } from '@/components/LoginDialog';
import { RegisterDialog } from '@/components/RegisterDialog';
import {
SignInRequiredDialog,
type SignInRequiredReason,
} from '@/components/SignInRequiredDialog';
interface FaceWithLocation {
id: number;
@@ -172,7 +176,8 @@ export function PhotoViewerClient({
const [reportDialogError, setReportDialogError] = useState<string | null>(null);
const [favoritingPhotoId, setFavoritingPhotoId] = useState<number | null>(null);
const [isFavorited, setIsFavorited] = useState<boolean>(false);
const [showSignInRequiredDialog, setShowSignInRequiredDialog] = useState(false);
const [signInRequiredReason, setSignInRequiredReason] =
useState<SignInRequiredReason | null>(null);
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [registerDialogOpen, setRegisterDialogOpen] = useState(false);
const [showRegisteredMessage, setShowRegisteredMessage] = useState(false);
@@ -1071,7 +1076,7 @@ export function PhotoViewerClient({
const handleReportPhoto = async () => {
// Check if user is logged in
if (!session) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('report');
return;
}
@@ -1124,7 +1129,7 @@ export function PhotoViewerClient({
if (!response.ok) {
const error = await response.json().catch(() => null);
if (response.status === 401) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('report');
} else if (response.status === 403) {
alert(error?.error || 'Cannot re-report this photo.');
} else if (response.status === 409) {
@@ -1159,7 +1164,7 @@ export function PhotoViewerClient({
const handleToggleFavorite = async () => {
// Check if user is logged in
if (!session) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('favorite');
return;
}
@@ -1178,7 +1183,7 @@ export function PhotoViewerClient({
if (!response.ok) {
const error = await response.json();
if (response.status === 401) {
setShowSignInRequiredDialog(true);
setSignInRequiredReason('favorite');
} else {
alert(error.error || 'Failed to toggle favorite');
}
@@ -1503,7 +1508,7 @@ export function PhotoViewerClient({
transform: 'translateX(-50%)',
}}
>
<div className="bg-white text-secondary rounded-md px-3 py-1.5 shadow-lg border border-gray-200">
<div className="bg-white text-foreground rounded-md px-3 py-1.5 shadow-lg border border-gray-200">
<p className="text-sm font-medium whitespace-nowrap">
{hoveredFaceTooltip}
</p>
@@ -1712,7 +1717,7 @@ export function PhotoViewerClient({
</DialogDescription>
</DialogHeader>
<div className="py-4">
<label htmlFor="report-comment" className="text-sm font-medium text-secondary">
<label htmlFor="report-comment" className="text-sm font-medium text-foreground">
Comment (optional)
</label>
<textarea
@@ -1752,46 +1757,15 @@ export function PhotoViewerClient({
</DialogContent>
</Dialog>
{/* Sign In Required Dialog for Report */}
<Dialog open={showSignInRequiredDialog} onOpenChange={setShowSignInRequiredDialog}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Sign In Required</DialogTitle>
<DialogDescription>
You need to be signed in to report photos. Your reports will be reviewed by administrators.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-gray-600 mb-4">
Please sign in or create an account to continue.
</p>
<div className="flex gap-2">
<Button
onClick={() => {
setLoginDialogOpen(true);
}}
className="flex-1"
>
Sign in
</Button>
<Button
variant="outline"
onClick={() => {
setRegisterDialogOpen(true);
}}
className="flex-1"
>
Register
</Button>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowSignInRequiredDialog(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<SignInRequiredDialog
open={signInRequiredReason !== null}
reason={signInRequiredReason ?? 'favorite'}
onOpenChange={(open) => {
if (!open) setSignInRequiredReason(null);
}}
onSignIn={() => setLoginDialogOpen(true)}
onRegister={() => setRegisterDialogOpen(true)}
/>
{/* Login Dialog */}
<LoginDialog
@@ -1805,7 +1779,7 @@ export function PhotoViewerClient({
onSuccess={async () => {
await update();
router.refresh();
setShowSignInRequiredDialog(false);
setSignInRequiredReason(null);
}}
onOpenRegister={() => {
setLoginDialogOpen(false);
@@ -1827,7 +1801,7 @@ export function PhotoViewerClient({
onSuccess={async () => {
await update();
router.refresh();
setShowSignInRequiredDialog(false);
setSignInRequiredReason(null);
}}
onOpenLogin={() => {
setShowRegisteredMessage(true);
@@ -153,7 +153,7 @@ export function RegisterDialog({
Or{' '}
<button
type="button"
className="font-medium text-secondary hover:text-secondary/80"
className="font-medium text-primary hover:text-primary/80"
onClick={() => {
handleOpenChange(false);
if (onOpenLogin) {
@@ -173,7 +173,7 @@ export function RegisterDialog({
)}
<div className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="name" className="block text-sm font-medium text-foreground dark:text-gray-300">
Name <span className="text-red-500">*</span>
</label>
<Input
@@ -189,7 +189,7 @@ export function RegisterDialog({
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="email" className="block text-sm font-medium text-foreground dark:text-gray-300">
Email address <span className="text-red-500">*</span>
</label>
<Input
@@ -205,7 +205,7 @@ export function RegisterDialog({
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="password" className="block text-sm font-medium text-foreground dark:text-gray-300">
Password
</label>
<div className="relative mt-1">
@@ -223,7 +223,7 @@ export function RegisterDialog({
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
className="absolute right-3 top-1/2 -translate-y-1/2 text-primary hover:text-primary/80 focus:outline-none"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? (
@@ -238,7 +238,7 @@ export function RegisterDialog({
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-secondary dark:text-gray-300">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground dark:text-gray-300">
Confirm Password
</label>
<div className="relative mt-1">
@@ -256,7 +256,7 @@ export function RegisterDialog({
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
className="absolute right-3 top-1/2 -translate-y-1/2 text-primary hover:text-primary/80 focus:outline-none"
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? (
@@ -0,0 +1,85 @@
'use client';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
export type SignInRequiredReason = 'favorite' | 'report';
const COPY: Record<
SignInRequiredReason,
{ description: string; detail: string }
> = {
favorite: {
description:
'You need to be signed in to save favorites. Favorites stay with your account so you can find them later.',
detail: 'Please sign in or create an account to favorite photos.',
},
report: {
description:
'You need to be signed in to report photos. Your reports will be reviewed by administrators.',
detail: 'Please sign in or create an account to continue.',
},
};
type SignInRequiredDialogProps = {
open: boolean;
reason: SignInRequiredReason;
onOpenChange: (open: boolean) => void;
onSignIn: () => void;
onRegister: () => void;
};
export function SignInRequiredDialog({
open,
reason,
onOpenChange,
onSignIn,
onRegister,
}: SignInRequiredDialogProps) {
const copy = COPY[reason];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Sign In Required</DialogTitle>
<DialogDescription>{copy.description}</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="mb-4 text-sm text-muted-foreground">{copy.detail}</p>
<div className="flex gap-2">
<Button
onClick={() => {
onSignIn();
}}
className="flex-1"
>
Sign in
</Button>
<Button
variant="outline"
onClick={() => {
onRegister();
}}
className="flex-1"
>
Register
</Button>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+4 -4
View File
@@ -80,7 +80,7 @@ function UserMenu() {
<PopoverContent className="w-56 p-2 z-[110]" align="end">
<div className="space-y-1">
<div className="px-2 py-1.5">
<p className="text-sm font-medium text-secondary">
<p className="text-sm font-medium text-foreground">
{session.user.name || 'User'}
</p>
<p className="text-xs text-muted-foreground">
@@ -90,7 +90,7 @@ function UserMenu() {
<div className="border-t pt-1">
<Button
variant="ghost"
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
className="w-full justify-start text-sm text-primary hover:text-primary/80 hover:bg-secondary/10"
onClick={() => {
setPopoverOpen(false);
router.push('/upload');
@@ -102,7 +102,7 @@ function UserMenu() {
{session.user.isAdmin && (
<Button
variant="ghost"
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
className="w-full justify-start text-sm text-primary hover:text-primary/80 hover:bg-secondary/10"
onClick={() => {
setPopoverOpen(false);
setManageUsersOpen(true);
@@ -114,7 +114,7 @@ function UserMenu() {
)}
<Button
variant="ghost"
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
className="w-full justify-start text-sm text-primary hover:text-primary/80 hover:bg-secondary/10"
onClick={() => {
setPopoverOpen(false);
handleSignOut();