← back to thosor87__sosoapp

Function bodies 60 total

All specs Real LLM only Function bodies
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: Times
App 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',
              clas
Toggle 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 scrollable
createEmptyAnnouncement 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.toDate
Repobility — 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 = registrat
MagicLinkGate 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 jus
shapeTooltipContent 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 al
GeomanControls 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

    ma
LayerToggle 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: shape
LayerToggle 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.pe
FoodSection 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-ro
AnimatedCounter 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',
      va
Repobility · 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"
            hei
getInitialData 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 ›