"use client"; import { BoltIcon, CheckCircleIcon, FireIcon, GlobeAltIcon, MagnifyingGlassIcon, MapPinIcon, PlusIcon, TrophyIcon, UserGroupIcon, } from "@heroicons/react/24/outline"; import { useMemo, useState } from "react"; import { ClubActivityCategory, ClubScope, GermanState, clubCategories, defaultClubProfile, germanStates, todayKey, } from "@/lib/hellth-data"; import { trpc } from "@/lib/trpc"; type ClubForm = { title: string; category: ClubActivityCategory; scope: ClubScope; date: string; time: string; durationMinutes: string; points: string; note: string; }; type ClubProfileView = { userId: string; state: string; city?: string | null; latitude?: number | null; longitude?: number | null; privacyMode: "city" | "state"; }; const initialForm: ClubForm = { title: "", category: "walk", scope: "state", date: todayKey(), time: "18:00", durationMinutes: "60", points: "100", note: "", }; function formatDateTime(value: string) { return new Date(value).toLocaleString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", }); } function categoryLabel(category: string) { return clubCategories.find((item) => item.key === category)?.label ?? "Sonstiges"; } function categoryStyle(category: string) { if (category === "walk") return "bg-emerald-50 text-emerald-700"; if (category === "run") return "bg-orange-50 text-orange-700"; if (category === "mobility") return "bg-blue-50 text-blue-700"; if (category === "strength") return "bg-purple-50 text-purple-700"; if (category === "bike") return "bg-cyan-50 text-cyan-700"; return "bg-gray-100 text-gray-700"; } function scopeLabel(scope: ClubScope) { if (scope === "local") return "Lokal"; if (scope === "state") return "Bundesland"; return "Deutschlandweit"; } function distanceKm( profile: { latitude?: number | null; longitude?: number | null }, activity: { latitude?: number | null; longitude?: number | null }, ) { if ( profile.latitude === undefined || profile.latitude === null || profile.longitude === undefined || profile.longitude === null || activity.latitude === undefined || activity.latitude === null || activity.longitude === undefined || activity.longitude === null ) { return undefined; } const toRad = (value: number) => (value * Math.PI) / 180; const earthKm = 6371; const deltaLat = toRad(activity.latitude - profile.latitude); const deltaLon = toRad(activity.longitude - profile.longitude); const lat1 = toRad(profile.latitude); const lat2 = toRad(activity.latitude); const haversine = Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) ** 2; return Math.round(earthKm * 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine))); } function locationLabel(profile: { state: string; city?: string | null; privacyMode: string }, scope: ClubScope) { if (scope === "nationwide") return "Deutschlandweit"; if (scope === "state" || profile.privacyMode === "state") return profile.state; return profile.city?.trim() || profile.state; } export default function HellfireclubPage() { const utils = trpc.useUtils(); const [query, setQuery] = useState(""); const [category, setCategory] = useState("all"); const [scope, setScope] = useState("state"); const [form, setForm] = useState(initialForm); const [status, setStatus] = useState(null); const [locationStatus, setLocationStatus] = useState(null); const profileQuery = trpc.health.clubProfile.useQuery(); const activitiesQuery = trpc.health.clubActivities.useQuery({}); const pointDefaultsQuery = trpc.health.pointDefaults.useQuery(); const leaderboardQuery = trpc.health.clubLeaderboard.useQuery({ days: 90 }); const upsertProfileMutation = trpc.health.upsertClubProfile.useMutation({ onSuccess: () => utils.health.clubProfile.invalidate(), }); const createActivityMutation = trpc.health.createClubActivity.useMutation({ onSuccess: async () => { await utils.health.clubActivities.invalidate(); setForm(initialForm); setStatus("Aktivität erstellt. Andere User können jetzt passend zu deinem Gebiet mitmachen."); }, }); const joinActivityMutation = trpc.health.joinClubActivity.useMutation({ onSuccess: async () => { await utils.health.clubActivities.invalidate(); setStatus("Du bist dabei. Die Aktivität ist in deinem Hellfireclub vorgemerkt."); }, }); const completeActivityMutation = trpc.health.completeClubActivity.useMutation({ onSuccess: async () => { await Promise.all([ utils.health.clubActivities.invalidate(), utils.health.clubLeaderboard.invalidate(), utils.health.activityWeek.invalidate(), utils.health.overview.invalidate(), ]); setStatus("Punkte gutgeschrieben und als Aktivität gespeichert."); }, }); const rawProfile = profileQuery.data; const clubActivityDefaultPoints = pointDefaultsQuery.data?.clubActivityDefaultPoints ?? 100; const profile: ClubProfileView = { userId: rawProfile?.userId ?? "", state: rawProfile?.state ?? defaultClubProfile.state, city: rawProfile?.city ?? defaultClubProfile.city, latitude: rawProfile?.latitude == null ? null : Number(rawProfile.latitude), longitude: rawProfile?.longitude == null ? null : Number(rawProfile.longitude), privacyMode: (rawProfile?.privacyMode ?? defaultClubProfile.privacyMode) as "city" | "state", }; const activities = activitiesQuery.data ?? []; function updateProfile(patch: Partial) { const next = { ...profile, ...patch }; upsertProfileMutation.mutate({ state: next.state, city: next.city ?? null, latitude: next.latitude == null ? null : Number(next.latitude), longitude: next.longitude == null ? null : Number(next.longitude), privacyMode: next.privacyMode as "city" | "state", }); } function requestLocation() { if (!navigator.geolocation) { setLocationStatus("Dein Browser unterstützt keine Standortfreigabe."); return; } setLocationStatus("Standort wird abgefragt..."); navigator.geolocation.getCurrentPosition( (position) => { updateProfile({ latitude: position.coords.latitude, longitude: position.coords.longitude, }); setLocationStatus("Standort gespeichert. Sichtbar bleibt nur Stadt oder Bundesland."); }, () => setLocationStatus("Standort konnte nicht gelesen werden. Bundesland-Filter funktioniert weiterhin."), { enableHighAccuracy: false, maximumAge: 1000 * 60 * 30, timeout: 8000 }, ); } const filteredActivities = useMemo(() => { const needle = query.trim().toLowerCase(); return activities .filter((activity) => { const activityScope = (activity.scope ?? "nationwide") as ClubScope; if (scope === "nationwide" || activityScope === "nationwide") return true; if (scope === "state") return activity.state === profile.state; if (activity.state !== profile.state) return false; const distance = distanceKm(profile, activity); if (distance !== undefined) return distance <= 30; return Boolean(activity.city && profile.city && activity.city.toLowerCase() === profile.city.toLowerCase()); }) .filter((activity) => category === "all" || activity.category === category) .filter((activity) => { if (!needle) return true; return [activity.title, activity.hostName, activity.note, activity.locationLabel, activity.city, activity.state, categoryLabel(activity.category)] .filter(Boolean) .some((value) => value!.toLowerCase().includes(needle)); }) .sort((a, b) => { const distanceA = distanceKm(profile, a) ?? Number.MAX_SAFE_INTEGER; const distanceB = distanceKm(profile, b) ?? Number.MAX_SAFE_INTEGER; if (scope === "local" && distanceA !== distanceB) return distanceA - distanceB; return new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime(); }); }, [activities, category, profile, query, scope]); const leaderboard = leaderboardQuery.data ?? []; function createActivity() { if (!form.title.trim()) return; createActivityMutation.mutate({ title: form.title.trim(), category: form.category, scope: form.scope, state: profile.state, city: profile.city ?? null, latitude: profile.latitude == null ? null : Number(profile.latitude), longitude: profile.longitude == null ? null : Number(profile.longitude), locationLabel: locationLabel(profile, form.scope), startsAt: `${form.date}T${form.time}:00.000Z`, durationMinutes: Number(form.durationMinutes) || 30, points: Number(form.points) || clubActivityDefaultPoints, note: form.note.trim() || null, }); } return (

Hellfireclub

Gemeinsam getrennt aktiv

Lokale Challenges, Bundesland-Events und deutschlandweite Aktivitäten. Jetzt persistent über API und Datenbank.

setQuery(event.target.value)} placeholder="Aktivität, Ort oder Kategorie suchen" className="min-w-0 flex-1 bg-transparent text-sm font-semibold outline-none" />
{(["local", "state", "nationwide"] as ClubScope[]).map((item) => ( ))}
{filteredActivities.map((activity) => { const participant = activity.participants.find((item) => item.userId === profile.userId); const completed = Boolean(participant?.completedAt); const distance = distanceKm(profile, activity); return (
{categoryLabel(activity.category)} {activity.points} Punkte {activity.scope === "nationwide" ? : } {activity.locationLabel ?? scopeLabel((activity.scope ?? "nationwide") as ClubScope)} {distance !== undefined ? ` · ${distance} km` : ""}

{activity.title}

{activity.hostName} möchte am {formatDateTime(activity.startsAt)} für {activity.durationMinutes} Minuten los.

{activity.note ?

{activity.note}

: null}
{activity.participants.length || 0} dabei {activity.participants.length > 0 ? {activity.participants.map((item) => item.displayName).join(", ")} : null}
); })} {!filteredActivities.length ?

Für diesen Filter gibt es noch keine Aktivitäten.

: null}