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:
1 parent
5923b736fa
commit
d131124515
9 files changed
+573
-25
No files matched your search
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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)",
|
||||||
|
|||||||
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
@@ -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
@@ -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
@@ -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.",
|
||||||
|
|||||||
Reference in new issue
Block a user