feat(theme): animate public background with aurora and particles, polish landing pages

Add background_effect (aurora/particles), background_overlay tint and
opacity to the theme manager, rendered site-wide by ThemeVars on every
public page. Polish the home and register pages (hero mascot, live stat
pulse, date pills, photo strip, CTA band, theme-aware register intro,
i18n for home/register section).
This commit is contained in:
openhands committed 2026-09-18 12:44:39 +02:00
1 parent 5923b736fa
commit d131124515
9 files changed
+573 -25

No files matched your search

+52
View File
@@ -12,10 +12,12 @@ import { siteSettings } from "@/lib/services/site-settings";
import { logStaffActivity } from "@/lib/services/staff-activity"; import { logStaffActivity } from "@/lib/services/staff-activity";
import { import {
BACKGROUND_DEFAULTS, BACKGROUND_DEFAULTS,
BACKGROUND_EFFECTS,
BACKGROUND_POSITIONS, BACKGROUND_POSITIONS,
BACKGROUND_REPEATS, BACKGROUND_REPEATS,
BACKGROUND_SIZES, BACKGROUND_SIZES,
isSafeBackgroundUrl, isSafeBackgroundUrl,
OVERLAY_OPACITY_MAX,
} from "@/lib/theme-bg"; } from "@/lib/theme-bg";
import { import {
type CustomTheme, type CustomTheme,
@@ -187,6 +189,17 @@ export async function saveBackground(formData: FormData): Promise<void> {
const repeat = String(formData.get("background_repeat") ?? "") const repeat = String(formData.get("background_repeat") ?? "")
.normalize("NFC") .normalize("NFC")
.trim(); .trim();
const effect = String(formData.get("background_effect") ?? "")
.normalize("NFC")
.trim();
const overlay = String(formData.get("background_overlay") ?? "")
.normalize("NFC")
.trim();
const overlayOpacity = String(
formData.get("background_overlay_opacity") ?? "",
)
.normalize("NFC")
.trim();
if (image && !isSafeBackgroundUrl(image)) redirect(MANAGER_PATH); if (image && !isSafeBackgroundUrl(image)) redirect(MANAGER_PATH);
@@ -210,6 +223,22 @@ export async function saveBackground(formData: FormData): Promise<void> {
? repeat ? repeat
: BACKGROUND_DEFAULTS.background_repeat, : BACKGROUND_DEFAULTS.background_repeat,
); );
await writeSetting(
"background_effect",
(BACKGROUND_EFFECTS as readonly string[]).includes(effect)
? effect
: BACKGROUND_DEFAULTS.background_effect,
);
await writeSetting(
"background_overlay",
/^#[0-9a-fA-F]{3,8}$/.test(overlay) ? overlay : "",
);
await writeSetting(
"background_overlay_opacity",
/^\d{1,2}$/.test(overlayOpacity)
? String(Math.min(parseInt(overlayOpacity, 10), OVERLAY_OPACITY_MAX))
: "0",
);
siteSettings.reload(); siteSettings.reload();
revalidatePath("/", "layout"); revalidatePath("/", "layout");
revalidatePath(MANAGER_PATH); revalidatePath(MANAGER_PATH);
@@ -344,6 +373,29 @@ export async function getThemeManagerData() {
) )
? (backgroundValues.background_repeat as string) ? (backgroundValues.background_repeat as string)
: BACKGROUND_DEFAULTS.background_repeat, : BACKGROUND_DEFAULTS.background_repeat,
effect: (BACKGROUND_EFFECTS as readonly string[]).includes(
backgroundValues.background_effect ?? "",
)
? (backgroundValues.background_effect as string)
: BACKGROUND_DEFAULTS.background_effect,
overlay: /^#[0-9a-fA-F]{3,8}$/.test(
backgroundValues.background_overlay ?? "",
)
? (backgroundValues.background_overlay as string)
: "",
overlayOpacity: /^\d{1,2}$/.test(
backgroundValues.background_overlay_opacity ?? "",
)
? String(
Math.min(
parseInt(
backgroundValues.background_overlay_opacity as string,
10,
),
OVERLAY_OPACITY_MAX,
),
)
: "0",
}, },
}; };
} }
+107 -9
View File
File diff suppressed because it is too large. Load diff
+12 -10
View File
@@ -65,16 +65,16 @@ export default async function RegisterPage() {
<div <div
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
background: `url(/assets/images/background.png) center/cover no-repeat`, background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 10%, transparent), color-mix(in srgb, var(--color-accent) 10%, transparent))",
}} }}
/> />
<div <div
className="absolute inset-0" aria-hidden="true"
style={{ className="absolute inset-0 overflow-hidden"
background: >
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 88%, transparent) 0%, color-mix(in srgb, var(--color-surface) 35%, transparent) 100%)", <div className="brand-halo left-1/2 top-0 h-64 w-[460px] max-w-full -translate-x-1/2 -translate-y-1/2" />
}} </div>
/>
<div className="relative"> <div className="relative">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="shrink-0 animate-float"> <div className="shrink-0 animate-float">
@@ -135,7 +135,8 @@ export default async function RegisterPage() {
{recentUsers.map((u) => ( {recentUsers.map((u) => (
<div <div
key={u.username} key={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5" title={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5 hover:ring-1 hover:ring-[color-mix(in_srgb,var(--color-primary)_35%,transparent)]"
style={{ style={{
background: background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
@@ -163,7 +164,7 @@ export default async function RegisterPage() {
{latestUsers.length > 0 && ( {latestUsers.length > 0 && (
<SurfaceCard <SurfaceCard
title="Newest citizens" title={tpr("newestCitizens")}
icon={ICON_FRIENDS} icon={ICON_FRIENDS}
bodyClassName="p-4" bodyClassName="p-4"
className="card-glow" className="card-glow"
@@ -172,7 +173,8 @@ export default async function RegisterPage() {
{latestUsers.map((u) => ( {latestUsers.map((u) => (
<div <div
key={u.username} key={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5" title={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5 hover:ring-1 hover:ring-[color-mix(in_srgb,var(--color-primary)_35%,transparent)]"
style={{ style={{
background: background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
+142
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+31
View File
@@ -7,9 +7,37 @@ export const BACKGROUND_KEYS = [
"background_position", "background_position",
"background_size", "background_size",
"background_repeat", "background_repeat",
"background_effect",
"background_overlay",
"background_overlay_opacity",
] as const; ] as const;
export type BackgroundKey = (typeof BACKGROUND_KEYS)[number]; export type BackgroundKey = (typeof BACKGROUND_KEYS)[number];
/**
* Animated background effects rendered behind the public site. Purely
* decorative and CSS-driven (transform/opacity only) so they stay cheap on
* every device and respect `prefers-reduced-motion`.
*/
export const BACKGROUND_EFFECTS = [
"none",
"aurora",
"particles",
"aurora-particles",
] as const;
export type BackgroundEffect = (typeof BACKGROUND_EFFECTS)[number];
export const BACKGROUND_EFFECT_DEFAULT: BackgroundEffect = "none";
export const OVERLAY_PRESETS = [
{ label: "None", value: "" },
{ label: "Black", value: "#000000" },
{ label: "White", value: "#ffffff" },
{ label: "Navy", value: "#0b1220" },
{ label: "Slate", value: "#1e293b" },
] as const;
export const OVERLAY_OPACITY_MAX = 90;
export const BACKGROUND_POSITIONS = [ export const BACKGROUND_POSITIONS = [
"center center", "center center",
"top center", "top center",
@@ -36,6 +64,9 @@ export const BACKGROUND_DEFAULTS: Record<BackgroundKey, string> = {
background_position: "center center", background_position: "center center",
background_size: "cover", background_size: "cover",
background_repeat: "no-repeat", background_repeat: "no-repeat",
background_effect: BACKGROUND_EFFECT_DEFAULT,
background_overlay: "",
background_overlay_opacity: "0",
}; };
/** /**
+15 -1
View File
@@ -288,7 +288,11 @@
"sectionStatsLabel": "Live stats", "sectionStatsLabel": "Live stats",
"sectionStatsTitle": "A community that keeps growing", "sectionStatsTitle": "A community that keeps growing",
"sectionCommunityLabel": "Community", "sectionCommunityLabel": "Community",
"sectionCommunityTitle": "Make yourself at home" "sectionCommunityTitle": "Make yourself at home",
"joinCtaTitle": "Ready to make yourself at home?",
"joinCtaBody": "Create your avatar, grab some furni and meet new friends — your hotel adventure starts here.",
"ctaJoin": "Join the hotel",
"browseCommunity": "Browse the community"
}, },
"news": { "news": {
"title": "News", "title": "News",
@@ -997,6 +1001,7 @@
"alreadyHaveAccountLink": "Sign in", "alreadyHaveAccountLink": "Sign in",
"register": "Register", "register": "Register",
"whoIsOnline": "Who's online", "whoIsOnline": "Who's online",
"newestCitizens": "Newest citizens",
"usersOnline": "{count} online", "usersOnline": "{count} online",
"termsError": "You must accept the terms & rules to register.", "termsError": "You must accept the terms & rules to register.",
"usernameError": "Username is required.", "usernameError": "Username is required.",
@@ -4810,6 +4815,15 @@
"applyBackground": "Apply background", "applyBackground": "Apply background",
"uploadButton": "Upload image…", "uploadButton": "Upload image…",
"clearBackground": "Remove background", "clearBackground": "Remove background",
"backgroundEffectLabel": "Effect",
"backgroundEffectHint": "Animate the public background with a gentle, theme-matched layer (aurora uses your primary/accent colors).",
"backgroundEffect_none": "None (plain background)",
"backgroundEffect_aurora": "Aurora glow",
"backgroundEffect_particles": "Floating particles",
"backgroundEffect_aurora-particles": "Aurora + particles",
"backgroundEffectPreview": "Effect: {effect} — shown behind the public site.",
"backgroundOverlayLabel": "Overlay tint",
"backgroundOverlayOpacityLabel": "Overlay strength",
"notice": { "notice": {
"saved": "Theme saved.", "saved": "Theme saved.",
"applied": "Theme applied.", "applied": "Theme applied.",
+19 -5
View File
@@ -283,12 +283,16 @@
"featuresCommunityDesc": "Fai amicizia e chatta", "featuresCommunityDesc": "Fai amicizia e chatta",
"featuresGames": "Giochi", "featuresGames": "Giochi",
"featuresGamesDesc": "Competi e divertiti", "featuresGamesDesc": "Competi e divertiti",
"sectionFeaturesLabel": "Features", "sectionFeaturesLabel": "Funzionalità",
"sectionFeaturesTitle": "Everything a great hotel needs", "sectionFeaturesTitle": "Tutto ciò che un grande hotel merita",
"sectionStatsLabel": "Live stats", "sectionStatsLabel": "Statistiche live",
"sectionStatsTitle": "A community that keeps growing", "sectionStatsTitle": "Una community che continua a crescere",
"sectionCommunityLabel": "Community", "sectionCommunityLabel": "Community",
"sectionCommunityTitle": "Make yourself at home" "sectionCommunityTitle": "Fatti sentire a casa",
"joinCtaTitle": "Pronto a sentirti a casa?",
"joinCtaBody": "Crea il tuo avatar, prendi dei mobili e incontra nuovi amici: la tua avventura nell'hotel inizia qui.",
"ctaJoin": "Entra nell'hotel",
"browseCommunity": "Scopri la community"
}, },
"news": { "news": {
"title": "Notizie", "title": "Notizie",
@@ -976,6 +980,7 @@
"alreadyHaveAccountPre": "Hai già un account?", "alreadyHaveAccountPre": "Hai già un account?",
"alreadyHaveAccountLink": "Accedi", "alreadyHaveAccountLink": "Accedi",
"whoIsOnline": "Chi è online", "whoIsOnline": "Chi è online",
"newestCitizens": "Nuovi cittadini",
"usersOnline": "{count} online", "usersOnline": "{count} online",
"accepted": "Accettato e confermato", "accepted": "Accettato e confermato",
"showPassword": "Mostra", "showPassword": "Mostra",
@@ -4809,6 +4814,15 @@
"applyBackground": "Applica sfondo", "applyBackground": "Applica sfondo",
"uploadButton": "Carica immagine…", "uploadButton": "Carica immagine…",
"clearBackground": "Rimuovi sfondo", "clearBackground": "Rimuovi sfondo",
"backgroundEffectLabel": "Effetto",
"backgroundEffectHint": "Anima lo sfondo pubblico con un livello delicato e abbinato al tema (l'aurora usa i colori primario/accento).",
"backgroundEffect_none": "Nessuno (sfondo semplice)",
"backgroundEffect_aurora": "Bagliore aurora",
"backgroundEffect_particles": "Particelle fluttuanti",
"backgroundEffect_aurora-particles": "Aurora + particelle",
"backgroundEffectPreview": "Effetto: {effect} — mostrato dietro il sito pubblico.",
"backgroundOverlayLabel": "Tinta di sovrapposizione",
"backgroundOverlayOpacityLabel": "Intensità tinta",
"notice": { "notice": {
"saved": "Tema salvato.", "saved": "Tema salvato.",
"applied": "Tema applicato.", "applied": "Tema applicato.",