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 {
BACKGROUND_DEFAULTS,
BACKGROUND_EFFECTS,
BACKGROUND_POSITIONS,
BACKGROUND_REPEATS,
BACKGROUND_SIZES,
isSafeBackgroundUrl,
OVERLAY_OPACITY_MAX,
} from "@/lib/theme-bg";
import {
type CustomTheme,
@@ -187,6 +189,17 @@ export async function saveBackground(formData: FormData): Promise<void> {
const repeat = String(formData.get("background_repeat") ?? "")
.normalize("NFC")
.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);
@@ -210,6 +223,22 @@ export async function saveBackground(formData: FormData): Promise<void> {
? 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();
revalidatePath("/", "layout");
revalidatePath(MANAGER_PATH);
@@ -344,6 +373,29 @@ export async function getThemeManagerData() {
)
? (backgroundValues.background_repeat as string)
: 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
className="absolute inset-0"
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
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 88%, transparent) 0%, color-mix(in srgb, var(--color-surface) 35%, transparent) 100%)",
}}
/>
aria-hidden="true"
className="absolute inset-0 overflow-hidden"
>
<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="flex items-center gap-4">
<div className="shrink-0 animate-float">
@@ -135,7 +135,8 @@ export default async function RegisterPage() {
{recentUsers.map((u) => (
<div
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={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
@@ -163,7 +164,7 @@ export default async function RegisterPage() {
{latestUsers.length > 0 && (
<SurfaceCard
title="Newest citizens"
title={tpr("newestCitizens")}
icon={ICON_FRIENDS}
bodyClassName="p-4"
className="card-glow"
@@ -172,7 +173,8 @@ export default async function RegisterPage() {
{latestUsers.map((u) => (
<div
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={{
background:
"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_size",
"background_repeat",
"background_effect",
"background_overlay",
"background_overlay_opacity",
] as const;
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 = [
"center center",
"top center",
@@ -36,6 +64,9 @@ export const BACKGROUND_DEFAULTS: Record<BackgroundKey, string> = {
background_position: "center center",
background_size: "cover",
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",
"sectionStatsTitle": "A community that keeps growing",
"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": {
"title": "News",
@@ -997,6 +1001,7 @@
"alreadyHaveAccountLink": "Sign in",
"register": "Register",
"whoIsOnline": "Who's online",
"newestCitizens": "Newest citizens",
"usersOnline": "{count} online",
"termsError": "You must accept the terms & rules to register.",
"usernameError": "Username is required.",
@@ -4810,6 +4815,15 @@
"applyBackground": "Apply background",
"uploadButton": "Upload image…",
"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": {
"saved": "Theme saved.",
"applied": "Theme applied.",
+19 -5
View File
@@ -283,12 +283,16 @@
"featuresCommunityDesc": "Fai amicizia e chatta",
"featuresGames": "Giochi",
"featuresGamesDesc": "Competi e divertiti",
"sectionFeaturesLabel": "Features",
"sectionFeaturesTitle": "Everything a great hotel needs",
"sectionStatsLabel": "Live stats",
"sectionStatsTitle": "A community that keeps growing",
"sectionFeaturesLabel": "Funzionalità",
"sectionFeaturesTitle": "Tutto ciò che un grande hotel merita",
"sectionStatsLabel": "Statistiche live",
"sectionStatsTitle": "Una community che continua a crescere",
"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": {
"title": "Notizie",
@@ -976,6 +980,7 @@
"alreadyHaveAccountPre": "Hai già un account?",
"alreadyHaveAccountLink": "Accedi",
"whoIsOnline": "Chi è online",
"newestCitizens": "Nuovi cittadini",
"usersOnline": "{count} online",
"accepted": "Accettato e confermato",
"showPassword": "Mostra",
@@ -4809,6 +4814,15 @@
"applyBackground": "Applica sfondo",
"uploadButton": "Carica immagine…",
"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": {
"saved": "Tema salvato.",
"applied": "Tema applicato.",