invite/src/lib/themes.ts
chris a5775930aa Add photo ZIP downloads, HEIC/HEIF uploads, thank-you notes, ICS export, and theme color overrides
- Photos: guests and hosts can download all event photos as a ZIP;
  HEIC/HEIF uploads are now accepted, with a WebP thumbnail generated
  for gallery display while the original file is preserved for
  download; uploader name now always shows as a caption
- Hosts can send a thank-you note (email/SMS) to guests who RSVP'd yes
- Guests can add the event to Google Calendar or download an .ics file,
  and see a live countdown to the event
- Hosts can override a theme's primary/accent colors per event
- New textures for a few themes' card backgrounds
2026-07-19 18:23:05 -04:00

246 lines
6.5 KiB
TypeScript

import {
damaskPattern,
confettiPattern,
leafPattern,
polkaDotPattern,
snowflakePattern,
starPattern,
scratchPattern,
pixelGridPattern,
floralLacePattern,
palmLeafPattern,
wheatPattern,
gradCapPattern,
texturePattern,
combinePatterns,
type Pattern,
} from '@/lib/patterns';
import { deriveThemeColors, type ThemeColors } from '@/lib/color';
export type Theme = {
id: string;
name: string;
emoji: string;
/** Light card vs. dark card - drives how resolveTheme derives a full
* palette from a host's custom primary/accent choice (see color.ts). */
mode: 'light' | 'dark';
colors: ThemeColors;
pattern: Pattern;
/** Rebuilds this theme's pattern for an arbitrary color set - used by
* resolveTheme when a host overrides the default colors. */
buildPattern: (colors: ThemeColors) => Pattern;
};
type ThemeDefinition = Omit<Theme, 'pattern'>;
function defineTheme(def: ThemeDefinition): Theme {
return { ...def, pattern: def.buildPattern(def.colors) };
}
// Colors applied via CSS custom properties (see ThemedInviteLayout).
// Fonts are mapped separately in src/lib/fonts.ts since next/font classes
// must be static imports, not computed dynamically. `pattern` is a tileable
// inline-SVG background (see src/lib/patterns.ts) that gives each theme a
// distinct texture, not just a color/font swap. `buildPattern` regenerates
// that same pattern for a different color set - see resolveTheme below.
export const THEMES: Theme[] = [
defineTheme({
id: 'classic-elegant',
name: 'Classic Elegant',
emoji: '🥂',
mode: 'light',
colors: {
bg: '#f5f1ea',
surface: '#ffffff',
primary: '#4b3b2a',
accent: '#b08d57',
text: '#2b2118',
},
buildPattern: (c) => combinePatterns(damaskPattern(c.accent), texturePattern('/textures/exclusive-paper.png', 560, 420)),
}),
defineTheme({
id: 'birthday-confetti',
name: 'Birthday Confetti',
emoji: '🎉',
mode: 'light',
colors: {
bg: '#fff3f8',
surface: '#ffffff',
primary: '#e0218a',
accent: '#ffb703',
text: '#3a1030',
},
buildPattern: (c) => confettiPattern([c.primary, c.accent, '#3dd6c2']),
}),
defineTheme({
id: 'garden-party',
name: 'Garden Party',
emoji: '🌿',
mode: 'light',
colors: {
bg: '#f1f7ee',
surface: '#ffffff',
primary: '#3f6b3a',
accent: '#e0a458',
text: '#233720',
},
buildPattern: (c) => leafPattern(c.primary, c.accent),
}),
defineTheme({
id: 'modern-minimal',
name: 'Modern Minimal',
emoji: '⬛',
mode: 'light',
colors: {
bg: '#fafafa',
surface: '#ffffff',
primary: '#111827',
accent: '#2563eb',
text: '#111827',
},
buildPattern: (c) => polkaDotPattern(c.accent, 1.75),
}),
defineTheme({
id: 'holiday-festive',
name: 'Holiday Festive',
emoji: '❄️',
mode: 'dark',
colors: {
bg: '#0f2d24',
surface: '#153a2e',
primary: '#e63946',
accent: '#f1c40f',
text: '#fdf6ec',
},
buildPattern: (c) => combinePatterns(snowflakePattern(c.accent), texturePattern('/textures/fresh-snow.png', 500, 500)),
}),
defineTheme({
id: 'baby-shower-pastel',
name: 'Baby Shower Pastel',
emoji: '🍼',
mode: 'light',
colors: {
bg: '#f4f1fb',
surface: '#ffffff',
primary: '#8e7cc3',
accent: '#f6b6c1',
text: '#3a3350',
},
buildPattern: (c) => starPattern([c.primary, c.accent]),
}),
defineTheme({
id: 'goth-grunge',
name: 'Goth Grunge',
emoji: '💀',
mode: 'dark',
colors: {
bg: '#121212',
surface: '#1c1c1c',
primary: '#b91c1c',
accent: '#9333ea',
text: '#e5e5e5',
},
buildPattern: (c) => combinePatterns(scratchPattern(c.accent), texturePattern('/textures/dark-leather.png', 398, 484)),
}),
defineTheme({
id: 'retro-arcade',
name: 'Retro Arcade',
emoji: '🕹️',
mode: 'dark',
colors: {
bg: '#0d0221',
surface: '#1a0b3d',
primary: '#ff2e88',
accent: '#00e5ff',
text: '#eafffb',
},
buildPattern: (c) => pixelGridPattern(c.accent, [c.primary, c.accent]),
}),
defineTheme({
id: 'wedding-romance',
name: 'Wedding Romance',
emoji: '💍',
mode: 'light',
colors: {
bg: '#fdf5f2',
surface: '#ffffff',
primary: '#8c5b6b',
accent: '#c9a45c',
text: '#3a2a2e',
},
buildPattern: (c) => combinePatterns(floralLacePattern(c.accent), texturePattern('/textures/white-linen.png', 400, 300)),
}),
defineTheme({
id: 'tropical-luau',
name: 'Tropical Luau',
emoji: '🌺',
mode: 'light',
colors: {
bg: '#e6fbf7',
surface: '#ffffff',
primary: '#0f766e',
accent: '#f59e0b',
text: '#0b3d3a',
},
buildPattern: (c) => palmLeafPattern(c.primary, c.accent),
}),
defineTheme({
id: 'rustic-harvest',
name: 'Rustic Harvest',
emoji: '🍂',
mode: 'light',
colors: {
bg: '#f7ecd9',
surface: '#fffaf1',
primary: '#7a4a24',
accent: '#c1622d',
text: '#3d2812',
},
buildPattern: (c) => combinePatterns(wheatPattern(c.accent), texturePattern('/textures/cardboard.png', 600, 600)),
}),
defineTheme({
id: 'graduation-grad',
name: 'Graduation',
emoji: '🎓',
mode: 'dark',
colors: {
bg: '#0b1f3a',
surface: '#122a4d',
primary: '#f2c14e',
accent: '#f2c14e',
text: '#f5f5f0',
},
buildPattern: (c) => gradCapPattern(c.accent),
}),
];
export const DEFAULT_THEME_ID = THEMES[0].id;
export function getTheme(id: string): Theme {
return THEMES.find((t) => t.id === id) ?? THEMES[0];
}
/**
* Resolves a theme for rendering, with optional host-chosen primary/accent
* overrides (Event.themePrimaryColor / themeAccentColor). With no overrides,
* returns the theme's own hand-tuned default palette and pattern completely
* unchanged. With one or both overrides, derives a full contrast-safe
* palette (see deriveThemeColors) and rebuilds the pattern to match, so a
* customized theme never ends up illegible.
*/
export function resolveTheme(
id: string,
primaryOverride?: string | null,
accentOverride?: string | null
): Theme {
const base = getTheme(id);
if (!primaryOverride && !accentOverride) return base;
const colors = deriveThemeColors(
base.mode,
primaryOverride || base.colors.primary,
accentOverride || base.colors.accent
);
return { ...base, colors, pattern: base.buildPattern(colors) };
}