Merge pull request 'JRCC logos (light/dark) + watermark + 9-style picker' (#75) from feature/jrcc-brand-styles into master
CI / skip-ci-check (push) Successful in 29s
CI / docker-ci (push) Successful in 30s
CI / python-lint (push) Successful in 31s
CI / secret-scan (push) Successful in 35s
CI / viewer-unit (push) Successful in 1m35s
CI / admin-unit (push) Successful in 1m52s
CI / e2e (push) Successful in 3m21s
CI / skip-ci-check (push) Successful in 29s
CI / docker-ci (push) Successful in 30s
CI / python-lint (push) Successful in 31s
CI / secret-scan (push) Successful in 35s
CI / viewer-unit (push) Successful in 1m35s
CI / admin-unit (push) Successful in 1m52s
CI / e2e (push) Successful in 3m21s
This commit was merged in pull request #75.
This commit is contained in:
@@ -103,3 +103,8 @@ logs/
|
||||
e2e/env-defaults.local.json
|
||||
|
||||
viewer-frontend/.data/
|
||||
|
||||
# Brand logos (explicitly tracked despite *.png)
|
||||
!viewer-frontend/public/logo.png
|
||||
!viewer-frontend/public/brand/
|
||||
!viewer-frontend/public/brand/**/*.png
|
||||
|
||||
@@ -28,7 +28,7 @@ Dev URLs: https://punimtagdev.levkin.ca · https://punimtagadmindev.levkin.ca
|
||||
|
||||
| Issue | Need |
|
||||
|-------|------|
|
||||
| **#44** JRCC watermark | SVG/PNG + placement/opacity |
|
||||
| **#44** JRCC watermark | ✅ PNG watermark + light/dark header logos (`public/brand/`, `/styles` picker) |
|
||||
| **#43** Tag without account | Product rules (approval, PII, ToS) |
|
||||
|
||||
## Performance notes (#26)
|
||||
|
||||
@@ -5,10 +5,10 @@ import { createPortal } from 'react-dom';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ManageUsersContent } from './ManageUsersContent';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import UserMenu from '@/components/UserMenu';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import { JrccLogo } from '@/components/JrccLogo';
|
||||
|
||||
interface ManageUsersPageClientProps {
|
||||
onClose?: () => void;
|
||||
@@ -34,16 +34,9 @@ export function ManageUsersPageClient({ onClose }: ManageUsersPageClientProps) {
|
||||
<main id="main-content" className="w-full px-4 pt-3 pb-8">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="PunimTag"
|
||||
width={300}
|
||||
height={80}
|
||||
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
|
||||
priority
|
||||
/>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
|
||||
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-1">
|
||||
<ThemeToggle />
|
||||
|
||||
@@ -45,38 +45,49 @@ async function getWatermarkOverlay(baseWidth: number, baseHeight: number) {
|
||||
let cached: Buffer | undefined = watermarkCache.get(cacheKey);
|
||||
if (!cached) {
|
||||
const diagonal = Math.sqrt(bucketWidth * bucketWidth + bucketHeight * bucketHeight);
|
||||
const watermarkWidth = Math.round(diagonal * 1.05);
|
||||
const watermarkHeight = Math.round((watermarkWidth * 120) / 360);
|
||||
const watermarkSvg = `
|
||||
<svg width="${watermarkWidth}" height="${watermarkHeight}" viewBox="0 0 360 120" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="wmGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1a3c7c" stop-opacity="0.45"/>
|
||||
<stop offset="100%" stop-color="#0a1e3f" stop-opacity="0.45"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="360" height="120" fill="url(#wmGradient)" rx="20" ry="20" />
|
||||
<g fill="rgba(255,255,255,0.65)">
|
||||
<text x="30" y="82" font-family="'Inter','Segoe UI',sans-serif" font-size="72" font-weight="700">J</text>
|
||||
<text x="144" y="82" font-family="'Inter','Segoe UI',sans-serif" font-size="72" font-weight="700">M</text>
|
||||
<g transform="translate(80,20) scale(0.7)">
|
||||
<polygon points="60,0 80,35 120,35 90,57 100,95 60,75 20,95 30,57 0,35 40,35"
|
||||
fill="none" stroke="rgba(255,255,255,0.7)" stroke-width="12" stroke-linejoin="round" />
|
||||
</g>
|
||||
<text x="214" y="50" font-family="'Inter','Segoe UI',sans-serif" font-size="24" font-weight="600">Jewish</text>
|
||||
<text x="214" y="78" font-family="'Inter','Segoe UI',sans-serif" font-size="24" font-weight="600">and Modern</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
const watermarkWidth = Math.round(diagonal * 0.55);
|
||||
const logoPath = path.join(process.cwd(), 'public', 'brand', 'jrcc-watermark.png');
|
||||
const fallbackLogoPath = path.join(process.cwd(), 'public', 'brand', 'jrcc-logo-light.png');
|
||||
const logoFile = existsSync(logoPath) ? logoPath : fallbackLogoPath;
|
||||
|
||||
let logoBuffer: Buffer;
|
||||
if (existsSync(logoFile)) {
|
||||
logoBuffer = await sharp(logoFile)
|
||||
.resize({
|
||||
width: watermarkWidth,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: false,
|
||||
})
|
||||
.ensureAlpha()
|
||||
.toBuffer();
|
||||
} else {
|
||||
// Fallback SVG if brand assets are missing
|
||||
const watermarkHeight = Math.round((watermarkWidth * 120) / 360);
|
||||
const watermarkSvg = `
|
||||
<svg width="${watermarkWidth}" height="${watermarkHeight}" viewBox="0 0 360 120" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="20" y="78" font-family="'Inter','Segoe UI',sans-serif" font-size="64" font-weight="700" fill="rgba(255,255,255,0.55)">JRCC</text>
|
||||
</svg>
|
||||
`;
|
||||
logoBuffer = await sharp(Buffer.from(watermarkSvg)).ensureAlpha().toBuffer();
|
||||
}
|
||||
|
||||
const logoMeta = await sharp(logoBuffer).metadata();
|
||||
const logoW = logoMeta.width || watermarkWidth;
|
||||
const logoH = logoMeta.height || Math.round(watermarkWidth * 0.33);
|
||||
const canvas = await sharp({
|
||||
create: {
|
||||
width: logoW + 80,
|
||||
height: logoH + 60,
|
||||
channels: 4,
|
||||
background: { r: 10, g: 30, b: 63, alpha: 0.28 },
|
||||
},
|
||||
})
|
||||
.composite([{ input: logoBuffer, gravity: 'centre' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const rotationAngle = -Math.atan(bucketHeight / bucketWidth) * (180 / Math.PI);
|
||||
const newCached = await sharp(Buffer.from(watermarkSvg))
|
||||
.resize({
|
||||
width: watermarkWidth,
|
||||
height: watermarkHeight,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
const newCached = await sharp(canvas)
|
||||
.rotate(rotationAngle, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.resize({
|
||||
width: bucketWidth,
|
||||
|
||||
@@ -10,8 +10,8 @@ const inter = Inter({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PunimTag Photo Viewer",
|
||||
description: "Browse and search your family photos",
|
||||
title: "JRCC Photo Gallery",
|
||||
description: "Jewish Russian Community Centre of Ontario — browse and search community photos",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import { JrccLogo } from '@/components/JrccLogo';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const STORAGE_KEY = 'jrcc-gallery-style-pick';
|
||||
|
||||
type StyleId =
|
||||
| 'flame-forum'
|
||||
| 'memory-lane'
|
||||
| 'face-challenge'
|
||||
| 'reunion-pulse'
|
||||
| 'story-shelves'
|
||||
| 'soft-salon'
|
||||
| 'night-gallery'
|
||||
| 'digest-desk'
|
||||
| 'cluster-lab';
|
||||
|
||||
type StyleOption = {
|
||||
id: StyleId;
|
||||
name: string;
|
||||
tagline: string;
|
||||
whyComeBack: string;
|
||||
palette: string[];
|
||||
fonts: string;
|
||||
mood: string;
|
||||
previewClass: string;
|
||||
accentClass: string;
|
||||
sampleTitle: string;
|
||||
sampleSub: string;
|
||||
};
|
||||
|
||||
const STYLES: StyleOption[] = [
|
||||
{
|
||||
id: 'flame-forum',
|
||||
name: 'Flame & Forum',
|
||||
tagline: 'Community board energy',
|
||||
whyComeBack: 'See who tagged faces this week and jump into open unknowns.',
|
||||
palette: ['#0B1F4B', '#1E40AF', '#F5C518', '#F8FAFC'],
|
||||
fonts: 'Display: Manrope · Body: Source Sans 3',
|
||||
mood: 'Confident JRCC blue/gold, notice-board rhythm',
|
||||
previewClass: 'bg-[#0B1F4B] text-white',
|
||||
accentClass: 'bg-[#F5C518] text-[#0B1F4B]',
|
||||
sampleTitle: "Who's new this week",
|
||||
sampleSub: '12 faces waiting · 4 people tagged',
|
||||
},
|
||||
{
|
||||
id: 'memory-lane',
|
||||
name: 'Memory Lane',
|
||||
tagline: 'On this day, years ago',
|
||||
whyComeBack: 'Daily nostalgia: same date across decades of community photos.',
|
||||
palette: ['#2A2118', '#C4A484', '#F3EDE4', '#8B4513'],
|
||||
fonts: 'Display: Fraunces · Body: Literata',
|
||||
mood: 'Warm archival paper, soft film grain',
|
||||
previewClass: 'bg-[#F3EDE4] text-[#2A2118]',
|
||||
accentClass: 'bg-[#8B4513] text-[#F3EDE4]',
|
||||
sampleTitle: 'On this day · Aug 4',
|
||||
sampleSub: 'Photos from 1998 · 2007 · 2019',
|
||||
},
|
||||
{
|
||||
id: 'face-challenge',
|
||||
name: 'Face Challenge',
|
||||
tagline: 'Five unknowns a day',
|
||||
whyComeBack: 'A short daily streak: help name faces, climb a light scoreboard.',
|
||||
palette: ['#111827', '#22D3EE', '#F472B6', '#F9FAFB'],
|
||||
fonts: 'Display: Space Grotesk · Body: IBM Plex Sans',
|
||||
mood: 'Playful streak UI, big face crops, one-tap suggest',
|
||||
previewClass: 'bg-[#111827] text-white',
|
||||
accentClass: 'bg-[#22D3EE] text-[#111827]',
|
||||
sampleTitle: 'Daily challenge · 3 / 5',
|
||||
sampleSub: 'Streak: 6 days · Keep going',
|
||||
},
|
||||
{
|
||||
id: 'reunion-pulse',
|
||||
name: 'Reunion Pulse',
|
||||
tagline: 'Event mode progress',
|
||||
whyComeBack: 'Live “faces still unknown” bar after weddings, picnics, holidays.',
|
||||
palette: ['#7F1D1D', '#FCA5A5', '#FEF2F2', '#1F2937'],
|
||||
fonts: 'Display: Sora · Body: DM Sans',
|
||||
mood: 'Celebration meter, upload dump → clear the queue',
|
||||
previewClass: 'bg-[#FEF2F2] text-[#7F1D1D]',
|
||||
accentClass: 'bg-[#7F1D1D] text-white',
|
||||
sampleTitle: 'Purim 2026 album',
|
||||
sampleSub: '68% identified · 41 faces left',
|
||||
},
|
||||
{
|
||||
id: 'story-shelves',
|
||||
name: 'Story Shelves',
|
||||
tagline: 'People as timelines',
|
||||
whyComeBack: 'Open a person and scroll their life across events — not crop grids.',
|
||||
palette: ['#1C1917', '#A8A29E', '#FAFAF9', '#0EA5E9'],
|
||||
fonts: 'Display: Newsreader · Body: Karla',
|
||||
mood: 'Editorial shelves, chapter headings per decade',
|
||||
previewClass: 'bg-[#FAFAF9] text-[#1C1917]',
|
||||
accentClass: 'bg-[#0EA5E9] text-white',
|
||||
sampleTitle: 'Anna K. · Timeline',
|
||||
sampleSub: '1989 → Toronto · 112 photos',
|
||||
},
|
||||
{
|
||||
id: 'soft-salon',
|
||||
name: 'Soft Salon',
|
||||
tagline: 'Calm museum gallery',
|
||||
whyComeBack: 'Quiet browsing with generous space — invite family without noise.',
|
||||
palette: ['#F7F5F2', '#3F3A36', '#9A8F84', '#D6CFC6'],
|
||||
fonts: 'Display: Cormorant Garamond · Body: Outfit',
|
||||
mood: 'Breathing room, soft stone, restrained motion',
|
||||
previewClass: 'bg-[#F7F5F2] text-[#3F3A36]',
|
||||
accentClass: 'bg-[#3F3A36] text-[#F7F5F2]',
|
||||
sampleTitle: 'Collection',
|
||||
sampleSub: 'Search by people · seasons · places',
|
||||
},
|
||||
{
|
||||
id: 'night-gallery',
|
||||
name: 'Night Gallery',
|
||||
tagline: 'Dark-first cinema',
|
||||
whyComeBack: 'Evening browsing: luminous photos on deep navy, focus on faces.',
|
||||
palette: ['#020617', '#1E3A8A', '#E2E8F0', '#FBBF24'],
|
||||
fonts: 'Display: Syne · Body: Figtree',
|
||||
mood: 'Cinematic dark, gold spark accents',
|
||||
previewClass: 'bg-[#020617] text-[#E2E8F0]',
|
||||
accentClass: 'bg-[#FBBF24] text-[#020617]',
|
||||
sampleTitle: 'Evening gallery',
|
||||
sampleSub: 'Favorites · recently tagged',
|
||||
},
|
||||
{
|
||||
id: 'digest-desk',
|
||||
name: 'Digest Desk',
|
||||
tagline: 'Reasons to open mail',
|
||||
whyComeBack: 'Weekly digest preview in-app: 3 photos + one “who is this?”',
|
||||
palette: ['#0F172A', '#334155', '#F1F5F9', '#EA580C'],
|
||||
fonts: 'Display: Libre Baskerville · Body: Public Sans',
|
||||
mood: 'Editorial inbox, clip + caption cards',
|
||||
previewClass: 'bg-[#F1F5F9] text-[#0F172A]',
|
||||
accentClass: 'bg-[#EA580C] text-white',
|
||||
sampleTitle: 'This week’s digest',
|
||||
sampleSub: '3 memories · 1 mystery face',
|
||||
},
|
||||
{
|
||||
id: 'cluster-lab',
|
||||
name: 'Cluster Lab',
|
||||
tagline: 'Group lookalikes',
|
||||
whyComeBack: 'Name one stranger → cascade to a cluster of similar unknowns.',
|
||||
palette: ['#042F2E', '#14B8A6', '#CCFBF1', '#134E4A'],
|
||||
fonts: 'Display: Rubik · Body: Nunito Sans',
|
||||
mood: 'Lab grid of face clusters, teal precision',
|
||||
previewClass: 'bg-[#042F2E] text-[#CCFBF1]',
|
||||
accentClass: 'bg-[#14B8A6] text-[#042F2E]',
|
||||
sampleTitle: 'Cluster · 9 similar faces',
|
||||
sampleSub: 'Name one to link the rest',
|
||||
},
|
||||
];
|
||||
|
||||
function StylePreview({ style }: { style: StyleOption }) {
|
||||
return (
|
||||
<div className={cn('rounded-xl overflow-hidden border border-black/10 shadow-sm', style.previewClass)}>
|
||||
<div className="px-3 pt-3 pb-2 flex items-center justify-between gap-2">
|
||||
<div className="text-[10px] uppercase tracking-[0.14em] opacity-70">JRCC gallery</div>
|
||||
<span className={cn('text-[10px] font-semibold px-2 py-0.5 rounded-full', style.accentClass)}>
|
||||
preview
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-3 pb-3">
|
||||
<div className="text-sm font-semibold leading-tight">{style.sampleTitle}</div>
|
||||
<div className="text-[11px] opacity-75 mt-0.5">{style.sampleSub}</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-square rounded-md bg-black/10 dark:bg-white/10 flex items-center justify-center text-[10px] opacity-60"
|
||||
>
|
||||
photo
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex gap-1.5">
|
||||
{style.palette.map((c) => (
|
||||
<span
|
||||
key={c}
|
||||
title={c}
|
||||
className="h-3 w-3 rounded-full border border-black/15"
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StylesPickerClient() {
|
||||
const [picked, setPicked] = useState<StyleId | null>(null);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = window.localStorage.getItem(STORAGE_KEY) as StyleId | null;
|
||||
if (saved && STYLES.some((s) => s.id === saved)) {
|
||||
setPicked(saved);
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
const selected = useMemo(
|
||||
() => STYLES.find((s) => s.id === picked) ?? null,
|
||||
[picked]
|
||||
);
|
||||
|
||||
const choose = (id: StyleId) => {
|
||||
setPicked(id);
|
||||
window.localStorage.setItem(STORAGE_KEY, id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur">
|
||||
<div className="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between gap-3">
|
||||
<Link href="/" aria-label="JRCC home" className="min-w-0">
|
||||
<JrccLogo heightClassName="h-10" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle />
|
||||
<Link
|
||||
href="/"
|
||||
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted transition-colors"
|
||||
>
|
||||
Back to gallery
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main-content" className="mx-auto max-w-6xl px-4 py-8">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-xs uppercase tracking-[0.16em] text-muted-foreground mb-2">
|
||||
Design lab · pick one
|
||||
</p>
|
||||
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight">
|
||||
Nine styles for a gallery people return to
|
||||
</h1>
|
||||
<p className="mt-3 text-muted-foreground leading-relaxed">
|
||||
Each card is a direction for more excitement and habit — not a full rebuild yet.
|
||||
Toggle light/dark above to check the JRCC logos. Tap a style to save your pick
|
||||
(stored in this browser only).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{hydrated && selected && (
|
||||
<div className="mt-6 rounded-xl border bg-card p-4 flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wide text-muted-foreground">Your pick</div>
|
||||
<div className="font-semibold text-lg">{selected.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{selected.whyComeBack}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
setPicked(null);
|
||||
}}
|
||||
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted self-start"
|
||||
>
|
||||
Clear pick
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{STYLES.map((style, index) => {
|
||||
const isSelected = picked === style.id;
|
||||
return (
|
||||
<button
|
||||
key={style.id}
|
||||
type="button"
|
||||
onClick={() => choose(style.id)}
|
||||
className={cn(
|
||||
'text-left rounded-2xl border p-3 transition-all hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
isSelected ? 'border-primary ring-2 ring-primary/40' : 'border-border'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-2 mb-2 px-0.5">
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<span className="text-xs font-medium text-primary">Selected</span>
|
||||
)}
|
||||
</div>
|
||||
<StylePreview style={style} />
|
||||
<div className="mt-3 px-0.5">
|
||||
<h2 className="font-semibold text-base">{style.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">{style.tagline}</p>
|
||||
<p className="mt-2 text-sm leading-snug">{style.whyComeBack}</p>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">{style.fonts}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{style.mood}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<section className="mt-12 mb-8 rounded-2xl border p-5 bg-muted/30">
|
||||
<h2 className="font-semibold text-lg">Logo usage (this pass)</h2>
|
||||
<ul className="mt-2 text-sm text-muted-foreground space-y-1.5 list-disc pl-5">
|
||||
<li>
|
||||
<strong className="text-foreground">Light mode header:</strong> classic black
|
||||
bilingual JRCC mark (`jrcc-logo-light.png`)
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Dark mode header:</strong> white + blue/gold
|
||||
mark (`jrcc-logo-dark.png`)
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Photo watermark:</strong> translucent JRCC
|
||||
overlay for guest downloads (`jrcc-watermark.png`)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
Tell us which style number wins and we'll implement that direction next.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Metadata } from 'next';
|
||||
import StylesPickerClient from './StylesPickerClient';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'JRCC gallery — style picker',
|
||||
description: 'Pick a visual direction for the JRCC photo gallery redesign.',
|
||||
};
|
||||
|
||||
export default function StylesPage() {
|
||||
return <StylesPickerClient />;
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import { useRouter } from 'next/navigation';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { UploadContent } from './UploadContent';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import UserMenu from '@/components/UserMenu';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import { JrccLogo } from '@/components/JrccLogo';
|
||||
|
||||
export function UploadPageClient() {
|
||||
const router = useRouter();
|
||||
@@ -21,16 +21,9 @@ export function UploadPageClient() {
|
||||
<main id="main-content" className="w-full px-4 pt-3 pb-8">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="PunimTag"
|
||||
width={300}
|
||||
height={80}
|
||||
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
|
||||
priority
|
||||
/>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
|
||||
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-1">
|
||||
<ThemeToggle />
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const emptySubscribe = () => () => {};
|
||||
|
||||
function useMounted() {
|
||||
return useSyncExternalStore(
|
||||
emptySubscribe,
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
}
|
||||
|
||||
type JrccLogoProps = {
|
||||
className?: string;
|
||||
/** Header height in CSS (Tailwind class). */
|
||||
heightClassName?: string;
|
||||
priority?: boolean;
|
||||
alt?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* JRCC wordmark that swaps light/dark assets after hydration.
|
||||
* Light: black bilingual classic. Dark: white + blue/gold mark.
|
||||
*/
|
||||
export function JrccLogo({
|
||||
className,
|
||||
heightClassName = 'h-11',
|
||||
priority = false,
|
||||
alt = 'JRCC — Jewish Russian Community Centre of Ontario',
|
||||
}: JrccLogoProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const mounted = useMounted();
|
||||
const isDark = mounted && resolvedTheme === 'dark';
|
||||
const src = isDark ? '/brand/jrcc-logo-dark.png' : '/brand/jrcc-logo-light.png';
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={420}
|
||||
height={140}
|
||||
className={cn(heightClassName, 'w-auto max-w-[min(100%,280px)] sm:max-w-[340px]', className)}
|
||||
priority={priority}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
import { JrccLogo } from '@/components/JrccLogo';
|
||||
|
||||
interface PageHeaderProps {
|
||||
photosCount: number;
|
||||
@@ -35,18 +35,11 @@ export function PageHeader({
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="PunimTag"
|
||||
width={300}
|
||||
height={80}
|
||||
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
|
||||
priority
|
||||
/>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
|
||||
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<ThemeToggle />
|
||||
<UserMenu />
|
||||
</div>
|
||||
@@ -72,9 +65,3 @@ export function PageHeader({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import UserMenu from '@/components/UserMenu';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import { JrccLogo } from '@/components/JrccLogo';
|
||||
|
||||
export function SimpleHeader() {
|
||||
return (
|
||||
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="PunimTag"
|
||||
width={300}
|
||||
height={80}
|
||||
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
|
||||
priority
|
||||
/>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
|
||||
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<ThemeToggle />
|
||||
<UserMenu />
|
||||
</div>
|
||||
@@ -30,9 +23,3 @@ export function SimpleHeader() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Brand assets (JRCC)
|
||||
|
||||
| File | Use |
|
||||
|------|-----|
|
||||
| `jrcc-logo-light.png` | Top-left header in **light** mode (classic B&W bilingual) |
|
||||
| `jrcc-logo-dark.png` | Top-left header in **dark** mode (white + blue/gold) |
|
||||
| `jrcc-watermark.png` | Diagonal guest watermark on photos |
|
||||
| `../logo.png` | SSR / fallback copy of the light logo |
|
||||
|
||||
Source PDFs (not committed): `JRCC Logo - Rus-Eng.pdf`, `JRCC Logo 1 - Vector.pdf`.
|
||||
|
||||
Style picker (nine directions): `/styles`
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Reference in New Issue
Block a user