Function bodies 60 total
seed function · typescript · L24-L69 (46 LOC)scripts/seed.ts
async function seed() {
console.log('Seeding Firestore...')
// Create the initial event document
const eventRef = doc(db, 'events', 'sommer2026')
await setDoc(eventRef, {
year: 2026,
title: 'Sorings Sommerfest 2026',
date: Timestamp.fromDate(new Date('2026-07-18T14:00:00')),
location: 'Bei Familie Soring',
accessToken: 'CHANGE_ME',
adminPasswordHash: 'CHANGE_ME',
announcements: [
{
id: 'welcome',
title: 'Willkommen!',
content:
'Schön, dass ihr dabei seid! Meldet euch an und gebt an, was ihr mitbringt.',
type: 'highlight',
order: 0,
isVisible: true,
},
{
id: 'zelten-info',
title: 'Zelten möglich!',
content:
'Dieses Jahr könnt ihr auch über Nacht bleiben. Gebt einfach bei der Anmeldung an, ob ihr zelten möchtet.',
type: 'info',
order: 1,
isVisible: true,
},
],
isRegistrationOpen: true,
createdAt: TimesApp function · typescript · L20-L54 (35 LOC)src/app/App.tsx
export function App() {
return (
<ErrorBoundary>
<BrowserRouter>
<MagicLinkGate>
<Header />
<AnimatePresence mode="wait">
<Routes>
<Route path="/" element={<LandingPage />} />
<Route
path="/uebersicht"
element={
<Suspense fallback={<LoadingScreen />}>
<OverviewPage />
</Suspense>
}
/>
<Route
path="/admin"
element={
<Suspense fallback={<LoadingScreen />}>
<AdminPage />
</Suspense>
}
/>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</AnimatePresence>
<Footer />
<ToastContainer />
</MagicLinkGate>
</BrowserRouter>
</ErrorBoundary>
)
}ErrorBoundary class · typescript · L12-L54 (43 LOC)src/components/feedback/ErrorBoundary.tsx
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
}
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo)
}
private handleReload = () => {
window.location.reload()
}
public render() {
if (this.state.hasError) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-[#FFFBF5] p-4">
<div className="w-full max-w-sm text-center">
<span className="text-6xl block mb-4">{'\u26A0\uFE0F'}</span>
<h1 className="text-2xl font-display font-bold text-warm-800 mb-2">
Etwas ist schiefgelaufen
</h1>
<p className="text-warm-500 text-sm mb-6">
Ein unerwarteter Fehler ist aufgetreten. Bitte lade die Seite neu.
</p>
<getDerivedStateFromError method · typescript · L17-L19 (3 LOC)src/components/feedback/ErrorBoundary.tsx
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}componentDidCatch method · typescript · L21-L23 (3 LOC)src/components/feedback/ErrorBoundary.tsx
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo)
}render method · typescript · L29-L53 (25 LOC)src/components/feedback/ErrorBoundary.tsx
public render() {
if (this.state.hasError) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-[#FFFBF5] p-4">
<div className="w-full max-w-sm text-center">
<span className="text-6xl block mb-4">{'\u26A0\uFE0F'}</span>
<h1 className="text-2xl font-display font-bold text-warm-800 mb-2">
Etwas ist schiefgelaufen
</h1>
<p className="text-warm-500 text-sm mb-6">
Ein unerwarteter Fehler ist aufgetreten. Bitte lade die Seite neu.
</p>
<button
onClick={this.handleReload}
className="inline-flex items-center justify-center rounded-xl font-medium transition-all duration-200 bg-primary-500 text-white hover:bg-primary-600 active:bg-primary-700 shadow-md hover:shadow-lg h-10 px-5 text-sm cursor-pointer"
>
Seite neu laden
</button>
</div>
</div>
)
}
LoadingScreen function · typescript · L3-L22 (20 LOC)src/components/feedback/LoadingScreen.tsx
export function LoadingScreen() {
return (
<div className="fixed inset-0 flex items-center justify-center bg-[#FFFBF5]">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center gap-4"
>
<motion.span
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="text-5xl"
>
{'\u2600\uFE0F'}
</motion.span>
<p className="text-warm-400 text-sm font-medium">Laden...</p>
</motion.div>
</div>
)
}Want this analysis on your repo? https://repobility.com/scan/
ToastItem function · typescript · L31-L56 (26 LOC)src/components/feedback/Toast.tsx
function ToastItem({ toast }: { toast: ToastData }) {
const removeToast = useToastStore((s) => s.removeToast)
useEffect(() => {
const timer = setTimeout(() => removeToast(toast.id), 4000)
return () => clearTimeout(timer)
}, [toast.id, removeToast])
return (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
className={cn(
'pointer-events-auto rounded-xl px-4 py-3 text-sm font-medium shadow-lg',
{
'bg-emerald-500 text-white': toast.type === 'success',
'bg-red-500 text-white': toast.type === 'error',
'bg-warm-800 text-white': toast.type === 'info',
}
)}
>
{toast.message}
</motion.div>
)
}ToastContainer function · typescript · L58-L70 (13 LOC)src/components/feedback/Toast.tsx
export function ToastContainer() {
const toasts = useToastStore((s) => s.toasts)
return (
<div className="fixed top-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none">
<AnimatePresence>
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} />
))}
</AnimatePresence>
</div>
)
}Footer function · typescript · L1-L45 (45 LOC)src/components/layout/Footer.tsx
export function Footer() {
return (
<footer className="bg-warm-800 text-warm-300 py-8 px-6 text-center">
<div className="mx-auto max-w-5xl space-y-3">
<div className="flex items-center justify-center gap-2 text-white">
<span className="text-xl">{'\u2600\uFE0F'}</span>
<span className="font-display font-bold text-lg">SoSo</span>
</div>
<p className="text-sm">
Self-Made with{' '}
<span className="text-red-400">{'\u2764\uFE0F'}</span>
{' '}and AI by{' '}
<a
href="https://www.lilapixel.de"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center align-middle hover:opacity-80 transition-opacity"
>
<img
src="https://drat580elycl3.cloudfront.net/images/LP_Logo.png"
alt="LILAPIXEL Grafikdesign"
className="h-8 w-auto ml-1.5 hover:-translate-y-0.5 transition-transform"
Header function · typescript · L12-L129 (118 LOC)src/components/layout/Header.tsx
export function Header() {
const location = useLocation()
const [menuOpen, setMenuOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
// Close menu on route change
useEffect(() => {
setMenuOpen(false)
}, [location.pathname])
// Close menu on click outside
useEffect(() => {
if (!menuOpen) return
function handleClick(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [menuOpen])
return (
<header
ref={menuRef}
className="sticky top-0 z-40 border-b border-warm-100 bg-white/80 backdrop-blur-md"
>
<div className="mx-auto max-w-5xl flex items-center justify-between px-4 md:px-6 h-14 md:h-16">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<span className="text-2xl"handleClick function · typescript · L25-L29 (5 LOC)src/components/layout/Header.tsx
function handleClick(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false)
}
}PageContainer function · typescript · L10-L22 (13 LOC)src/components/layout/PageContainer.tsx
export function PageContainer({ children, className }: PageContainerProps) {
return (
<motion.main
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className={cn('mx-auto max-w-5xl px-4 md:px-6 py-6 md:py-10', className)}
>
{children}
</motion.main>
)
}Badge function · typescript · L8-L26 (19 LOC)src/components/ui/Badge.tsx
export function Badge({ className, variant = 'default', children, ...props }: BadgeProps) {
return (
<span
className={cn(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
{
'bg-warm-100 text-warm-700': variant === 'default',
'bg-emerald-100 text-emerald-700': variant === 'success',
'bg-amber-100 text-amber-700': variant === 'warning',
'bg-blue-100 text-blue-700': variant === 'info',
},
className
)}
{...props}
>
{children}
</span>
)
}Card function · typescript · L8-L21 (14 LOC)src/components/ui/Card.tsx
export function Card({ className, hover = false, children, ...props }: CardProps) {
return (
<div
className={cn(
'rounded-2xl bg-white border border-warm-100 shadow-sm',
hover && 'transition-all duration-300 hover:shadow-md hover:-translate-y-0.5',
className
)}
{...props}
>
{children}
</div>
)
}Repobility · code-quality intelligence · https://repobility.com
CardHeader function · typescript · L23-L29 (7 LOC)src/components/ui/Card.tsx
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn('px-6 pt-6 pb-2', className)} {...props}>
{children}
</div>
)
}CardContent function · typescript · L31-L37 (7 LOC)src/components/ui/Card.tsx
export function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn('px-6 pb-6', className)} {...props}>
{children}
</div>
)
}Modal function · typescript · L13-L53 (41 LOC)src/components/ui/Modal.tsx
export function Modal({ isOpen, onClose, title, children, className }: ModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ type: 'spring', duration: 0.3 }}
className={cn(
'relative w-full max-w-lg rounded-2xl bg-white shadow-xl p-6',
clasToggle function · typescript · L10-L33 (24 LOC)src/components/ui/Toggle.tsx
export function Toggle({ checked, onChange, label, className }: ToggleProps) {
return (
<label className={cn('inline-flex items-center gap-3 cursor-pointer', className)}>
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={cn(
'relative inline-flex h-6 w-11 shrink-0 rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2',
checked ? 'bg-primary-500' : 'bg-warm-300'
)}
>
<span
className={cn(
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow-sm ring-0 transition-transform duration-200',
checked ? 'translate-x-5.5 mt-0.5 ml-0' : 'translate-x-0.5 mt-0.5'
)}
/>
</button>
{label && <span className="text-sm font-medium text-warm-700">{label}</span>}
</label>
)
}AdminDashboard function · typescript · L29-L160 (132 LOC)src/features/admin/components/AdminDashboard.tsx
export function AdminDashboard() {
const [activeTab, setActiveTab] = useState<TabId>('announcements')
const eventId = useAuthStore((s) => s.eventId)
const logoutAdmin = useAuthStore((s) => s.logoutAdmin)
const subscribeToRegistrations = useRegistrationStore(
(s) => s.subscribeToRegistrations
)
useEffect(() => {
if (!eventId) return
const unsubscribe = subscribeToRegistrations(eventId)
return () => unsubscribe()
}, [eventId, subscribeToRegistrations])
const renderContent = () => {
switch (activeTab) {
case 'announcements':
return <AnnouncementEditor />
case 'registrations':
return <RegistrationManager />
case 'timeline':
return <TimelineEditor />
case 'map':
return <MapEditor />
case 'settings':
return <EventSettings />
default:
return null
}
}
return (
<div className="flex flex-col md:flex-row gap-6 min-h-[60vh]">
{/* Mobile: horizontal scrollablecreateEmptyAnnouncement function · typescript · L20-L29 (10 LOC)src/features/admin/components/AnnouncementEditor.tsx
function createEmptyAnnouncement(order: number): Announcement {
return {
id: crypto.randomUUID(),
title: '',
content: '',
type: 'info',
order,
isVisible: true,
}
}AnnouncementEditor function · typescript · L31-L367 (337 LOC)src/features/admin/components/AnnouncementEditor.tsx
export function AnnouncementEditor() {
const eventConfig = useAuthStore((s) => s.eventConfig)
const eventId = useAuthStore((s) => s.eventId)
const addToast = useToastStore((s) => s.addToast)
const [editingId, setEditingId] = useState<string | null>(null)
const [editData, setEditData] = useState<Announcement | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
const announcements = eventConfig?.announcements ?? []
const saveAnnouncements = async (updated: Announcement[]) => {
if (!eventId) return
setIsSaving(true)
try {
const docRef = doc(db, 'events', eventId)
await updateDoc(docRef, {
announcements: updated,
updatedAt: serverTimestamp(),
})
addToast('Ankündigungen gespeichert', 'success')
} catch (error) {
console.error('Error saving announcements:', error)
addToast('Fehler beim Speichern', 'error')
} finally EventSettings function · typescript · L10-L189 (180 LOC)src/features/admin/components/EventSettings.tsx
export function EventSettings() {
const eventConfig = useAuthStore((s) => s.eventConfig)
const eventId = useAuthStore((s) => s.eventId)
const addToast = useToastStore((s) => s.addToast)
const [title, setTitle] = useState('')
const [date, setDate] = useState('')
const [location, setLocation] = useState('')
const [accessToken, setAccessToken] = useState('')
const [newPassword, setNewPassword] = useState('')
const [isRegistrationOpen, setIsRegistrationOpen] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [isSavingPassword, setIsSavingPassword] = useState(false)
useEffect(() => {
if (!eventConfig) return
setTitle(eventConfig.title || '')
setLocation(eventConfig.location || '')
setAccessToken(eventConfig.accessToken || '')
setIsRegistrationOpen(eventConfig.isRegistrationOpen ?? true)
// Convert Timestamp to date string for the date input
if (eventConfig.date) {
try {
const d = eventConfig.date.toDateRepobility — the code-quality scanner for AI-generated software · https://repobility.com
RegistrationManager function · typescript · L13-L300 (288 LOC)src/features/admin/components/RegistrationManager.tsx
export function RegistrationManager() {
const registrations = useRegistrationStore((s) => s.registrations)
const addToast = useToastStore((s) => s.addToast)
const [search, setSearch] = useState('')
const [editReg, setEditReg] = useState<Registration | null>(null)
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const filtered = useMemo(() => {
if (!search.trim()) return registrations
const q = search.toLowerCase()
return registrations.filter(
(r) =>
r.familyName.toLowerCase().includes(q) ||
r.contactName.toLowerCase().includes(q)
)
}, [registrations, search])
const stats = useMemo(() => {
const totalAdults = registrations.reduce((s, r) => s + r.adultsCount, 0)
const totalChildren = registrations.reduce((s, r) => s + r.childrenCount, 0)
const totalCake = registrations.filter((r) => r.food.bringsCake).length
const totalSalad = registratMagicLinkGate function · typescript · L13-L82 (70 LOC)src/features/auth/components/MagicLinkGate.tsx
export function MagicLinkGate({ children }: MagicLinkGateProps) {
const [searchParams] = useSearchParams()
const { isValidated, isLoading, validateToken } = useAuthStore()
const [manualToken, setManualToken] = useState('')
const [error, setError] = useState('')
useEffect(() => {
const tokenFromUrl = searchParams.get('token')
const storedToken = localStorage.getItem('soso-token')
const token = tokenFromUrl || storedToken
if (token) {
validateToken(token)
} else {
useAuthStore.getState().setLoading(false)
}
}, [searchParams, validateToken])
const handleManualSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!manualToken.trim()) return
setError('')
const valid = await validateToken(manualToken.trim())
if (!valid) {
setError('Ung\u00FCltiger Zugangscode')
}
}
if (isLoading) return <LoadingScreen />
if (!isValidated) {
return (
<div className="min-h-screen flex items-center jusshapeTooltipContent function · typescript · L19-L22 (4 LOC)src/features/map/components/GeomanControls.tsx
function shapeTooltipContent(properties: GeoJSONFeature['properties']): string {
const icon = CATEGORY_ICONS[properties.category] || ''
return properties.label ? `${icon} ${properties.label}` : icon
}layerToFeature function · typescript · L38-L80 (43 LOC)src/features/map/components/GeomanControls.tsx
function layerToFeature(
layer: L.Layer,
category: string,
color: string
): GeoJSONFeature | null {
if (!('toGeoJSON' in layer)) return null
const geoLayer = layer as L.Layer & { toGeoJSON: () => GeoJSON.Feature }
const geojson = geoLayer.toGeoJSON()
const anyLayer = layer as Record<string, unknown>
// Use persistent ID if available, otherwise generate a new UUID
const id = (anyLayer._persistentId as string) || crypto.randomUUID()
anyLayer._persistentId = id
// For edited existing shapes, preserve their saved properties
const saved = anyLayer._shapeProps as GeoJSONFeature['properties'] | undefined
const properties: GeoJSONFeature['properties'] = {
id,
label: saved?.label ?? '',
category: saved?.category ?? category,
color: saved?.color ?? color,
strokeColor: saved?.strokeColor ?? '#44403c',
strokeWidth: saved?.strokeWidth ?? 2,
opacity: saved?.opacity ?? 0.5,
}
// Preserve circle radius (toGeoJSON converts circles to Point,addShapeToMap function · typescript · L86-L137 (52 LOC)src/features/map/components/GeomanControls.tsx
function addShapeToMap(map: L.Map, shape: GeoJSONFeature) {
const style: L.PathOptions = {
color: shape.properties.strokeColor || '#44403c',
weight: shape.properties.strokeWidth || 2,
fillColor: shape.properties.color || '#8b5cf6',
fillOpacity: shape.properties.opacity ?? 0.5,
}
// Handle circles specially (GeoJSON Point + saved radius)
if (shape.properties.shapeType === 'Circle' && shape.geometry.type === 'Point') {
const coords = shape.geometry.coordinates as number[]
const circle = L.circle([coords[1], coords[0]], {
radius: shape.properties.radius || 50,
...style,
})
const anyCircle = circle as unknown as Record<string, unknown>
anyCircle._persistentId = shape.properties.id
anyCircle._shapeProps = { ...shape.properties }
circle.bindTooltip(shapeTooltipContent(shape.properties), {
permanent: true,
direction: 'center',
className: 'shape-label',
})
circle.addTo(map)
return
}
// Handle alGeomanControls function · typescript · L139-L281 (143 LOC)src/features/map/components/GeomanControls.tsx
export function GeomanControls({
onShapeCreated,
onShapeEdited,
onShapeDeleted,
activeCategory,
activeColor,
initialShapes,
}: GeomanControlsProps) {
const map = useMap()
const categoryRef = useRef(activeCategory)
const colorRef = useRef(activeColor)
const shapesLoadedRef = useRef(false)
// Keep refs in sync
useEffect(() => {
categoryRef.current = activeCategory
}, [activeCategory])
useEffect(() => {
colorRef.current = activeColor
}, [activeColor])
// Render saved shapes onto the Leaflet map (once per mount)
useEffect(() => {
if (!map || shapesLoadedRef.current || !initialShapes?.length) return
shapesLoadedRef.current = true
initialShapes.forEach((shape) => {
try {
addShapeToMap(map, shape)
} catch (err) {
console.error('Error loading saved shape:', err, shape)
}
})
}, [map, initialShapes])
// Initialize geoman controls and event handlers
useEffect(() => {
if (!map) return
maLayerToggle function · typescript · L62-L97 (36 LOC)src/features/map/components/MapDisplay.tsx
function LayerToggle({
activeLayer,
onToggle,
}: {
activeLayer: TileLayerType
onToggle: (layer: TileLayerType) => void
}) {
return (
<div className="absolute top-3 right-3 z-[1000] flex rounded-lg overflow-hidden shadow-md border border-warm-200">
<button
type="button"
onClick={() => onToggle('osm')}
className={cn(
'px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer',
activeLayer === 'osm'
? 'bg-primary-500 text-white'
: 'bg-white text-warm-600 hover:bg-warm-50'
)}
>
Karte
</button>
<button
type="button"
onClick={() => onToggle('satellite')}
className={cn(
'px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer',
activeLayer === 'satellite'
? 'bg-primary-500 text-white'
: 'bg-white text-warm-600 hover:bg-warm-50'
)}
>
Satellit
</button>
</div>
CircleLayers function · typescript · L99-L137 (39 LOC)src/features/map/components/MapDisplay.tsx
function CircleLayers({
circles,
buildPopup,
}: {
circles: GeoJSONFeature[]
buildPopup: (category: string, label: string) => string
}) {
const map = useMap()
useEffect(() => {
const layers: L.Circle[] = []
circles.forEach((shape) => {
const coords = shape.geometry.coordinates as number[]
const circle = L.circle([coords[1], coords[0]], {
radius: shape.properties.radius || 50,
color: shape.properties.strokeColor || '#44403c',
weight: shape.properties.strokeWidth || 2,
fillColor: shape.properties.color || '#8b5cf6',
fillOpacity: shape.properties.opacity ?? 0.5,
})
const category = shape.properties.category || 'other'
const label = shape.properties.label || ''
circle.bindPopup(buildPopup(category, label))
const icon = CATEGORY_ICONS[category] || ''
circle.bindTooltip(label ? `${icon} ${label}` : icon, {
permanent: true,
direction: 'center',
className: 'shape-Powered by Repobility — scan your code at https://repobility.com
SetBearing function · typescript · L139-L147 (9 LOC)src/features/map/components/MapDisplay.tsx
function SetBearing({ bearing }: { bearing: number }) {
const map = useMap()
useEffect(() => {
if (bearing && (map as L.Map & { setBearing?: (b: number) => void }).setBearing) {
(map as L.Map & { setBearing: (b: number) => void }).setBearing(bearing)
}
}, [map, bearing])
return null
}MapDisplay function · typescript · L153-L270 (118 LOC)src/features/map/components/MapDisplay.tsx
export function MapDisplay({ mapData }: MapDisplayProps) {
const [tileLayer, setTileLayer] = useState<TileLayerType>('osm')
const center: [number, number] = [mapData.center.lat, mapData.center.lng]
// Separate circles from other shapes (GeoJSON can't represent circles natively)
const { circleShapes, otherShapes } = useMemo(() => {
const circles: GeoJSONFeature[] = []
const others: GeoJSONFeature[] = []
for (const shape of mapData.shapes || []) {
if (shape.properties.shapeType === 'Circle' && shape.geometry.type === 'Point') {
circles.push(shape)
} else {
others.push(shape)
}
}
return { circleShapes: circles, otherShapes: others }
}, [mapData.shapes])
// Build a GeoJSON FeatureCollection from non-circle shapes
const geojsonData = useMemo(() => {
return {
type: 'FeatureCollection' as const,
features: otherShapes.map((shape: GeoJSONFeature) => ({
type: 'Feature' as const,
geometry: shapeLayerToggle function · typescript · L79-L114 (36 LOC)src/features/map/components/MapEditor.tsx
function LayerToggle({
activeLayer,
onToggle,
}: {
activeLayer: TileLayerType
onToggle: (layer: TileLayerType) => void
}) {
return (
<div className="absolute top-3 right-3 z-[1000] flex rounded-lg overflow-hidden shadow-md border border-warm-200">
<button
type="button"
onClick={() => onToggle('osm')}
className={cn(
'px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer',
activeLayer === 'osm'
? 'bg-primary-500 text-white'
: 'bg-white text-warm-600 hover:bg-warm-50'
)}
>
Karte
</button>
<button
type="button"
onClick={() => onToggle('satellite')}
className={cn(
'px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer',
activeLayer === 'satellite'
? 'bg-primary-500 text-white'
: 'bg-white text-warm-600 hover:bg-warm-50'
)}
>
Satellit
</button>
</div>
FlyToLocation function · typescript · L118-L139 (22 LOC)src/features/map/components/MapEditor.tsx
function FlyToLocation({ center, zoom, bearing }: { center: [number, number]; zoom: number; bearing: number }) {
const map = useMap()
const didFly = useRef(false)
useEffect(() => {
if (didFly.current) return
if (
center[0] !== DEFAULT_CENTER[0] ||
center[1] !== DEFAULT_CENTER[1] ||
zoom !== DEFAULT_ZOOM
) {
map.setView(center, zoom)
// Restore saved bearing/rotation
if (bearing && (map as L.Map & { setBearing?: (b: number) => void }).setBearing) {
(map as L.Map & { setBearing: (b: number) => void }).setBearing(bearing)
}
didFly.current = true
}
}, [map, center, zoom, bearing])
return null
}SearchFly function · typescript · L143-L151 (9 LOC)src/features/map/components/MapEditor.tsx
function SearchFly({ target }: { target: [number, number] | null }) {
const map = useMap()
useEffect(() => {
if (target) {
map.flyTo(target, 17, { duration: 1.5 })
}
}, [map, target])
return null
}serializeShapes function · typescript · L23-L31 (9 LOC)src/features/map/store.ts
function serializeShapes(shapes: GeoJSONFeature[]): unknown[] {
return shapes.map((s) => ({
...s,
geometry: {
...s.geometry,
coordinates: JSON.stringify(s.geometry.coordinates),
},
}))
}deserializeShapes function · typescript · L33-L49 (17 LOC)src/features/map/store.ts
function deserializeShapes(shapes: unknown[]): GeoJSONFeature[] {
if (!Array.isArray(shapes)) return []
return shapes.map((raw) => {
const s = raw as Record<string, unknown>
const geo = s.geometry as Record<string, unknown>
return {
...s,
geometry: {
...geo,
coordinates:
typeof geo.coordinates === 'string'
? JSON.parse(geo.coordinates)
: geo.coordinates,
},
} as GeoJSONFeature
})
}serializeBackups function · typescript · L51-L56 (6 LOC)src/features/map/store.ts
function serializeBackups(backups: MapBackup[]): unknown[] {
return backups.map((b) => ({
...b,
shapes: serializeShapes(b.shapes),
}))
}Want this analysis on your repo? https://repobility.com/scan/
deserializeBackups function · typescript · L58-L67 (10 LOC)src/features/map/store.ts
function deserializeBackups(backups: unknown[]): MapBackup[] {
if (!Array.isArray(backups)) return []
return backups.map((raw) => {
const b = raw as Record<string, unknown>
return {
...b,
shapes: deserializeShapes(b.shapes as unknown[]),
}
}) as MapBackup[]
}CampingList function · typescript · L5-L82 (78 LOC)src/features/overview/components/CampingList.tsx
export function CampingList() {
const registrations = useRegistrationStore((s) => s.registrations)
const campers = registrations.filter((r) => r.camping.wantsCamping)
const totalTents = campers.reduce((sum, r) => sum + r.camping.tentCount, 0)
const totalPersons = campers.reduce(
(sum, r) => sum + (r.camping.personCount || r.adultsCount + r.childrenCount),
0
)
return (
<Card className="overflow-hidden">
<div className="px-5 py-4 border-b border-warm-100 bg-blue-50/50">
<h3 className="font-display font-bold text-warm-800">
{'\u26FA'} Zelter ({campers.length} Familien)
</h3>
</div>
<div className="p-4">
{campers.length === 0 ? (
<p className="text-sm text-warm-400 text-center py-4 italic">
Noch niemand zum Zelten angemeldet
</p>
) : (
<>
<div className="space-y-2">
{campers.map((reg, i) => {
const persons = reg.camping.peFoodSection function · typescript · L10-L61 (52 LOC)src/features/overview/components/FoodOverview.tsx
function FoodSection({
title,
emoji,
items,
emptyText,
colorClass,
}: {
title: string
emoji: string
items: FoodItem[]
emptyText: string
colorClass: string
}) {
return (
<Card className="overflow-hidden flex-1">
<div className={`px-5 py-4 border-b border-warm-100 ${colorClass}`}>
<h3 className="font-display font-bold text-warm-800">
{emoji} {title} ({items.length})
</h3>
</div>
<div className="p-4">
{items.length === 0 ? (
<p className="text-sm text-warm-400 text-center py-4 italic">
{emptyText}
</p>
) : (
<div className="space-y-2">
{items.map((item, i) => (
<motion.div
key={`${item.familyName}-${i}`}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="flex items-start gap-3 rounded-lg bg-warm-50 p-3"FoodOverview function · typescript · L63-L98 (36 LOC)src/features/overview/components/FoodOverview.tsx
export function FoodOverview() {
const registrations = useRegistrationStore((s) => s.registrations)
const cakes: FoodItem[] = registrations
.filter((r) => r.food.bringsCake)
.map((r) => ({
familyName: r.familyName,
description: r.food.cakeDescription,
}))
const salads: FoodItem[] = registrations
.filter((r) => r.food.bringsSalad)
.map((r) => ({
familyName: r.familyName,
description: r.food.saladDescription,
}))
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FoodSection
title="Kuchen"
emoji={'\uD83C\uDF82'}
items={cakes}
emptyText="Noch keine Kuchen angemeldet"
colorClass="bg-amber-50/50"
/>
<FoodSection
title="Salate"
emoji={'\uD83E\uDD57'}
items={salads}
emptyText="Noch keine Salate angemeldet"
colorClass="bg-emerald-50/50"
/>
</div>
)
}RegistrationList function · typescript · L11-L243 (233 LOC)src/features/overview/components/RegistrationList.tsx
export function RegistrationList() {
const registrations = useRegistrationStore((s) => s.registrations)
const isLoading = useRegistrationStore((s) => s.isLoading)
const [search, setSearch] = useState('')
const [editingRegistration, setEditingRegistration] =
useState<Registration | null>(null)
const filtered = registrations.filter(
(r) =>
r.familyName.toLowerCase().includes(search.toLowerCase()) ||
r.contactName.toLowerCase().includes(search.toLowerCase())
)
if (isLoading) {
return (
<Card className="p-8">
<div className="flex items-center justify-center gap-3 text-warm-400">
<div className="w-5 h-5 border-2 border-warm-300 border-t-primary-500 rounded-full animate-spin" />
<span>Lade Anmeldungen...</span>
</div>
</Card>
)
}
return (
<>
<Card className="overflow-hidden">
<div className="p-4 md:p-6 border-b border-warm-100">
<div className="flex flex-col sm:flex-roAnimatedCounter function · typescript · L6-L41 (36 LOC)src/features/overview/components/StatCards.tsx
function AnimatedCounter({ value, duration = 1.2 }: { value: number; duration?: number }) {
const [display, setDisplay] = useState(0)
const ref = useRef<HTMLSpanElement>(null)
const isInView = useInView(ref, { once: true })
useEffect(() => {
if (!isInView) return
let start = 0
const end = value
if (end === 0) {
setDisplay(0)
return
}
const startTime = performance.now()
const durationMs = duration * 1000
function animate(currentTime: number) {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / durationMs, 1)
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3)
const current = Math.round(start + (end - start) * eased)
setDisplay(current)
if (progress < 1) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
}, [value, isInView, duration])
return <span ref={ref}>{display}</span>
}animate function · typescript · L24-L35 (12 LOC)src/features/overview/components/StatCards.tsx
function animate(currentTime: number) {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / durationMs, 1)
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3)
const current = Math.round(start + (end - start) * eased)
setDisplay(current)
if (progress < 1) {
requestAnimationFrame(animate)
}
}StatCards function · typescript · L52-L136 (85 LOC)src/features/overview/components/StatCards.tsx
export function StatCards() {
const registrations = useRegistrationStore((s) => s.registrations)
const totalAdults = registrations.reduce((sum, r) => sum + r.adultsCount, 0)
const totalChildren = registrations.reduce(
(sum, r) => sum + r.childrenCount,
0
)
const totalGuests = totalAdults + totalChildren
const campers = registrations.filter((r) => r.camping.wantsCamping)
const totalCampingPersons = campers.reduce(
(sum, r) => sum + (r.camping.personCount || r.adultsCount + r.childrenCount),
0
)
const totalTents = campers.reduce((sum, r) => sum + r.camping.tentCount, 0)
const stats: StatCardData[] = [
{
label: 'Gesamt',
value: totalGuests,
icon: '\uD83D\uDC65',
color: 'text-primary-600',
bgColor: 'bg-primary-50',
},
{
label: 'Erwachsene',
value: totalAdults,
icon: '\uD83E\uDDD1',
color: 'text-secondary-600',
bgColor: 'bg-secondary-50',
},
{
label: 'Kinder',
vaRepobility · code-quality intelligence · https://repobility.com
NumberStepper function · typescript · L14-L96 (83 LOC)src/features/registration/components/NumberStepper.tsx
export function NumberStepper({
value,
onChange,
min = 0,
max = 99,
label,
error,
className,
}: NumberStepperProps) {
const decrement = () => {
if (value > min) onChange(value - 1)
}
const increment = () => {
if (value < max) onChange(value + 1)
}
return (
<div className={cn('space-y-1.5', className)}>
{label && (
<label className="block text-sm font-medium text-warm-700">
{label}
</label>
)}
<div className="inline-flex items-center gap-3 rounded-xl border border-warm-200 bg-white px-2 py-1.5">
<button
type="button"
onClick={decrement}
disabled={value <= min}
className="flex h-9 w-9 items-center justify-center rounded-lg bg-warm-100 text-warm-600 transition-all duration-200 hover:bg-primary-100 hover:text-primary-700 active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
>
<svg
width="16"
heigetInitialData function · typescript · L48-L79 (32 LOC)src/features/registration/components/RegistrationForm.tsx
function getInitialData(reg?: Registration): FormData {
if (reg) {
return {
familyName: reg.familyName,
contactName: reg.contactName,
adultsCount: reg.adultsCount,
childrenCount: reg.childrenCount,
food: { ...reg.food },
camping: { ...reg.camping, personCount: reg.camping.personCount ?? 0 },
comments: reg.comments,
}
}
return {
familyName: '',
contactName: '',
adultsCount: 1,
childrenCount: 0,
food: {
bringsCake: false,
cakeDescription: '',
bringsSalad: false,
saladDescription: '',
},
camping: {
wantsCamping: false,
tentCount: 0,
personCount: 0,
notes: '',
},
comments: '',
}
}validateStep1 function · typescript · L10-L26 (17 LOC)src/features/registration/validation.ts
export function validateStep1(data: Step1Data): ValidationResult {
const errors: Record<string, string> = {}
if (!data.familyName.trim()) {
errors.familyName = 'Haushalt/Familie ist erforderlich'
}
if (!data.contactName.trim()) {
errors.contactName = 'Ansprechpartner ist erforderlich'
}
if (data.adultsCount < 1) {
errors.adultsCount = 'Mindestens 1 Erwachsener erforderlich'
}
return { valid: Object.keys(errors).length === 0, errors }
}page 1 / 2next ›