diff --git a/app/admin/components/ProofingView.tsx b/app/admin/components/ProofingView.tsx index 51bc760..eac3c04 100644 --- a/app/admin/components/ProofingView.tsx +++ b/app/admin/components/ProofingView.tsx @@ -20,20 +20,12 @@ interface AlbumOption { assetCount: number; } -type Download = AdminProofSession['download']; - const STATE_LABEL: Record = { open: 'Open', submitted: 'Submitted', expired: 'Expired', }; -const DOWNLOAD_LABEL: Record = { - none: 'No downloads', - selection: 'Selection as ZIP', - album: 'Whole album as ZIP', -}; - function formatDate(iso: string | null): string { if (!iso) return '—'; const date = iso.length === 10 ? new Date(`${iso}T12:00:00`) : new Date(iso); @@ -54,7 +46,7 @@ function CreateForm({ onCreated }: { onCreated: () => void }) { const [albumId, setAlbumId] = useState(''); const [alias, setAlias] = useState(''); const [expiresOn, setExpiresOn] = useState(''); - const [download, setDownload] = useState('none'); + const [downloadsAllowed, setDownloadsAllowed] = useState(false); const [limit, setLimit] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); @@ -80,15 +72,15 @@ function CreateForm({ onCreated }: { onCreated: () => void }) { alias: alias.trim() || null, albumId, expiresOn: expiresOn || null, - download, - downloadLimit: download !== 'none' && limit ? Number(limit) : null, + download: downloadsAllowed ? 'selection' : 'none', + downloadLimit: downloadsAllowed && limit ? Number(limit) : null, }), }); setClientName(''); setAlias(''); setAlbumId(''); setExpiresOn(''); - setDownload('none'); + setDownloadsAllowed(false); setLimit(''); onCreated(); } catch (err) { @@ -159,21 +151,21 @@ function CreateForm({ onCreated }: { onCreated: () => void }) { onChange={(e) => setExpiresOn(e.target.value)} /> -
- - -
- {download !== 'none' && ( +
+ + + +
+ {downloadsAllowed && (
diff --git a/components/ProofSessionControls.tsx b/components/ProofSessionControls.tsx index 77dcbe5..4808168 100644 --- a/components/ProofSessionControls.tsx +++ b/components/ProofSessionControls.tsx @@ -1,13 +1,5 @@ 'use client'; -/** - * The selection bar and review dialog of a client proofing link. - * - * Stands in for the anonymous mode's bar and share/export modal: there is - * nothing to share (the link already is the share), the selection saves itself, - * and the one action that matters is submitting it to the photographer. - */ - import { useCallback, useState } from 'react'; import { useProofing } from './ProofingContext'; import { useDictionary } from './I18nProvider'; @@ -22,8 +14,8 @@ const barStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', flexWrap: 'wrap', - gap: '12px', - padding: '8px 16px', + gap: '10px', + padding: '8px 12px', borderRadius: 'var(--radius-lg)', background: 'var(--bg-card, #1e1e1e)', color: 'var(--text-primary, #ffffff)', @@ -33,6 +25,32 @@ const barStyle: React.CSSProperties = { maxWidth: 'calc(100vw - 32px)', }; +const barButton: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '0.4rem', + padding: '7px 12px', + borderRadius: 'var(--radius-md)', + background: 'var(--bg-card-hover)', + color: 'var(--text-primary)', + border: '1px solid var(--border-subtle)', + fontFamily: 'inherit', + fontSize: '0.85rem', + fontWeight: 500, + lineHeight: 1.3, + cursor: 'pointer', + textDecoration: 'none', +}; + +const downloadButton: React.CSSProperties = { + ...barButton, + background: 'var(--accent, #e60012)', + color: 'var(--on-accent, #fff)', + border: 'none', + fontWeight: 600, +}; + const primaryButton: React.CSSProperties = { display: 'flex', alignItems: 'center', @@ -41,37 +59,39 @@ const primaryButton: React.CSSProperties = { padding: '0.75rem 1rem', borderRadius: 'var(--radius-sm, 6px)', background: 'var(--accent, #e60012)', - // Black or white, whichever the layout found to carry on this accent. color: 'var(--on-accent, #fff)', border: 'none', fontWeight: 500, - // The same for the + ))} + + {/* Select / deselect every photo */} + {!session.submitted && ( )} + + {/* Selection counter / filter */} + + {session.submitted ? p.locked : status} +
@@ -176,7 +284,8 @@ export function ProofSessionControls() { background: 'var(--bg-card, #1e1e1e)', color: 'var(--text-primary, #ffffff)', borderRadius: 'var(--radius-md, 12px)', - border: '1px solid var(--border-subtle, rgba(255,255,255,0.1))', + border: + '1px solid var(--border-subtle, rgba(255,255,255,0.1))', padding: '1.5rem', maxWidth: '480px', width: '100%', @@ -193,10 +302,15 @@ export function ProofSessionControls() { >

{p.modalTitle(count)}

+ )} - {canDownload && count > 0 && selectionSaved && ( - - {p.downloadSelection} - - )} - {canDownload && session.download === 'album' && ( - - {p.downloadAll} - - )} - {session.download !== 'none' && downloadsLeft !== null && ( -

- {downloadsLeft > 0 ? p.downloadsLeft(downloadsLeft) : t.download.limitReached} -

- )} + {session.download !== 'none' && + downloadsLeft !== null && ( +

+ {downloadsLeft > 0 + ? p.downloadsLeft(downloadsLeft) + : t.download.limitReached} +

+ )} + {session.saveState === 'error' && ( -

+

{p.saveFailed}

)} diff --git a/components/ProofingContext.tsx b/components/ProofingContext.tsx index 2a5738a..9bff1ab 100644 --- a/components/ProofingContext.tsx +++ b/components/ProofingContext.tsx @@ -59,6 +59,11 @@ export interface ProofingContextType { getFormattedList: () => string; /** Selected asset tokens in album order — the payload for a ZIP download. */ getSelectedTokens: () => string[]; + /** Number of assets in the current album. */ + albumCount: number; + + /** Select every asset, or clear everything when all are already selected. */ + toggleSelectAll: () => void; allowMailto: boolean; /** * `encodeEmail()` of the "email to photographer" recipient, when one is @@ -284,6 +289,20 @@ export function ProofingProvider({ } }; + const toggleSelectAll = () => { + if (!sessionInit || submitted) return; + + editSelection((prev) => { + const allSelected = + albumTokens.length > 0 && + albumTokens.every((token) => prev.has(token)); + + return allSelected + ? new Set() + : new Set(albumTokens); + }); + }; + const getProofingUrl = () => { if (albumTokens.length === 0 || favorites.size === 0) { return typeof window !== 'undefined' ? window.location.href : ''; @@ -335,6 +354,8 @@ export function ProofingProvider({ getProofingUrl, getFormattedList, getSelectedTokens, + albumCount: albumTokens.length, + toggleSelectAll, allowMailto, encodedMailto, downloadArchiveUrl,