- Add organizer model/API, admin and public organizer pages, meetup cards - Refresh events/home/contact; add calendar dialog and carousel components - Optional Plausible via NEXT_PUBLIC_PLAUSIBLE_* env vars in root layout - Prisma migration, seed updates, baseline-and-migrate script Made-with: Cursor
127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { CalendarPlus, Copy, Check, Download, ExternalLink, X } from "lucide-react";
|
|
|
|
const siteUrl =
|
|
typeof window !== "undefined"
|
|
? window.location.origin
|
|
: process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org";
|
|
|
|
function Dialog({ onClose }: { onClose: () => void }) {
|
|
const icsUrl = `${siteUrl}/calendar.ics`;
|
|
const webcalUrl = icsUrl.replace(/^https?:\/\//, "webcal://");
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [onClose]);
|
|
|
|
const handleCopy = async () => {
|
|
await navigator.clipboard.writeText(icsUrl);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
className="relative bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-md p-6 shadow-2xl"
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 text-on-surface-variant/50 hover:text-on-surface transition-colors"
|
|
aria-label="Close"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
|
|
<div className="flex items-center gap-2.5 mb-4">
|
|
<div className="bg-primary/10 text-primary rounded-lg p-2">
|
|
<CalendarPlus size={20} />
|
|
</div>
|
|
<h2 className="text-lg font-bold">Add to Calendar</h2>
|
|
</div>
|
|
|
|
<p className="text-sm text-on-surface-variant leading-relaxed mb-6">
|
|
Subscribe to this feed to get all public Belgian Bitcoin Embassy
|
|
meetups in your calendar. New events are added automatically.
|
|
</p>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="block text-xs font-semibold text-on-surface-variant/60 mb-1.5">
|
|
Calendar URL
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
readOnly
|
|
value={icsUrl}
|
|
className="flex-1 min-w-0 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-xs text-on-surface select-all focus:outline-none focus:border-primary/50"
|
|
onFocus={(e) => e.currentTarget.select()}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
className="shrink-0 flex items-center gap-1.5 bg-zinc-800 border border-zinc-700 hover:border-primary/50 text-on-surface-variant hover:text-primary rounded-lg px-3 py-2 text-xs font-medium transition-all"
|
|
>
|
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
{copied ? "Copied" : "Copy"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 pt-2">
|
|
<a
|
|
href={webcalUrl}
|
|
className="flex items-center justify-center gap-1.5 bg-primary text-on-primary rounded-lg px-3 py-2.5 text-xs font-semibold hover:brightness-110 transition-all"
|
|
>
|
|
<ExternalLink size={14} />
|
|
Open in Calendar
|
|
</a>
|
|
<a
|
|
href="/calendar.ics"
|
|
download="bbe-events.ics"
|
|
className="flex items-center justify-center gap-1.5 bg-zinc-800 border border-zinc-700 hover:border-primary/50 text-on-surface-variant hover:text-primary rounded-lg px-3 py-2.5 text-xs font-semibold transition-all"
|
|
>
|
|
<Download size={14} />
|
|
Download .ics
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AddToCalendarButton({ className }: { className?: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
const close = useCallback(() => setOpen(false), []);
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(true)}
|
|
title="Subscribe to get all future meetups automatically"
|
|
className={
|
|
className ??
|
|
"flex items-center gap-1.5 text-xs text-on-surface-variant/60 hover:text-primary border border-zinc-700 hover:border-primary/50 rounded-lg px-3 py-1.5 transition-all"
|
|
}
|
|
>
|
|
<CalendarPlus size={14} />
|
|
Add to Calendar
|
|
</button>
|
|
{open && <Dialog onClose={close} />}
|
|
</>
|
|
);
|
|
}
|