Function bodies 60 total
validateStep2 function · typescript · L30-L42 (13 LOC)src/features/registration/validation.ts
export function validateStep2(data: Step2Data): ValidationResult {
const errors: Record<string, string> = {}
if (data.food.bringsCake && !data.food.cakeDescription.trim()) {
errors.cakeDescription = 'Bitte beschreibe den Kuchen'
}
if (data.food.bringsSalad && !data.food.saladDescription.trim()) {
errors.saladDescription = 'Bitte beschreibe den Salat'
}
return { valid: Object.keys(errors).length === 0, errors }
}validateStep3 function · typescript · L46-L54 (9 LOC)src/features/registration/validation.ts
export function validateStep3(data: Step3Data): ValidationResult {
const errors: Record<string, string> = {}
if (data.camping.wantsCamping && data.camping.tentCount < 1) {
errors.tentCount = 'Mindestens 1 Zelt erforderlich'
}
return { valid: Object.keys(errors).length === 0, errors }
}TimelineNode function · typescript · L24-L129 (106 LOC)src/features/timeline/components/TimelineDisplay.tsx
function TimelineNode({
item,
index,
isLast,
}: {
item: TimelineItem
index: number
isLast: boolean
}) {
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-50px' })
const isEven = index % 2 === 0
const colors = CATEGORY_COLOR_MAP[item.category]
return (
<div ref={ref} className="relative flex items-start">
{/* Desktop layout: alternating sides */}
{/* Left content (desktop only) */}
<div className="hidden md:flex flex-1 justify-end pr-8">
{isEven && (
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1 }}
className="max-w-sm text-right"
>
<TimelineCard item={item} colors={colors} align="right" />
</motion.div>
)}
</div>
{/* Center line + dot (desktop) */}
<div className="hidden md:flex flex-col iTimelineCard function · typescript · L131-L169 (39 LOC)src/features/timeline/components/TimelineDisplay.tsx
function TimelineCard({
item,
colors,
align,
}: {
item: TimelineItem
colors: { bg: string; text: string; dot: string }
align: 'left' | 'right'
}) {
return (
<div
className={cn(
'rounded-xl border border-warm-100 bg-white p-4 shadow-sm mb-4 md:mb-0',
align === 'right' ? 'md:text-right' : ''
)}
>
<div
className={cn(
'flex items-center gap-2 mb-2',
align === 'right' ? 'md:flex-row-reverse' : ''
)}
>
<span
className={cn(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-bold',
colors.bg,
colors.text
)}
>
{item.time}
</span>
</div>
<h4 className="font-semibold text-warm-800 mb-1">{item.title}</h4>
{item.description && (
<p className="text-sm text-warm-500">{item.description}</p>
)}
</div>
)
}TimelineDisplay function · typescript · L175-L203 (29 LOC)src/features/timeline/components/TimelineDisplay.tsx
export function TimelineDisplay({ items }: TimelineDisplayProps) {
const visibleItems = items.filter((item) => item.isVisible)
if (visibleItems.length === 0) {
return (
<div className="rounded-2xl border border-warm-100 bg-white p-8 text-center text-warm-400">
Der Ablaufplan wird noch erstellt.
</div>
)
}
return (
<div className="relative">
{/* Desktop center line (behind everything) */}
<div className="hidden md:block absolute left-1/2 top-5 bottom-5 w-0.5 bg-warm-200 -translate-x-1/2" />
<div className="space-y-0">
{visibleItems.map((item, index) => (
<TimelineNode
key={item.id}
item={item}
index={index}
isLast={index === visibleItems.length - 1}
/>
))}
</div>
</div>
)
}cn function · typescript · L4-L6 (3 LOC)src/lib/utils/cn.ts
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}AdminPage function · typescript · L9-L68 (60 LOC)src/pages/AdminPage.tsx
export function AdminPage() {
const { isAdmin, loginAdmin } = useAuthStore()
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
const success = await loginAdmin(password)
if (!success) {
setError('Falsches Passwort')
setPassword('')
}
}
if (!isAdmin) {
return (
<PageContainer className="flex items-center justify-center min-h-[60vh]">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-sm text-center"
>
<span className="text-4xl block mb-4">{'\u{1F512}'}</span>
<h1 className="text-xl font-display font-bold text-warm-800 mb-2">Orga-Bereich</h1>
<p className="text-warm-500 text-sm mb-6">
Bitte gib das Admin-Passwort ein.
</p>
<form onSubmit={handleLogin} className="space-y-4">
Repobility analyzer · published findings · https://repobility.com
LandingPage function · typescript · L13-L232 (220 LOC)src/pages/LandingPage.tsx
export function LandingPage() {
const eventConfig = useAuthStore((s) => s.eventConfig)
const eventId = useAuthStore((s) => s.eventId)
const accessToken = useAuthStore((s) => s.accessToken)
const addToast = useToastStore((s) => s.addToast)
const [linkCopied, setLinkCopied] = useState(false)
const handleCopyLink = () => {
const url = `${window.location.origin}/?token=${accessToken}`
navigator.clipboard.writeText(url).then(() => {
addToast('Einladungslink kopiert!', 'success')
setLinkCopied(true)
setTimeout(() => setLinkCopied(false), 2000)
}).catch(() => {
addToast('Kopieren fehlgeschlagen', 'error')
})
}
const subscribeToRegistrations = useRegistrationStore(
(s) => s.subscribeToRegistrations
)
const timelineItems = useTimelineStore((s) => s.items)
const subscribeToTimeline = useTimelineStore((s) => s.subscribeToTimeline)
const mapData = useMapStore((s) => s.mapData)
const subscribeToMap = useMapStore((s) => s.subscriNotFoundPage function · typescript · L5-L26 (22 LOC)src/pages/NotFoundPage.tsx
export function NotFoundPage() {
return (
<div className="min-h-[60vh] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<span className="text-6xl block mb-4">{'\u{1F3D6}\uFE0F'}</span>
<h1 className="text-2xl font-display font-bold text-warm-800 mb-2">
Seite nicht gefunden
</h1>
<p className="text-warm-500 mb-6">
Diese Seite existiert leider nicht.
</p>
<Link to="/">
<Button>Zur{'\u00FC'}ck zur Startseite</Button>
</Link>
</motion.div>
</div>
)
}OverviewPage function · typescript · L11-L50 (40 LOC)src/pages/OverviewPage.tsx
export function OverviewPage() {
const eventId = useAuthStore((s) => s.eventId)
const subscribeToRegistrations = useRegistrationStore(
(s) => s.subscribeToRegistrations
)
useEffect(() => {
if (!eventId) return
const unsubscribe = subscribeToRegistrations(eventId)
return () => unsubscribe()
}, [eventId, subscribeToRegistrations])
return (
<PageContainer>
<motion.h1
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="text-3xl font-display font-bold text-warm-800 mb-6"
>
Übersicht
</motion.h1>
{/* Stat Cards */}
<section className="mb-8">
<StatCards />
</section>
{/* Registration List */}
<section className="mb-8">
<RegistrationList />
</section>
{/* Food & Camping */}
<section className="mb-8 space-y-4">
<FoodOverview />
<CampingList />
</section>
</PageContainer>
)
}‹ prevpage 2 / 2