Fix marquee behavior and unify marquee settings

This commit is contained in:
MOH
2026-03-10 05:23:27 +01:00
parent 701c1b2a6c
commit e219295327
6 changed files with 134 additions and 91 deletions
+21 -31
View File
@@ -37,45 +37,35 @@ export async function saveMarqueeSettingsAction(formData: FormData) {
ensureAdmin(); ensureAdmin();
try { try {
const germanSettings = {
row1: String(formData.get("row1-de") ?? "").trim(),
row2: String(formData.get("row2-de") ?? "").trim(),
row3: String(formData.get("row3-de") ?? "").trim(),
row4: String(formData.get("row4-de") ?? "").trim(),
};
const settings = { const settings = {
locales: { locales: {
ar: { ar: { ...germanSettings },
row1: String(formData.get("row1-ar") ?? "").trim(), en: { ...germanSettings },
row2: String(formData.get("row2-ar") ?? "").trim(), de: germanSettings,
row3: String(formData.get("row3-ar") ?? "").trim(),
row4: String(formData.get("row4-ar") ?? "").trim(),
},
en: {
row1: String(formData.get("row1-en") ?? "").trim(),
row2: String(formData.get("row2-en") ?? "").trim(),
row3: String(formData.get("row3-en") ?? "").trim(),
row4: String(formData.get("row4-en") ?? "").trim(),
},
de: {
row1: String(formData.get("row1-de") ?? "").trim(),
row2: String(formData.get("row2-de") ?? "").trim(),
row3: String(formData.get("row3-de") ?? "").trim(),
row4: String(formData.get("row4-de") ?? "").trim(),
},
}, },
}; };
for (const locale of routing.locales) { if (!germanSettings.row1) {
if (!settings.locales[locale].row1) { throw new Error("Row 1 fuer de ist erforderlich.");
throw new Error(`Row 1 fuer ${locale} ist erforderlich.`); }
}
if (!settings.locales[locale].row2) { if (!germanSettings.row2) {
throw new Error(`Row 2 fuer ${locale} ist erforderlich.`); throw new Error("Row 2 fuer de ist erforderlich.");
} }
if (!settings.locales[locale].row3) { if (!germanSettings.row3) {
throw new Error(`Row 3 fuer ${locale} ist erforderlich.`); throw new Error("Row 3 fuer de ist erforderlich.");
} }
if (!settings.locales[locale].row4) { if (!germanSettings.row4) {
throw new Error(`Row 4 fuer ${locale} ist erforderlich.`); throw new Error("Row 4 fuer de ist erforderlich.");
}
} }
await updateMarqueeSettings(settings); await updateMarqueeSettings(settings);
-3
View File
@@ -2,7 +2,6 @@ import Link from "next/link";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { Container } from "@/components/layout/container"; import { Container } from "@/components/layout/container";
import { RetroLedMarquee } from "@/components/layout/retro-led-marquee";
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
const navItems = [ const navItems = [
@@ -21,7 +20,6 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) {
const locale = useLocale(); const locale = useLocale();
const tNav = useTranslations("navigation"); const tNav = useTranslations("navigation");
const tFooter = useTranslations("footer"); const tFooter = useTranslations("footer");
const ledText = "MADE IN GERMANY BY MOH";
return ( return (
<footer className="border-t border-border bg-background"> <footer className="border-t border-border bg-background">
@@ -46,7 +44,6 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) {
<p className="text-xs text-muted-foreground/80"> <p className="text-xs text-muted-foreground/80">
{tFooter("copyright", { year: new Date().getFullYear() })} {tFooter("copyright", { year: new Date().getFullYear() })}
</p> </p>
<RetroLedMarquee text={ledText} />
</div> </div>
</Container> </Container>
</footer> </footer>
+70 -22
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { motion, useReducedMotion } from "framer-motion"; import { useLayoutEffect, useRef, useState } from "react";
import { motion, useAnimationFrame, useMotionValue, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -86,33 +87,80 @@ const DEFAULT_ROWS: MarqueeRow[] = [
function MarqueeRowTrack({ items, direction, duration = 24 }: MarqueeRow) { function MarqueeRowTrack({ items, direction, duration = 24 }: MarqueeRow) {
const shouldReduceMotion = useReducedMotion(); const shouldReduceMotion = useReducedMotion();
const viewportRef = useRef<HTMLDivElement>(null);
const segmentRef = useRef<HTMLDivElement>(null);
const [segmentWidth, setSegmentWidth] = useState(0);
const [copyCount, setCopyCount] = useState(3);
const x = useMotionValue(0);
const isReady = shouldReduceMotion || segmentWidth > 0;
useLayoutEffect(() => {
const viewport = viewportRef.current;
const segment = segmentRef.current;
if (!viewport || !segment) {
return;
}
const measure = () => {
const nextSegmentWidth = segment.scrollWidth;
const viewportWidth = viewport.clientWidth;
if (!nextSegmentWidth || !viewportWidth) {
return;
}
setSegmentWidth(nextSegmentWidth);
setCopyCount(Math.max(3, Math.ceil(viewportWidth / nextSegmentWidth) + 2));
x.set(direction === "right" ? -nextSegmentWidth : 0);
};
measure();
const observer = new ResizeObserver(() => {
measure();
});
observer.observe(viewport);
observer.observe(segment);
return () => {
observer.disconnect();
};
}, [direction, items, x]);
useAnimationFrame((_, delta) => {
if (shouldReduceMotion || segmentWidth === 0) {
return;
}
const distancePerMs = segmentWidth / (duration * 1000);
const offset = distancePerMs * delta;
const current = x.get();
if (direction === "right") {
const next = current + offset;
x.set(next >= 0 ? next - segmentWidth : next);
return;
}
const next = current - offset;
x.set(next <= -segmentWidth ? next + segmentWidth : next);
});
return ( return (
<div className="overflow-hidden leading-[0.92]"> <div ref={viewportRef} className="overflow-hidden leading-[0.92]">
<motion.div <motion.div
className="flex w-max min-w-full flex-nowrap" dir="ltr"
animate={ className={cn("flex w-max flex-nowrap", !isReady && "opacity-0")}
shouldReduceMotion style={{ x }}
? { x: 0 }
: {
x: direction === "left" ? ["0%", "-50%"] : ["-50%", "0%"],
}
}
transition={
shouldReduceMotion
? undefined
: {
duration,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
}
}
> >
{Array.from({ length: 2 }).map((_, copyIndex) => ( {Array.from({ length: copyCount }).map((_, copyIndex) => (
<div <div
key={`${direction}-${copyIndex}`} key={`${direction}-${copyIndex}`}
ref={copyIndex === 0 ? segmentRef : undefined}
className="flex shrink-0 items-center gap-3 pr-3 sm:gap-4 sm:pr-4 lg:gap-5 lg:pr-5" className="flex shrink-0 items-center gap-3 pr-3 sm:gap-4 sm:pr-4 lg:gap-5 lg:pr-5"
aria-hidden={copyIndex === 1} aria-hidden={copyIndex > 0}
> >
{items.map((item, itemIndex) => ( {items.map((item, itemIndex) => (
<div <div
@@ -156,7 +204,7 @@ export function StackedMarqueeSection({
> >
<div className="relative flex flex-col gap-0 sm:gap-0 lg:gap-0"> <div className="relative flex flex-col gap-0 sm:gap-0 lg:gap-0">
{rows.map((row, index) => ( {rows.map((row, index) => (
<div key={`${row.direction}-${index}`} className={index === 0 ? "" : "-mt-0.5 sm:-mt-1"}> <div key={`${row.direction}-${index}`}>
<MarqueeRowTrack <MarqueeRowTrack
items={row.items} items={row.items}
direction={row.direction} direction={row.direction}
+21 -29
View File
@@ -9,12 +9,6 @@ type MarqueeSettingsFormProps = {
initialSettings: MarqueeSettings; initialSettings: MarqueeSettings;
}; };
const localeMeta = [
{ key: "de", label: "Deutsch" },
{ key: "en", label: "English" },
{ key: "ar", label: "Arabic" },
] as const;
const rowMeta = [ const rowMeta = [
{ key: "row1", label: "Row 1" }, { key: "row1", label: "Row 1" },
{ key: "row2", label: "Row 2" }, { key: "row2", label: "Row 2" },
@@ -31,31 +25,29 @@ export function MarqueeSettingsForm({
<AppCard> <AppCard>
<CardHeader> <CardHeader>
<CardTitle>Marquee Rows</CardTitle> <CardTitle>Marquee Rows</CardTitle>
<CardDescription>Ein Feld pro Zeile. Ein Begriff pro Zeile oder mit Komma getrennt.</CardDescription> <CardDescription>Nur Deutsch bearbeiten. Die Werte werden fuer alle Sprachen uebernommen.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid gap-6 xl:grid-cols-3"> <CardContent>
{localeMeta.map((locale) => ( <div className="space-y-4 rounded-nested border border-border/70 bg-background/70 p-4">
<div key={locale.key} className="space-y-4 rounded-nested border border-border/70 bg-background/70 p-4"> <div>
<div> <h3 className="text-sm font-semibold text-foreground">Deutsch</h3>
<h3 className="text-sm font-semibold text-foreground">{locale.label}</h3> <p className="text-xs text-muted-foreground">Vier Marquee Zeilen. Diese Werte gelten fuer alle Sprachen.</p>
<p className="text-xs text-muted-foreground">Vier Marquee Zeilen fuer diese Sprache.</p>
</div>
<div className="space-y-4">
{rowMeta.map((row) => (
<div key={`${locale.key}-${row.key}`} className="space-y-2">
<Label htmlFor={`${row.key}-${locale.key}`}>{row.label}</Label>
<Textarea
id={`${row.key}-${locale.key}`}
name={`${row.key}-${locale.key}`}
defaultValue={initialSettings.locales[locale.key][row.key]}
className="min-h-[180px] resize-y"
/>
</div>
))}
</div>
</div> </div>
))}
<div className="space-y-4">
{rowMeta.map((row) => (
<div key={`de-${row.key}`} className="space-y-2">
<Label htmlFor={`${row.key}-de`}>{row.label}</Label>
<Textarea
id={`${row.key}-de`}
name={`${row.key}-de`}
defaultValue={initialSettings.locales.de[row.key]}
className="min-h-[180px] resize-y"
/>
</div>
))}
</div>
</div>
</CardContent> </CardContent>
</AppCard> </AppCard>
</form> </form>
+6 -2
View File
@@ -39,6 +39,7 @@ export {
buildDefaultMarqueeSettings, buildDefaultMarqueeSettings,
parseMarqueeSettingsValue, parseMarqueeSettingsValue,
splitMarqueeRowItems, splitMarqueeRowItems,
syncMarqueeSettingsToGermanSource,
type MarqueeLocaleSettings, type MarqueeLocaleSettings,
type MarqueeSettings, type MarqueeSettings,
} from "./marquee-settings"; } from "./marquee-settings";
@@ -80,6 +81,7 @@ import {
MARQUEE_SETTINGS_KEY, MARQUEE_SETTINGS_KEY,
buildDefaultMarqueeSettings, buildDefaultMarqueeSettings,
parseMarqueeSettingsValue, parseMarqueeSettingsValue,
syncMarqueeSettingsToGermanSource,
type MarqueeSettings, type MarqueeSettings,
} from "./marquee-settings"; } from "./marquee-settings";
@@ -231,14 +233,16 @@ export async function getMarqueeSettings(): Promise<MarqueeSettings> {
} }
export async function updateMarqueeSettings(settings: MarqueeSettings): Promise<void> { export async function updateMarqueeSettings(settings: MarqueeSettings): Promise<void> {
const normalizedSettings = syncMarqueeSettingsToGermanSource(settings);
await prisma.appConfig.upsert({ await prisma.appConfig.upsert({
where: { key: MARQUEE_SETTINGS_KEY }, where: { key: MARQUEE_SETTINGS_KEY },
update: { update: {
value: JSON.stringify(settings), value: JSON.stringify(normalizedSettings),
}, },
create: { create: {
key: MARQUEE_SETTINGS_KEY, key: MARQUEE_SETTINGS_KEY,
value: JSON.stringify(settings), value: JSON.stringify(normalizedSettings),
}, },
}); });
} }
+16 -4
View File
@@ -97,26 +97,38 @@ function normalizeLocaleSettings(value: unknown, defaults: MarqueeLocaleSettings
}; };
} }
export function syncMarqueeSettingsToGermanSource(settings: MarqueeSettings): MarqueeSettings {
const source = settings.locales.de;
return {
locales: {
de: { ...source },
en: { ...source },
ar: { ...source },
},
};
}
export function parseMarqueeSettingsValue(rawValue?: string | null): MarqueeSettings { export function parseMarqueeSettingsValue(rawValue?: string | null): MarqueeSettings {
const defaults = buildDefaultMarqueeSettings(); const defaults = buildDefaultMarqueeSettings();
if (!rawValue) { if (!rawValue) {
return defaults; return syncMarqueeSettingsToGermanSource(defaults);
} }
try { try {
const parsed = JSON.parse(rawValue) as { locales?: Record<string, unknown> }; const parsed = JSON.parse(rawValue) as { locales?: Record<string, unknown> };
const locales = parsed && typeof parsed === "object" ? parsed.locales : undefined; const locales = parsed && typeof parsed === "object" ? parsed.locales : undefined;
return { return syncMarqueeSettingsToGermanSource({
locales: { locales: {
ar: normalizeLocaleSettings(locales?.ar, defaults.locales.ar), ar: normalizeLocaleSettings(locales?.ar, defaults.locales.ar),
en: normalizeLocaleSettings(locales?.en, defaults.locales.en), en: normalizeLocaleSettings(locales?.en, defaults.locales.en),
de: normalizeLocaleSettings(locales?.de, defaults.locales.de), de: normalizeLocaleSettings(locales?.de, defaults.locales.de),
}, },
}; });
} catch { } catch {
return defaults; return syncMarqueeSettingsToGermanSource(defaults);
} }
} }