feat: organizers, meetups UI, Plausible analytics, and migration tooling
- 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
This commit is contained in:
75
frontend/components/public/ContactChannelGrid.tsx
Normal file
75
frontend/components/public/ContactChannelGrid.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Send, Zap, ExternalLink } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
const CHANNELS = [
|
||||
{
|
||||
key: "telegram_link" as const,
|
||||
title: "Telegram",
|
||||
description:
|
||||
"Join our Telegram group for quick questions and community chat.",
|
||||
Icon: Send,
|
||||
},
|
||||
{
|
||||
key: "nostr_link" as const,
|
||||
title: "Nostr",
|
||||
description: "Follow us on Nostr for censorship-resistant communication.",
|
||||
Icon: Zap,
|
||||
},
|
||||
{
|
||||
key: "x_link" as const,
|
||||
title: "X (Twitter)",
|
||||
description: "Follow us on X for announcements and updates.",
|
||||
Icon: ExternalLink,
|
||||
},
|
||||
];
|
||||
|
||||
export function ContactChannelGrid() {
|
||||
const [settings, setSettings] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.getPublicSettings()
|
||||
.then((data) => setSettings(data))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{CHANNELS.map(({ key, title, description, Icon }) => {
|
||||
const href = settings[key] || "#";
|
||||
const isExternal = href.startsWith("http");
|
||||
return (
|
||||
<a
|
||||
key={key}
|
||||
href={href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
className="bg-surface-container-low p-8 rounded-xl hover:bg-surface-container transition-colors group"
|
||||
>
|
||||
<Icon size={28} className="text-primary mb-4" />
|
||||
<h2 className="text-xl font-bold mb-2">{title}</h2>
|
||||
<p className="text-on-surface-variant text-sm">{description}</p>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="bg-surface-container-low p-8 rounded-xl">
|
||||
<h2 className="text-xl font-bold mb-2">Meetups</h2>
|
||||
<p className="text-on-surface-variant text-sm mb-4">
|
||||
The best way to connect is in person. Come to our monthly meetup in
|
||||
Brussels.
|
||||
</p>
|
||||
<Link
|
||||
href="/#meetup"
|
||||
className="text-primary font-bold text-sm hover:underline"
|
||||
>
|
||||
See next meetup →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user