Add editable localized marquee settings
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MarqueeDirection = "left" | "right";
|
||||
|
||||
export type MarqueeRow = {
|
||||
items: string[];
|
||||
direction: MarqueeDirection;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
type StackedMarqueeSectionProps = {
|
||||
className?: string;
|
||||
rows?: MarqueeRow[];
|
||||
};
|
||||
|
||||
const DEFAULT_ROWS: MarqueeRow[] = [
|
||||
{
|
||||
direction: "left",
|
||||
duration: 126,
|
||||
items: [
|
||||
"Next.js",
|
||||
"React",
|
||||
"WordPress",
|
||||
"Docker",
|
||||
"Technical SEO",
|
||||
"Tailwind CSS",
|
||||
"Google Analytics",
|
||||
"Framer Motion",
|
||||
"Cloudflare",
|
||||
"Performance",
|
||||
],
|
||||
},
|
||||
{
|
||||
direction: "right",
|
||||
duration: 220,
|
||||
items: [
|
||||
"TypeScript",
|
||||
"Node.js",
|
||||
"WooCommerce",
|
||||
"Schema Markup",
|
||||
"DevOps",
|
||||
"Responsive Design",
|
||||
"Core Web Vitals",
|
||||
"Plausible Analytics",
|
||||
"Design Systems",
|
||||
"Server Infrastructure",
|
||||
],
|
||||
},
|
||||
{
|
||||
direction: "left",
|
||||
duration: 168,
|
||||
items: [
|
||||
"JavaScript",
|
||||
"REST APIs",
|
||||
"Git",
|
||||
"Linux",
|
||||
"Search Console",
|
||||
"Conversion Tracking",
|
||||
"UI Engineering",
|
||||
"Accessibility",
|
||||
"Component Architecture",
|
||||
"Web Performance",
|
||||
],
|
||||
},
|
||||
{
|
||||
direction: "right",
|
||||
duration: 144,
|
||||
items: [
|
||||
"Frontend Strategy",
|
||||
"CMS Integration",
|
||||
"Motion Design",
|
||||
"Content Architecture",
|
||||
"E-Commerce",
|
||||
"SEO Audits",
|
||||
"Deployments",
|
||||
"Scalable UI",
|
||||
"Analytics Setup",
|
||||
"Product Pages",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function MarqueeRowTrack({ items, direction, duration = 24 }: MarqueeRow) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden leading-[0.92]">
|
||||
<motion.div
|
||||
className="flex w-max min-w-full flex-nowrap"
|
||||
animate={
|
||||
shouldReduceMotion
|
||||
? { 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) => (
|
||||
<div
|
||||
key={`${direction}-${copyIndex}`}
|
||||
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}
|
||||
>
|
||||
{items.map((item, itemIndex) => (
|
||||
<div
|
||||
key={`${copyIndex}-${item}`}
|
||||
className="flex shrink-0 items-center gap-3 sm:gap-4 lg:gap-5"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 whitespace-nowrap text-[clamp(2.05rem,6vw,4.8rem)] uppercase leading-[0.92] tracking-[-0.01em] text-[hsl(var(--surface-3))] dark:text-[hsl(var(--surface-3))] [text-shadow:none]",
|
||||
itemIndex % 2 === 0 ? "font-black" : "font-light",
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
<span
|
||||
className="text-[clamp(1.85rem,5vw,4rem)] font-light leading-[0.92] tracking-[-0.01em] text-[hsl(var(--surface-3))] dark:text-[hsl(var(--surface-3))] [text-shadow:none]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StackedMarqueeSection({
|
||||
className,
|
||||
rows = DEFAULT_ROWS,
|
||||
}: StackedMarqueeSectionProps) {
|
||||
return (
|
||||
<section
|
||||
aria-label="Technology marquee"
|
||||
className={cn(
|
||||
"relative isolate overflow-hidden bg-transparent py-3 sm:py-4 lg:py-5",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative flex flex-col gap-0 sm:gap-0 lg:gap-0">
|
||||
{rows.map((row, index) => (
|
||||
<div key={`${row.direction}-${index}`} className={index === 0 ? "" : "-mt-0.5 sm:-mt-1"}>
|
||||
<MarqueeRowTrack
|
||||
items={row.items}
|
||||
direction={row.direction}
|
||||
duration={row.duration}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { AppCard } from "@/components/ui/app-card";
|
||||
import { CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { MarqueeSettings } from "@/lib/marquee-settings";
|
||||
|
||||
type MarqueeSettingsFormProps = {
|
||||
action: (formData: FormData) => Promise<void>;
|
||||
initialSettings: MarqueeSettings;
|
||||
};
|
||||
|
||||
const localeMeta = [
|
||||
{ key: "de", label: "Deutsch" },
|
||||
{ key: "en", label: "English" },
|
||||
{ key: "ar", label: "Arabic" },
|
||||
] as const;
|
||||
|
||||
const rowMeta = [
|
||||
{ key: "row1", label: "Row 1" },
|
||||
{ key: "row2", label: "Row 2" },
|
||||
{ key: "row3", label: "Row 3" },
|
||||
{ key: "row4", label: "Row 4" },
|
||||
] as const;
|
||||
|
||||
export function MarqueeSettingsForm({
|
||||
action,
|
||||
initialSettings,
|
||||
}: MarqueeSettingsFormProps) {
|
||||
return (
|
||||
<form id="marquee-settings-form" action={action} className="space-y-6">
|
||||
<AppCard>
|
||||
<CardHeader>
|
||||
<CardTitle>Marquee Rows</CardTitle>
|
||||
<CardDescription>Ein Feld pro Zeile. Ein Begriff pro Zeile oder mit Komma getrennt.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 xl:grid-cols-3">
|
||||
{localeMeta.map((locale) => (
|
||||
<div key={locale.key} className="space-y-4 rounded-nested border border-border/70 bg-background/70 p-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground">{locale.label}</h3>
|
||||
<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>
|
||||
))}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ShieldAlert,
|
||||
SwatchBook,
|
||||
Tags,
|
||||
Type,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -35,6 +36,7 @@ type RootDashboardCopy = {
|
||||
portfolio: string;
|
||||
media: string;
|
||||
siteSettings: string;
|
||||
marquee?: string;
|
||||
smtp?: string;
|
||||
contactProtection?: string;
|
||||
logout: string;
|
||||
@@ -43,7 +45,7 @@ type RootDashboardCopy = {
|
||||
|
||||
type RootDashboardShellProps = {
|
||||
copy: RootDashboardCopy;
|
||||
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp";
|
||||
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee";
|
||||
smtpChild?: "settings" | "contact-protection";
|
||||
portfolioChild?: "overview" | "projects" | "new-project" | "categories";
|
||||
logoutAction: () => Promise<void>;
|
||||
@@ -92,9 +94,11 @@ export async function RootDashboardShell({
|
||||
? SwatchBook
|
||||
: active === "site-settings"
|
||||
? Globe2
|
||||
: active === "marquee"
|
||||
? Type
|
||||
: active === "smtp"
|
||||
? ShieldAlert
|
||||
: active === "media"
|
||||
: active === "media"
|
||||
? ImageIcon
|
||||
: portfolioChild === "categories"
|
||||
? Tags
|
||||
|
||||
Reference in New Issue
Block a user