Files
Calendar_Frontend/components/landing/CopyLinkCard.tsx
PaddyandClaude Opus 5.5 0cda40f85e Rewrite the calendar frontend in Next.js
Replaces the Angular 18 app with Next.js 16 / React 19 / Tailwind 4 / shadcn,
the same stack and design system as nachklang-feedback, planned in
docs/rewrite-plan.md.

- Admin: all calendars in one mobile-friendly list (month groups, calendar
  chips, Kommend/Vergangen/Alle, search, sort, filters in the URL), an edit
  sheet for create/edit/duplicate/delete, publish confirmation, and an
  "Abonnieren" dialog with subscription links for every calendar.
- Public landing page: subscribe links from env instead of hardcoded, plus
  the next public events, server-rendered and revalidated every 5 minutes.
- All times handled explicitly as Europe/Berlin; whole-day events stored as
  00:00-23:59. Fixes carried over from the Angular app: the move dialog
  leaking a cancelled calendar change, saves that looked saved when they
  failed, in-place Date mutation, and duplicated calendar-id mappings.
- Same shared-session auth as before (proxy.ts gate, /admin/me, 401 vs 403).
- 112 Vitest tests over lib/; docs in docs/ (architecture, features,
  gotchas, API wish list, deployment).

Needs the API branch feature/calendar-ical-escaping-subscriptions deployed
first (GET /calendar/events/subscriptions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:15:59 +02:00

46 lines
1.7 KiB
TypeScript

"use client";
import { CheckIcon, LinkIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
/**
* The one interactive subscribe option: copies the iCal URL for apps that
* want it pasted (Outlook and friends). Takes the shared card classes from the
* page so the three options stay identical.
*/
export function CopyLinkCard({ url, className }: { url: string; className: string }) {
const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
const copy = async () => {
try {
await navigator.clipboard.writeText(url);
setState("copied");
} catch {
// No clipboard access (older browser, insecure context): show the URL instead of failing silently.
setState("failed");
}
setTimeout(() => setState("idle"), 2500);
};
return (
<button type="button" onClick={copy} className={className} aria-live="polite">
<span
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-lg transition-colors",
state === "copied" ? "bg-emerald-50 text-emerald-600" : "bg-primary/10 text-primary",
)}
>
{state === "copied" ? <CheckIcon aria-hidden className="size-5" /> : <LinkIcon aria-hidden className="size-5" />}
</span>
<span className="flex min-w-0 flex-col gap-0.5">
<span className="font-medium">{state === "copied" ? "Link kopiert" : "iCal-Link kopieren"}</span>
{/* break-all only for the URL (it has no spaces to wrap at); on words it splits mid-word. */}
<span className={cn("text-sm text-muted-foreground", state === "failed" && "break-all")}>
{state === "failed" ? url : "Outlook und andere Apps"}
</span>
</span>
</button>
);
}