Die sechs noch offenen Typen haben kein fremdes Ziel, auf das man weiterleiten koennte — sie brauchen eine Seite. Dafuer bekommt der Typ-Vertrag `hasLandingPage`: /r/[slug] schickt solche Codes auf /s/[slug], statt `redirectTarget` zu befragen. Die oeffentlichen Seiten duerfen die Navigation der Anwendung nicht zeigen — wer einen Code scannt, hat mit „Verwaltung" nichts zu tun. Deshalb liegt das App-Chrome jetzt in der Routengruppe (app); das Wurzel-Layout traegt nur noch html und body. Die URLs aendern sich dadurch nicht.
35 lines
1.2 KiB
TypeScript
35 lines
1.2 KiB
TypeScript
import { Icon } from "@/components/icons";
|
|
import { LandingShell } from "@/components/landing/shell";
|
|
import type { LandingProps } from "@/components/landing/types";
|
|
import { linksDataSchema } from "@/lib/qr/types/links";
|
|
|
|
export function LinksLanding({ data }: LandingProps) {
|
|
const { title, description, links } = linksDataSchema.parse(data);
|
|
|
|
return (
|
|
<LandingShell title={title} subtitle={description || null}>
|
|
<ul className="space-y-3">
|
|
{links.map((link, index) => (
|
|
<li key={`${index}-${link.url}`}>
|
|
<a
|
|
href={link.url}
|
|
// Fremde Ziele: kein Zugriff auf dieses Fenster.
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-5 py-4 text-slate-900 shadow-sm transition-colors hover:border-indigo-300 hover:bg-indigo-50/60"
|
|
>
|
|
<span className="min-w-0 flex-1 font-medium break-words">
|
|
{link.label}
|
|
</span>
|
|
<Icon
|
|
name="external"
|
|
className="size-4 shrink-0 text-slate-400"
|
|
aria-hidden="true"
|
|
/>
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</LandingShell>
|
|
);
|
|
}
|