diff --git a/app/about-us/AboutUsContent.tsx b/app/about-us/AboutUsContent.tsx new file mode 100644 index 0000000..1d03a37 --- /dev/null +++ b/app/about-us/AboutUsContent.tsx @@ -0,0 +1,341 @@ +"use client" + +import { useState } from 'react' +import Image from 'next/image' +import { useInView, useAnimatedNumber } from '@/lib/hooks' +import Hero from '@/components/Hero' +import HeroCard from '@/components/HeroCard' +import CTA from '@/components/CTA' + +const executiveBoard = [ + { name: "Ali Serag El Din", role: "President", photo: "/aboutUs/Board/Ali-opt.png", linkedinUrl: "https://www.linkedin.com/in/ali-serag-el-din/" }, + { name: "Defne Aytuna", role: "Vice President", photo: "/aboutUs/Board/Defne-opt.png", linkedinUrl: "https://www.linkedin.com/in/defne-aytuna/" }, + { name: "Simon Burmer", role: "CFO", photo: "/aboutUs/Board/Simon-opt.png", linkedinUrl: "https://www.linkedin.com/in/simon-burmer/" }, +] + +const departmentBoard = [ + { name: "Mohammed Thabit", role: "MD Events", photo: "/aboutUs/Board/Mohammed-opt.png", linkedinUrl: "https://www.linkedin.com/in/mohammed-thabit/" }, + { name: "Piotr Nobis", role: "MD Marketing", photo: "/aboutUs/Board/Piotr-opt.png", linkedinUrl: "https://www.linkedin.com/in/piotr-nobis/" }, + { name: "Anna Heletych", role: "MD People", photo: "/aboutUs/Board/Anna-opt.png", linkedinUrl: "https://www.linkedin.com/in/anna-heletych/" }, + { name: "Niklas Simakov", role: "MD Finance & Operations", photo: "/aboutUs/Board/Niklas-opt.png", linkedinUrl: "https://www.linkedin.com/in/niklas-simakov/" }, + { name: "Marius Heumader", role: "MD Partnerships", photo: "/aboutUs/Board/Marius-opt.png", linkedinUrl: "https://www.linkedin.com/in/marius-heumader/" }, +] + +const advisoryBoard = [ + { name: "Advisory Member 1", role: "CEO, Company", bio: "Brings 20+ years of experience in scaling tech companies across Europe. Advises on corporate strategy and international expansion.", photo: "https://images.unsplash.com/photo-1560250097-0b93528c311a?w=400&h=500&fit=crop&crop=face" }, + { name: "Advisory Member 2", role: "Founder, Startup", bio: "Serial entrepreneur with three successful exits. Mentors early-stage founders on product-market fit and fundraising.", photo: "https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?w=400&h=500&fit=crop&crop=face" }, + { name: "Advisory Member 3", role: "Partner, VC Fund", bio: "Leads early-stage investments in deep tech and SaaS. Connects our startups with the European investor network.", photo: "https://images.unsplash.com/photo-1566492031773-4f4e44671857?w=400&h=500&fit=crop&crop=face" }, + { name: "Advisory Member 4", role: "Professor, TUM", bio: "Chair of Entrepreneurship at TU Munich. Bridges academic research with real-world startup building.", photo: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=500&fit=crop&crop=face" }, + { name: "Advisory Member 5", role: "Managing Director", bio: "Runs one of Munich's leading accelerator programs. Expert in go-to-market strategy and corporate partnerships.", photo: "https://images.unsplash.com/photo-1580489944761-15a19d654956?w=400&h=500&fit=crop&crop=face" }, + { name: "Advisory Member 6", role: "Angel Investor", bio: "Backed 40+ startups across fintech, healthtech, and mobility. Offers hands-on support in the critical first 18 months.", photo: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop&crop=face" }, + { name: "Advisory Member 7", role: "COO, Scale-up", bio: "Operational leader who scaled a Munich startup from 10 to 500 employees. Advises on hiring, culture, and processes.", photo: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=500&fit=crop&crop=face" }, +] + +const missionPartners = [ + { name: "MTZ", description: "The Münchner Technologiezentrum provides office space, business coaching, and networking for young tech companies near Olympic Park — helping startups grow from first idea to market.", logo: "https://mtz.de/wp-content/uploads/2021/10/White-1.svg", image: "/aboutUs/missionPartner/mtz-opt.jpg" }, + { name: "Munich Startup", description: "Munich's official startup portal connecting founders with resources, investors, and the local ecosystem — mapping the city's innovation landscape and amplifying its startup scene.", logo: "https://www.munich-startup.de/wp-content/themes/munichstartup/dist/images/munich-startup-logo-w.svg", image: "/aboutUs/missionPartner/MunichStartup.png" }, + { name: "CDTM and Manage & More", descriptionParts: [ + { text: "CDTM and Manage & More share our mission in empowering the next generation of founders in Munich. " }, + { text: "However START Munich and CDTM / M&M differ. We are a community that encourages learning by doing, while they are educational programs that provide structured curriculum. ", pink: true }, + { text: "Some of our members also join CDTM or M&M, but handling the intensive time commitment for both is challenging." }, + ], image: "/aboutUs/missionPartner/CDTM.png", image2: "/aboutUs/missionPartner/mandm.jpeg" }, +] + +const showAdvisoryBoard = process.env.NEXT_PUBLIC_SHOW_ADVISORY_BOARD === 'true' + +export default function AboutUsPage() { + const [selectedAdvisor, setSelectedAdvisor] = useState(0) + const animatedYears = useAnimatedNumber(20) + const animatedMembers = useAnimatedNumber(600) + const missionView = useInView(0.1) + const partnersView = useInView(0.1) + const execView = useInView(0.1) + const deptView = useInView(0.1) + const advView = useInView(0.1) + + return ( +
+ + WHO WE +
+ ARE + + } + description="A student-led community at the heart of Munich's startup scene — turning bold ideas into real ventures since 2003." + > +
+ +
+ {animatedYears} + + +
+

Years Active

+
+ +
+ {animatedMembers} + + +
+

Members

+
+
+
+ + {/* ═══ 01 VISION & MISSION — bold editorial ═══ */} +
+
+
+
+ Our Purpose +

+ VISION
& MISSION +

+
+
+ +
+
+
+
+ + + +
+ Mission +
+

+ Empowering
founders of
tomorrow. +

+

+ A self-driven community where learning happens by doing — and every member gets what is needed to build things that truly matters. +

+
+ +
+
+
+ + + + +
+ Vision +
+

+ Being the launchpad for innovation +

+

+ We envision START as the place for ambitious students in Munich, where ideas are turned into real innovations. +

+
+
+
+
+ + {/* ═══ 02 MISSION PARTNERS ═══ */} +
+
+
+ Mission Partners +

Organisations who share our mission and support us in building Munich's next generation of founders.

+
+ +
+ {missionPartners.map((partner, i) => ( +
+
+ {partner.image2 ? ( +
+ {partner.name} +
+ {partner.name} +
+ ) : ( + {partner.name} + )} +
+
+
+ {partner.image2 ? ( + <> +
+ CDTM +
+
+ Manage and More +
+ + ) : ( +
+ {partner.logo + ? {partner.name} + : {partner.name.charAt(0)} + } +
+ )} +

{partner.name}

+
+

+ {'descriptionParts' in partner + ? partner.descriptionParts?.map((part, i) => ( + {part.text} + )) + : partner.description} +

+
+
+ ))} +
+
+
+ + {/* Divider */} +
+
+
+ + + {/* ═══ 03 EXECUTIVE BOARD — side-by-side layout ═══ */} +
+
+ +
+
+ {/* Left: heading */} +
+ Meet the Team +

+ THE
EXECUTIVE
BOARD +

+
+ + {/* Right: portrait cards */} +
+ {executiveBoard.map((member, i) => ( + +
+ {member.name} +
+

{member.name}

+

{member.role}

+
+ ))} +
+
+
+
+ + {/* ═══ 04 DEPARTMENT BOARD — compact row with circles ═══ */} +
+
+
+
+
+ The Department Board +
+ +
+ {departmentBoard.map((member, i) => ( + +
+ {member.name} +
+

{member.name}

+

{member.role}

+
+ ))} +
+
+
+
+ + {/* ═══ 05 ADVISORY BOARD — thumbnail grid with expandable detail ═══ */} + {showAdvisoryBoard && ( +
+
+
+
+ Advisory Board +
+ Seasoned entrepreneurs and industry leaders who sharpen our direction. +
+ + {/* 7-column thumbnail grid */} +
+ {advisoryBoard.map((member, i) => ( + + ))} +
+ + {/* Expandable detail panel */} +
+ {selectedAdvisor !== null && ( +
+
+ {advisoryBoard[selectedAdvisor].name} +
+
+
+
+

{advisoryBoard[selectedAdvisor].name}

+

{advisoryBoard[selectedAdvisor].role}

+
+ +
+

{advisoryBoard[selectedAdvisor].bio}

+
+
+ )} +
+
+
+ )} + + {/* ═══ CTA ═══ */} +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/app/about-us/page.tsx b/app/about-us/page.tsx index a493cf0..21102dd 100644 --- a/app/about-us/page.tsx +++ b/app/about-us/page.tsx @@ -1,326 +1,19 @@ -"use client" - -import { useState } from 'react' -import Image from 'next/image' -import { useInView, useAnimatedNumber } from '@/lib/hooks' -import Hero from '@/components/Hero' -import HeroCard from '@/components/HeroCard' - -const executiveBoard = [ - { name: "Ali Serag El Din", role: "President", photo: "/aboutUs/Board/Ali-opt.png", linkedinUrl: "https://www.linkedin.com/in/ali-serag-el-din/" }, - { name: "Defne Aytuna", role: "Vice President", photo: "/aboutUs/Board/Defne-opt.png", linkedinUrl: "https://www.linkedin.com/in/defne-aytuna/" }, - { name: "Simon Burmer", role: "CFO", photo: "/aboutUs/Board/Simon-opt.png", linkedinUrl: "https://www.linkedin.com/in/simon-burmer/" }, -] - -const departmentBoard = [ - { name: "Mohammed Thabit", role: "MD Events", photo: "/aboutUs/Board/Mohammed-opt.png", linkedinUrl: "https://www.linkedin.com/in/mohammed-thabit/" }, - { name: "Piotr Nobis", role: "MD Marketing", photo: "/aboutUs/Board/Piotr-opt.png", linkedinUrl: "https://www.linkedin.com/in/piotr-nobis/" }, - { name: "Anna Heletych", role: "MD People", photo: "/aboutUs/Board/Anna-opt.png", linkedinUrl: "https://www.linkedin.com/in/anna-heletych/" }, - { name: "Niklas Simakov", role: "MD Finance & Operations", photo: "/aboutUs/Board/Niklas-opt.png", linkedinUrl: "https://www.linkedin.com/in/niklas-simakov/" }, - { name: "Marius Heumader", role: "MD Partnerships", photo: "/aboutUs/Board/Marius-opt.png", linkedinUrl: "https://www.linkedin.com/in/marius-heumader/" }, -] - -const advisoryBoard = [ - { name: "Advisory Member 1", role: "CEO, Company", bio: "Brings 20+ years of experience in scaling tech companies across Europe. Advises on corporate strategy and international expansion.", photo: "https://images.unsplash.com/photo-1560250097-0b93528c311a?w=400&h=500&fit=crop&crop=face" }, - { name: "Advisory Member 2", role: "Founder, Startup", bio: "Serial entrepreneur with three successful exits. Mentors early-stage founders on product-market fit and fundraising.", photo: "https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?w=400&h=500&fit=crop&crop=face" }, - { name: "Advisory Member 3", role: "Partner, VC Fund", bio: "Leads early-stage investments in deep tech and SaaS. Connects our startups with the European investor network.", photo: "https://images.unsplash.com/photo-1566492031773-4f4e44671857?w=400&h=500&fit=crop&crop=face" }, - { name: "Advisory Member 4", role: "Professor, TUM", bio: "Chair of Entrepreneurship at TU Munich. Bridges academic research with real-world startup building.", photo: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=500&fit=crop&crop=face" }, - { name: "Advisory Member 5", role: "Managing Director", bio: "Runs one of Munich's leading accelerator programs. Expert in go-to-market strategy and corporate partnerships.", photo: "https://images.unsplash.com/photo-1580489944761-15a19d654956?w=400&h=500&fit=crop&crop=face" }, - { name: "Advisory Member 6", role: "Angel Investor", bio: "Backed 40+ startups across fintech, healthtech, and mobility. Offers hands-on support in the critical first 18 months.", photo: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop&crop=face" }, - { name: "Advisory Member 7", role: "COO, Scale-up", bio: "Operational leader who scaled a Munich startup from 10 to 500 employees. Advises on hiring, culture, and processes.", photo: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=500&fit=crop&crop=face" }, -] - -const missionPartners = [ - { name: "MTZ", description: "The Münchner Technologiezentrum provides office space, business coaching, and networking for young tech companies near Olympic Park — helping startups grow from first idea to market.", logo: "https://mtz.de/wp-content/uploads/2021/10/White-1.svg", image: "/aboutUs/missionPartner/mtz-opt.jpg" }, - { name: "Munich Startup", description: "Munich's official startup portal connecting founders with resources, investors, and the local ecosystem — mapping the city's innovation landscape and amplifying its startup scene.", logo: "https://www.munich-startup.de/wp-content/themes/munichstartup/dist/images/munich-startup-logo-w.svg", image: "/aboutUs/missionPartner/MunichStartup.png" }, - { name: "CDTM and Manage & More", descriptionParts: [ - { text: "CDTM and Manage & More share our mission in empowering the next generation of founders in Munich. " }, - { text: "However START Munich and CDTM / M&M differ. We are a community that encourages learning by doing, while they are educational programs that provide structured curriculum. ", pink: true }, - { text: "Some of our members also join CDTM or M&M, but handling the intensive time commitment for both is challenging." }, - ], image: "/aboutUs/missionPartner/CDTM.png", image2: "/aboutUs/missionPartner/mandm.jpeg" }, -] - -const showAdvisoryBoard = process.env.NEXT_PUBLIC_SHOW_ADVISORY_BOARD === 'true' +import type { Metadata } from 'next' +import AboutUsContent from './AboutUsContent' + +export const metadata: Metadata = { + title: 'About Us', + description: + 'Meet the team behind START Munich. Learn about our mission, vision, and the passionate students driving Munich\'s leading entrepreneurship community since 2003.', + alternates: { canonical: 'https://www.startmunich.de/about-us' }, + openGraph: { + url: 'https://www.startmunich.de/about-us', + title: 'About Us | START Munich', + description: + 'Meet the team behind START Munich. Learn about our mission, vision, and the passionate students driving Munich\'s leading entrepreneurship community since 2003.', + }, +} export default function AboutUsPage() { - const [selectedAdvisor, setSelectedAdvisor] = useState(0) - const animatedYears = useAnimatedNumber(20) - const animatedMembers = useAnimatedNumber(600) - const missionView = useInView(0.1) - const partnersView = useInView(0.1) - const execView = useInView(0.1) - const deptView = useInView(0.1) - const advView = useInView(0.1) - - return ( -
- - WHO WE -
- ARE - - } - description="A student-led community at the heart of Munich's startup scene — turning bold ideas into real ventures since 2003." - > -
- -
- {animatedYears} - + -
-

Years Active

-
- -
- {animatedMembers} - + -
-

Members

-
-
-
- - {/* ═══ 01 VISION & MISSION — bold editorial ═══ */} -
-
-
-
- Our Purpose -

- VISION
& MISSION -

-
-
- -
-
-
-
- - - -
- Mission -
-

- Empowering
founders of
tomorrow. -

-

- A self-driven community where learning happens by doing — and every member gets what is needed to build things that truly matters. -

-
- -
-
-
- - - - -
- Vision -
-

- Being the launchpad for innovation -

-

- We envision START as the place for ambitious students in Munich, where ideas are turned into real innovations. -

-
-
-
-
- - {/* ═══ 02 MISSION PARTNERS ═══ */} -
-
-
- Mission Partners -

Organisations who share our mission and support us in building Munich's next generation of founders.

-
- -
- {missionPartners.map((partner, i) => ( -
-
- {partner.image2 ? ( -
- {partner.name} -
- {partner.name} -
- ) : ( - {partner.name} - )} -
-
-
- {partner.image2 ? ( - <> -
- CDTM -
-
- Manage and More -
- - ) : ( -
- {partner.logo - ? {partner.name} - : {partner.name.charAt(0)} - } -
- )} -

{partner.name}

-
-

- {'descriptionParts' in partner - ? partner.descriptionParts?.map((part, i) => ( - {part.text} - )) - : partner.description} -

-
-
- ))} -
-
-
- - {/* Divider */} -
-
-
- - - {/* ═══ 03 EXECUTIVE BOARD — side-by-side layout ═══ */} -
-
- -
-
- {/* Left: heading */} -
- Meet the Team -

- THE
EXECUTIVE
BOARD -

-
- - {/* Right: portrait cards */} -
- {executiveBoard.map((member, i) => ( - -
- {member.name} -
-

{member.name}

-

{member.role}

-
- ))} -
-
-
-
- - {/* ═══ 04 DEPARTMENT BOARD — compact row with circles ═══ */} -
-
-
-
-
- The Department Board -
- -
- {departmentBoard.map((member, i) => ( - -
- {member.name} -
-

{member.name}

-

{member.role}

-
- ))} -
-
-
-
- - {/* ═══ 05 ADVISORY BOARD — thumbnail grid with expandable detail ═══ */} - {showAdvisoryBoard && ( -
-
-
-
- Advisory Board -
- Seasoned entrepreneurs and industry leaders who sharpen our direction. -
- - {/* 7-column thumbnail grid */} -
- {advisoryBoard.map((member, i) => ( - - ))} -
- - {/* Expandable detail panel */} -
- {selectedAdvisor !== null && ( -
-
- {advisoryBoard[selectedAdvisor].name} -
-
-
-
-

{advisoryBoard[selectedAdvisor].name}

-

{advisoryBoard[selectedAdvisor].role}

-
- -
-

{advisoryBoard[selectedAdvisor].bio}

-
-
- )} -
-
-
- )} -
- ) -} \ No newline at end of file + return +} diff --git a/app/api/members/batch/[batchId]/route.ts b/app/api/members/batch/[batchId]/route.ts index 79f8689..f695891 100644 --- a/app/api/members/batch/[batchId]/route.ts +++ b/app/api/members/batch/[batchId]/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from 'next/server' -import { getMembersByBatch } from '@/lib/mockMembers' interface ExternalMember { id: number @@ -81,8 +80,7 @@ export async function GET( if (!response.ok) { console.error(`API error: ${response.status} ${response.statusText}`) - const fallbackMembers = getMembersByBatch(batchId) - return NextResponse.json(fallbackMembers) + return NextResponse.json([]) } const data = await response.json() @@ -91,10 +89,7 @@ export async function GET( : data.members || data.data || [] if (!Array.isArray(dataMembers) || dataMembers.length === 0) { - const fallbackMembers = getMembersByBatch(batchId) - if (fallbackMembers.length > 0) { - return NextResponse.json(fallbackMembers) - } + return NextResponse.json([]) } // Helper to pick the best image field diff --git a/app/api/members/route.ts b/app/api/members/route.ts index 06ce848..b96a1b4 100644 --- a/app/api/members/route.ts +++ b/app/api/members/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from 'next/server' -import { mockMembers } from '@/lib/mockMembers' export const revalidate = 3600; @@ -63,8 +62,8 @@ export async function GET() { // If members table is not configured, return mock data if (!NOCODB_API_TOKEN || !NOCODB_MEMBERS_TABLE_ID) { - console.log('Members table not configured in NocoDB, using mock data'); - return NextResponse.json(mockMembers); + console.log('Members table not configured in NocoDB'); + return NextResponse.json([]); } try { diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..f6496c6 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,22 @@ +"use client" + +export default function Error({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+
+

Something went wrong

+ +
+
+ ) +} diff --git a/app/events/EventsContent.tsx b/app/events/EventsContent.tsx new file mode 100644 index 0000000..dc0fd5d --- /dev/null +++ b/app/events/EventsContent.tsx @@ -0,0 +1,844 @@ +"use client" + +import React, { useState, useEffect, useRef } from "react" +import Link from "next/link" +import Script from "next/script" +import { useRouter } from "next/navigation" + +import { EventCard, TimelineMarker, ScrollIndicator } from "@/components/EventComponents" +import Hero from "@/components/Hero" +import HeroCard from "@/components/HeroCard" +import PastEventsGrid from "./PastEventsGrid" +import UpcomingEventsGrid from "./UpcomingEventsGrid" +import { useAnimatedNumber } from "@/lib/useAnimatedNumber" + +export const dynamic = 'force-dynamic' + +interface RecurringEvent { + id: string + name: string + description: string + month: string + frequency: string + icon: string + image: string + category: string +} + +const recurringEvents: RecurringEvent[] = [ + { + id: "rtss", + name: "Road to START Summit (RTSS)", + description: "Our flagship pitch event where aspiring founders present their startup ideas to a panel of investors, entrepreneurs, and industry experts.", + month: "October", + frequency: "Once per year", + icon: "presentation", + image: "/events/eventCards/summit-opt.jpg", + category: "Pitch Event" + }, + { + id: "rtsh", + name: "Road to START Hack (RTSH)", + description: "An intensive hackathon bringing together developers, designers, and entrepreneurs to build innovative solutions in 24-48 hours.", + month: "November", + frequency: "Once per year", + icon: "code", + image: "/events/eventCards/hack-opt.jpg", + category: "Hackathon" + }, + { + id: "legal-hack", + name: "Munich Hacking Legal", + description: "A unique hackathon focused on building legal tech solutions that address real challenges in the legal industry, combining technology with regulatory expertise.", + month: "March", + frequency: "Once per year", + icon: "code", + image: "/events/eventCards/legal-opt.jpg", + category: "Hackathon" + }, + { + id: "start-labs", + name: "START Labs", + description: "A hands-on program where students work on real-world challenges from industry partners, developing prototypes and solutions across various tech verticals like MedTech, FinTech, and more.", + month: "May", + frequency: "Once per year", + icon: "code", + image: "/events/eventCards/labs-opt.jpg", + category: "Incubator" + }, + { + id: "info-event", + name: "Info Event", + description: "Join us at the start of each semester to learn about START Munich, meet our community, and discover how you can get involved.", + month: "October & April", + frequency: "Once per semester", + icon: "info", + image: "/events/eventCards/info-opt.jpg", + category: "Talk" + }, + { + id: "fail-tales", + name: "Founder Fail Tales", + description: "Real stories from real founders about their biggest failures and lessons learned.", + month: "October & April", + frequency: "Once per semester", + icon: "stories", + image: "/events/eventCards/fail-opt.jpg", + category: "Talk" + }, + { + id: "pitch-network", + name: "PITCH & NETWORK", + description: "Practice your pitch, get feedback from experienced entrepreneurs, and network with fellow founders in an intimate setting.", + month: "January & June", + frequency: "Once per semester", + icon: "presentation", + image: "/events/eventCards/pitch-opt.jpg", + category: "Pitch Event" + } +] + +export default function EventsPage() { + const router = useRouter() + const [loading, setLoading] = useState(true) + const sliderRef = useRef(null) + const sliderSectionRef = useRef(null) + const dragState = useRef({ isDragging: false, startX: 0, scrollLeft: 0 }) + const [scrollProgress, setScrollProgress] = useState(0) + const [hoveredEvent, setHoveredEvent] = useState(null) + + useEffect(() => { + setLoading(false) + }, []) + + // Use animated number hook for statistics (faster animation - 800ms) + const animatedHackathons = useAnimatedNumber(4, loading, 800) + const animatedPublicEvents = useAnimatedNumber(10, loading, 800) + + useEffect(() => { + const slider = sliderRef.current + if (!slider || loading) return + + const updateScroll = () => { + const maxScroll = slider.scrollWidth - slider.clientWidth + const progress = maxScroll > 0 ? (slider.scrollLeft / maxScroll) * 100 : 0 + setScrollProgress(progress) + } + + slider.addEventListener('scroll', updateScroll) + updateScroll() + setTimeout(updateScroll, 100) + + return () => slider.removeEventListener('scroll', updateScroll) + }, [loading]) + + const handleDrag = { + start: (e: React.MouseEvent) => { + const slider = sliderRef.current + if (!slider) return + dragState.current = { + isDragging: true, + startX: e.pageX - slider.offsetLeft, + scrollLeft: slider.scrollLeft + } + }, + move: (e: React.MouseEvent) => { + if (!dragState.current.isDragging || !sliderRef.current) return + e.preventDefault() + const x = e.pageX - sliderRef.current.offsetLeft + sliderRef.current.scrollLeft = dragState.current.scrollLeft - (x - dragState.current.startX) * 2 + }, + end: () => { + dragState.current.isDragging = false + } + } + + const scrollToEvent = (eventId: string) => { + const slider = sliderRef.current + if (!slider) return + + const card = slider.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement | null + if (!card) return + + const cardLeft = card.offsetLeft + const cardWidth = card.offsetWidth + const sliderWidth = slider.offsetWidth + + slider.scrollTo({ + left: cardLeft - (sliderWidth / 2) + (cardWidth / 2), + behavior: 'smooth' + }) + } + + const scrollToEventMobile = (eventId: string) => { + scrollToEvent(eventId) + sliderSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + + const handleTimelineMarkerHover = (eventId: string) => { + setHoveredEvent(eventId) + scrollToEvent(eventId) + } + + const calculateTimelinePosition = (month: number, day: number = 1): string => { + // month: 1-12, day: 1-20 (assuming 20 days per month for positioning) + // Each month takes up 8.33% of the timeline (100% / 12) + // Within a month, each day takes up 8.33% / 20 = 0.4165% + const monthProgress = (month - 1) / 12 // 0 to 11/12 + const dayProgress = (day) / 30 / 12 // 0 to 19/20/12 + const totalProgress = (monthProgress + dayProgress) * 100 + return `${totalProgress.toFixed(2)}%` + } + + const getIconSvg = (icon: string) => { + switch (icon) { + case "trophy": + return ( + + + + ) + case "code": + return ( + + + + ) + case "info": + return ( + + + + ) + case "stories": + return ( + + + + ) + case "presentation": + return ( + + + + ) + default: + return null + } + } + + if (loading) { + return ( +
+
+

Loading events...

+
+
+ ) + } + + return ( + <> + + +
+ {/* Hero Section */} + + START MUNICH +
+ EVENTS + + } + description="Connect, learn, and grow with Munich's most vibrant student entrepreneur community through our curated events" + > + {/* Statistics Boxes - Matching Startup Cards Style */} +
+ {/** Stat 1 **/} + +
+ + {Math.floor(animatedHackathons)} + + + +
+

Hackathons Yearly

+
+ + {/** Stat 2 **/} + +
+ + {Math.floor(animatedPublicEvents)} + + + +
+

Public Events Yearly

+
+
+
+ + {/* Content Below Hero */} +
+ + {/* Featured Event Spotlight */} +
+
+ {/* Background image */} +
+ Munich Hacking Legal +
+
+ + {/* Content */} +
+
+ {/* Badges */} +
+ + + Upcoming Event + + + Hackathon + +
+ +

+ Munich
+ Hacking Legal +

+ +

+ A unique hackathon focused on building legal tech solutions that address real challenges in the legal industry, combining technology with regulatory expertise. +

+ +
+ + Learn More + + + + +
+ + + + This April +
+
+
+
+
+
+ + {/* Upcoming Events Calendar Section */} +
+
+ {/* Title and description */} +
+ What's Next +

+ UPCOMING EVENTS +

+

+ Stay updated with all our latest events and register to join us. +

+
+ + +
+ + +
+ + {/* Recurring Events Section */} +
+
+ Annual Calendar +

+ OUR RECURRING EVENTS +

+

+ Mark your calendars. These flagship events happen every year. +

+
+ + {/* Timeline Visualization */} +
+ + {/* Desktop Timeline */} +
+ {/* Months */} +
+ {['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].map((month, i) => ( +
+ {month} +
+ ))} +
+ + {/* Timeline Line */} +
+
+ + {/* Month Dividers */} + {[...Array(12)].map((_, i) => ( +
+ ))} + + {/* Event Markers - Using TimelineMarker Components */} + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> + + setHoveredEvent(null)} + /> +
+ + {/* Desktop Legend */} +
+
+
+ Pitch Events +
+
+
+ Hackathons +
+
+
+ Incubator +
+
+
+ Talks +
+
+
+ + {/* Mobile Timeline - Simplified List View */} +
+
+
+
Jan
+ +
+ +
+
Mar
+ +
+ +
+
Apr
+
+ + +
+
+ +
+
May
+ +
+ +
+
Jun
+ +
+ +
+
Oct
+
+ + + +
+
+ +
+
Nov
+ +
+
+ + {/* Mobile Legend */} +
+
+
+ Pitch Events +
+
+
+ Hackathons +
+
+
+ Incubator +
+
+
+ Talks +
+
+
+
+ +
+
+ + {/* Events Slider */} +
+
+
+ {/* Main Events Group */} +
+ Main Events +
+ {recurringEvents.filter(e => ['rtss', 'rtsh', 'legal-hack', 'start-labs'].includes(e.id)).map((event, index) => ( + window.open('https://www.hacking-legal.org/', '_blank') + : event.id === 'rtsh' ? () => window.open('https://hack.startmunich.de/events/rtsh', '_blank') + : event.id === 'rtss' ? () => window.open('https://summit.startmunich.de/events/rtss', '_blank') + : event.id === 'start-labs' ? () => window.open('https://www.startmunich.de', '_blank') + : undefined + } + /> + ))} +
+
+ + {/* Divider */} +
+
+
+ + {/* Side Events Group */} +
+ Side Events +
+ {recurringEvents.filter(e => !['rtss', 'rtsh', 'legal-hack', 'start-labs'].includes(e.id)).map((event, index) => ( + + ))} +
+
+
+ + + + {/* Gradient Fade Edges */} +
+
+
+ +
+ + {/* Member Exclusive Events Section */} +
+
+ {/* Decorative Elements */} +
+
+ +
+
+ {/* Left Side - Content */} +
+
+ + + + Members Only +
+ +

+ HUNGRY FOR MORE? + {/* Exclusive Member Events */} +

+ +

+ As a START Munich member, you get access to exclusive events including private dinners with successful founders, closed-door workshops with industry experts, peer feedback sessions, and intimate networking gatherings. These events are designed to provide maximum value and foster deep connections within our community. +

+
+ + {/* Right Side - CTA */} +
+ + + Our Members Journey + + + + +
+
+
+
+
+ + + {/* Past Events Calendar Section */} +
+
+ Looking Back +

+ PAST EVENTS +

+

+ Check out the amazing events we've hosted in the past. +

+
+ + +
+ +
+
+ + ) +} diff --git a/components/PastEventsGrid.tsx b/app/events/PastEventsGrid.tsx similarity index 100% rename from components/PastEventsGrid.tsx rename to app/events/PastEventsGrid.tsx diff --git a/components/UpcomingEventsGrid.tsx b/app/events/UpcomingEventsGrid.tsx similarity index 100% rename from components/UpcomingEventsGrid.tsx rename to app/events/UpcomingEventsGrid.tsx diff --git a/app/events/page.tsx b/app/events/page.tsx index a288177..c29d37f 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -1,844 +1,21 @@ -"use client" - -import React, { useState, useEffect, useRef } from "react" -import Link from "next/link" -import Script from "next/script" -import { useRouter } from "next/navigation" - -import { EventCard, TimelineMarker, ScrollIndicator } from "@/components/EventComponents" -import Hero from "@/components/Hero" -import HeroCard from "@/components/HeroCard" -import PastEventsGrid from "@/components/PastEventsGrid" -import UpcomingEventsGrid from "@/components/UpcomingEventsGrid" -import { useAnimatedNumber } from "@/lib/useAnimatedNumber" +import type { Metadata } from 'next' +import EventsContent from './EventsContent' export const dynamic = 'force-dynamic' -interface RecurringEvent { - id: string - name: string - description: string - month: string - frequency: string - icon: string - image: string - category: string -} - -const recurringEvents: RecurringEvent[] = [ - { - id: "rtss", - name: "Road to START Summit (RTSS)", - description: "Our flagship pitch event where aspiring founders present their startup ideas to a panel of investors, entrepreneurs, and industry experts.", - month: "October", - frequency: "Once per year", - icon: "presentation", - image: "/events/eventCards/summit-opt.jpg", - category: "Pitch Event" - }, - { - id: "rtsh", - name: "Road to START Hack (RTSH)", - description: "An intensive hackathon bringing together developers, designers, and entrepreneurs to build innovative solutions in 24-48 hours.", - month: "November", - frequency: "Once per year", - icon: "code", - image: "/events/eventCards/hack-opt.jpg", - category: "Hackathon" +export const metadata: Metadata = { + title: 'Events', + description: + 'Discover START Munich events — hackathons, pitch competitions, startup labs, info sessions, and more. Connect with Munich\'s student entrepreneur community.', + alternates: { canonical: 'https://www.startmunich.de/events' }, + openGraph: { + url: 'https://www.startmunich.de/events', + title: 'Events | START Munich', + description: + 'Discover START Munich events — hackathons, pitch competitions, startup labs, info sessions, and more. Connect with Munich\'s student entrepreneur community.', }, - { - id: "legal-hack", - name: "Munich Hacking Legal", - description: "A unique hackathon focused on building legal tech solutions that address real challenges in the legal industry, combining technology with regulatory expertise.", - month: "March", - frequency: "Once per year", - icon: "code", - image: "/events/eventCards/legal-opt.jpg", - category: "Hackathon" - }, - { - id: "start-labs", - name: "START Labs", - description: "A hands-on program where students work on real-world challenges from industry partners, developing prototypes and solutions across various tech verticals like MedTech, FinTech, and more.", - month: "May", - frequency: "Once per year", - icon: "code", - image: "/events/eventCards/labs-opt.jpg", - category: "Incubator" - }, - { - id: "info-event", - name: "Info Event", - description: "Join us at the start of each semester to learn about START Munich, meet our community, and discover how you can get involved.", - month: "October & April", - frequency: "Once per semester", - icon: "info", - image: "/events/eventCards/info-opt.jpg", - category: "Talk" - }, - { - id: "fail-tales", - name: "Founder Fail Tales", - description: "Real stories from real founders about their biggest failures and lessons learned.", - month: "October & April", - frequency: "Once per semester", - icon: "stories", - image: "/events/eventCards/fail-opt.jpg", - category: "Talk" - }, - { - id: "pitch-network", - name: "PITCH & NETWORK", - description: "Practice your pitch, get feedback from experienced entrepreneurs, and network with fellow founders in an intimate setting.", - month: "January & June", - frequency: "Once per semester", - icon: "presentation", - image: "/events/eventCards/pitch-opt.jpg", - category: "Pitch Event" - } -] +} export default function EventsPage() { - const router = useRouter() - const [loading, setLoading] = useState(true) - const sliderRef = useRef(null) - const sliderSectionRef = useRef(null) - const dragState = useRef({ isDragging: false, startX: 0, scrollLeft: 0 }) - const [scrollProgress, setScrollProgress] = useState(0) - const [hoveredEvent, setHoveredEvent] = useState(null) - - useEffect(() => { - setLoading(false) - }, []) - - // Use animated number hook for statistics (faster animation - 800ms) - const animatedHackathons = useAnimatedNumber(4, loading, 800) - const animatedPublicEvents = useAnimatedNumber(10, loading, 800) - - useEffect(() => { - const slider = sliderRef.current - if (!slider || loading) return - - const updateScroll = () => { - const maxScroll = slider.scrollWidth - slider.clientWidth - const progress = maxScroll > 0 ? (slider.scrollLeft / maxScroll) * 100 : 0 - setScrollProgress(progress) - } - - slider.addEventListener('scroll', updateScroll) - updateScroll() - setTimeout(updateScroll, 100) - - return () => slider.removeEventListener('scroll', updateScroll) - }, [loading]) - - const handleDrag = { - start: (e: React.MouseEvent) => { - const slider = sliderRef.current - if (!slider) return - dragState.current = { - isDragging: true, - startX: e.pageX - slider.offsetLeft, - scrollLeft: slider.scrollLeft - } - }, - move: (e: React.MouseEvent) => { - if (!dragState.current.isDragging || !sliderRef.current) return - e.preventDefault() - const x = e.pageX - sliderRef.current.offsetLeft - sliderRef.current.scrollLeft = dragState.current.scrollLeft - (x - dragState.current.startX) * 2 - }, - end: () => { - dragState.current.isDragging = false - } - } - - const scrollToEvent = (eventId: string) => { - const slider = sliderRef.current - if (!slider) return - - const card = slider.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement | null - if (!card) return - - const cardLeft = card.offsetLeft - const cardWidth = card.offsetWidth - const sliderWidth = slider.offsetWidth - - slider.scrollTo({ - left: cardLeft - (sliderWidth / 2) + (cardWidth / 2), - behavior: 'smooth' - }) - } - - const scrollToEventMobile = (eventId: string) => { - scrollToEvent(eventId) - sliderSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) - } - - const handleTimelineMarkerHover = (eventId: string) => { - setHoveredEvent(eventId) - scrollToEvent(eventId) - } - - const calculateTimelinePosition = (month: number, day: number = 1): string => { - // month: 1-12, day: 1-20 (assuming 20 days per month for positioning) - // Each month takes up 8.33% of the timeline (100% / 12) - // Within a month, each day takes up 8.33% / 20 = 0.4165% - const monthProgress = (month - 1) / 12 // 0 to 11/12 - const dayProgress = (day) / 30 / 12 // 0 to 19/20/12 - const totalProgress = (monthProgress + dayProgress) * 100 - return `${totalProgress.toFixed(2)}%` - } - - const getIconSvg = (icon: string) => { - switch (icon) { - case "trophy": - return ( - - - - ) - case "code": - return ( - - - - ) - case "info": - return ( - - - - ) - case "stories": - return ( - - - - ) - case "presentation": - return ( - - - - ) - default: - return null - } - } - - if (loading) { - return ( -
-
-

Loading events...

-
-
- ) - } - - return ( - <> - - -
- {/* Hero Section */} - - START MUNICH -
- EVENTS - - } - description="Connect, learn, and grow with Munich's most vibrant student entrepreneur community through our curated events" - > - {/* Statistics Boxes - Matching Startup Cards Style */} -
- {/** Stat 1 **/} - -
- - {Math.floor(animatedHackathons)} - - + -
-

Hackathons Yearly

-
- - {/** Stat 2 **/} - -
- - {Math.floor(animatedPublicEvents)} - - + -
-

Public Events Yearly

-
-
-
- - {/* Content Below Hero */} -
- - {/* Featured Event Spotlight */} -
-
- {/* Background image */} -
- Munich Hacking Legal -
-
- - {/* Content */} -
-
- {/* Badges */} -
- - - Upcoming Event - - - Hackathon - -
- -

- Munich
- Hacking Legal -

- -

- A unique hackathon focused on building legal tech solutions that address real challenges in the legal industry, combining technology with regulatory expertise. -

- -
- - Learn More - - - - -
- - - - This April -
-
-
-
-
-
- - {/* Upcoming Events Calendar Section */} -
-
- {/* Title and description */} -
- What's Next -

- UPCOMING EVENTS -

-

- Stay updated with all our latest events and register to join us. -

-
- - -
- - -
- - {/* Recurring Events Section */} -
-
- Annual Calendar -

- OUR RECURRING EVENTS -

-

- Mark your calendars. These flagship events happen every year. -

-
- - {/* Timeline Visualization */} -
- - {/* Desktop Timeline */} -
- {/* Months */} -
- {['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].map((month, i) => ( -
- {month} -
- ))} -
- - {/* Timeline Line */} -
-
- - {/* Month Dividers */} - {[...Array(12)].map((_, i) => ( -
- ))} - - {/* Event Markers - Using TimelineMarker Components */} - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> - - setHoveredEvent(null)} - /> -
- - {/* Desktop Legend */} -
-
-
- Pitch Events -
-
-
- Hackathons -
-
-
- Incubator -
-
-
- Talks -
-
-
- - {/* Mobile Timeline - Simplified List View */} -
-
-
-
Jan
- -
- -
-
Mar
- -
- -
-
Apr
- -
- -
-
May
-
- - -
-
- -
-
Jun
- -
- -
-
Oct
-
- - - -
-
- -
-
Nov
- -
-
- - {/* Mobile Legend */} -
-
-
- Pitch Events -
-
-
- Hackathons -
-
-
- Incubator -
-
-
- Talks -
-
-
-
- -
-
- - {/* Events Slider */} -
-
-
- {/* Main Events Group */} -
- Main Events -
- {recurringEvents.filter(e => ['rtss', 'rtsh', 'legal-hack', 'start-labs'].includes(e.id)).map((event, index) => ( - window.open('https://www.hacking-legal.org/', '_blank') - : event.id === 'rtsh' ? () => window.open('https://hack.startmunich.de/events/rtsh', '_blank') - : event.id === 'rtss' ? () => window.open('https://summit.startmunich.de/events/rtss', '_blank') - : event.id === 'start-labs' ? () => window.open('https://www.startmunich.de', '_blank') - : undefined - } - /> - ))} -
-
- - {/* Divider */} -
-
-
- - {/* Side Events Group */} -
- Side Events -
- {recurringEvents.filter(e => !['rtss', 'rtsh', 'legal-hack', 'start-labs'].includes(e.id)).map((event, index) => ( - - ))} -
-
-
- - - - {/* Gradient Fade Edges */} -
-
-
- -
- - {/* Member Exclusive Events Section */} -
-
- {/* Decorative Elements */} -
-
- -
-
- {/* Left Side - Content */} -
-
- - - - Members Only -
- -

- HUNGRY FOR MORE? - {/* Exclusive Member Events */} -

- -

- As a START Munich member, you get access to exclusive events including private dinners with successful founders, closed-door workshops with industry experts, peer feedback sessions, and intimate networking gatherings. These events are designed to provide maximum value and foster deep connections within our community. -

-
- - {/* Right Side - CTA */} -
- - - Our Members Journey - - - - -
-
-
-
-
- - - {/* Past Events Calendar Section */} -
-
- Looking Back -

- PAST EVENTS -

-

- Check out the amazing events we've hosted in the past. -

-
- - -
- -
-
- - ) + return } diff --git a/app/for-partners/page.tsx b/app/for-partners/page.tsx index 03e0956..1fb546c 100644 --- a/app/for-partners/page.tsx +++ b/app/for-partners/page.tsx @@ -1,10 +1,24 @@ import Image from 'next/image' import Script from 'next/script' +import type { Metadata } from 'next' import Hero from "@/components/Hero" import HeroCard from "@/components/HeroCard" import PhotoGallery from './PhotoGallery' import { getAllPartners } from '@/lib/partners' +export const metadata: Metadata = { + title: 'Partner With Us', + description: + 'Partner with START Munich to reach top talent from TUM, LMU, and HM. Sponsor events, host workshops, and recruit Munich\'s most ambitious student entrepreneurs.', + alternates: { canonical: 'https://www.startmunich.de/for-partners' }, + openGraph: { + url: 'https://www.startmunich.de/for-partners', + title: 'Partner With Us | START Munich', + description: + 'Partner with START Munich to reach top talent from TUM, LMU, and HM. Sponsor events, host workshops, and recruit Munich\'s most ambitious student entrepreneurs.', + }, +} + export const revalidate = 3600 const faqs = [ @@ -102,8 +116,30 @@ const whyStartSpecial = [ export default async function ForPartnersPage() { const partners = await getAllPartners() + // Plain-text FAQ answers for JSON-LD (faq1 has JSX, so we provide a text fallback) + const faqPlainText: Record = { + faq1: 'We can move fast and sometimes launch within days. Please check our event calendar first. If you want to join a specific event, contact us as early as possible so we can plan it with you.', + } + + const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: typeof faq.answer === 'string' ? faq.answer : faqPlainText[faq.id] ?? '', + }, + })), + } + return ( <> + + +
+ {/* Hero Section */} + + YOUR START MUNICH +
+ JOURNEY + + } + description="Become a member and spend at least two active semesters contributing to the community" + > +
+ +
+ + {semesterCount} + + + +
+

Semesters

+
+ + +
+ + ∞ + +
+

Possibilities

+
+
+
+ + {/* ═══ MEMBER JOURNEY TIMELINE ═══ */} +
+
+ Your Path +

+ MEMBER JOURNEY +

+

+ The 5 milestones of your first two semesters at START Munich +

+
+ +
+
{ + isDraggingTimeline.current = true + dragStartX.current = e.pageX - (timelineSliderRef.current?.offsetLeft ?? 0) + dragStartScrollLeft.current = timelineSliderRef.current?.scrollLeft ?? 0 + }} + onMouseMove={(e) => { + if (!isDraggingTimeline.current || !timelineSliderRef.current) return + e.preventDefault() + const x = e.pageX - (timelineSliderRef.current.offsetLeft ?? 0) + timelineSliderRef.current.scrollLeft = dragStartScrollLeft.current - (x - dragStartX.current) + }} + onMouseUp={() => { isDraggingTimeline.current = false }} + onMouseLeave={() => { isDraggingTimeline.current = false }} + > +
+ {timelineEvents.map((event, index) => ( +
+
+ {/* Pink accent top bar */} +
+ +
+ {/* Step number + divider */} +
+ + 0{index + 1} + +
+ {event.icon} +
+ +

+ {event.title} +

+ +

+ {event.description} +

+ +
+ {(event.details as string[]).map((detail, i) => ( +
+
+ {detail} +
+ ))} +
+
+
+
+ ))} +
+
+ +
+ +
+
+
+ + {/* ═══ MAIN CONTENT ═══ */} +
+ + {/* ═══ DEPARTMENTS ═══ */} +
+
+ Where You Fit +

+ OUR DEPARTMENTS +

+

+ Choose your department and contribute to our community +

+
+ + {/* Desktop: arc layout */} + {(() => { + // Left-side half-circle "(" : center=(300,300), r=300 + // Arc from (300,0) at top, curving LEFT through (0,300), to (300,600) at bottom + const cx = 300, cy = 300, r = 300 + const toRad = (deg: number) => (deg * Math.PI) / 180 + // Standard-math angles (CCW from +x): 108°→top-left … 252°→bottom-left + const angles = [108, 144, 180, 216, 252] + // Per-dot label vertical nudge (positive = lower, negative = higher) + const labelYOffsets = [18, 6, 6, 6, -8] + const dots = departments.map((dept, i) => { + const a = toRad(angles[i]) + return { + dept, + x: cx + r * Math.cos(a), + y: cy - r * Math.sin(a), + labelYOffset: labelYOffsets[i], + } + }) + const activeDept = departments.find(d => d.id === activeDeptId) + + return ( + <> + {/* Desktop */} +
+ {/* Detail panel — left */} +
+ {activeDept && ( +
+
+ {activeDept.icon} +
+

{activeDept.name.toUpperCase()}

+

{activeDept.description}

+
+

Responsibilities

+
+ {activeDept.responsibilities.map((resp, i) => ( +
+
+ {resp} +
+ ))} +
+
+
+ )} +
+ + {/* Arc — right */} +
+ + {/* Always-pink arc: (300,0) CCW through (0,300) to (300,600) */} + + + {/* Dots + labels */} + {dots.map(({ dept, x, y, labelYOffset }) => { + const isActive = activeDeptId === dept.id + return ( + setActiveDeptId(dept.id)} + className="cursor-pointer" + style={{ transition: 'all 0.3s' }} + > + {isActive && ( + + )} + + {isActive && } + {/* Label — to the right of dot, toward the opening */} + + {dept.name.toUpperCase()} + + + ) + })} + +
+
+ + {/* Mobile: icon strip + card */} +
+
+ {departments.map((dept) => ( + + ))} +
+ {activeDept && ( +
+

{activeDept.name.toUpperCase()}

+

{activeDept.description}

+
+

Responsibilities

+ {activeDept.responsibilities.map((resp, i) => ( +
+
+ {resp} +
+ ))} +
+
+ )} +
+ + ) + })()} +
+ + {/* ═══ INTERNAL EVENTS ═══ */} +
+
+ Community Life +

+ INTERNAL EVENTS +

+

+ Regular events and activities of our community +

+
+ +
+ {/* Event list card */} +
{ + if (!lockedEventId && !isMoreLocked) { + setHoveredEventId(null) + setIsMoreHovered(false) + } + }} + > +
+ {startEvents.map((event, index) => { + const isActive = activeEventId === event.id || (!activeEventId && !activeMore && currentEventIndex === index) + return ( +
{ if (!lockedEventId && !isMoreLocked) setHoveredEventId(event.id) }} + onMouseLeave={() => { if (!lockedEventId && !isMoreLocked) setHoveredEventId(null) }} + onClick={() => { setLockedEventId(event.id); setIsMoreLocked(false); setIsMoreHovered(false); setHoveredEventId(null); scrollToEventImage() }} + > +
+ {event.icon} +
+
+
+

{event.title}

+ {event.frequency && ( + {event.frequency} + )} +
+

+ {event.description} +

+
+
+ ) + })} + + {/* And a lot more */} + {(() => { + const isMoreActive = activeMore || (!activeEventId && !activeMore && currentEventIndex === startEvents.length) + return ( +
{ if (!lockedEventId && !isMoreLocked) setIsMoreHovered(true) }} + onMouseLeave={() => { if (!lockedEventId && !isMoreLocked) setIsMoreHovered(false) }} + onClick={() => { setIsMoreLocked(true); setLockedEventId(null); setHoveredEventId(null); setIsMoreHovered(false); scrollToEventImage() }} + > +
+ +
+
+

And a lot more...

+

+ Discover many more exciting events and opportunities. +

+
+
+ ) + })()} +
+
+ + {/* Image panel */} +
+ {activeMore || (!activeEventId && !activeMore && currentEventIndex === startEvents.length) ? ( +
+ {moreImages.map((img, i) => ( +
+ {`More +
+
+ ))} +
+
+

AND MORE...

+
+
+
+ ) : (() => { + const images = activeEventId && eventImages.length > 0 + ? eventImages + : currentEventImages.length > 0 + ? currentEventImages + : null + if (!images) return null + const idx = eventImageIndex % images.length + return ( +
+ {images[idx]?.title} +
+

{images[idx]?.title}

+
+
+ + +
+
+ {idx + 1} / {images.length} +
+
+ ) + })()} +
+
+
+ + {/* ═══ COMMUNITY SPECIALS ═══ */} +
+
+ Exclusive Access +

+ COMMUNITY SPECIALS +

+

+ Unique opportunities exclusively for START Munich members +

+
+ +
+ {/* Bay Area */} + +
+ San Francisco Bay Area +
+
+
+

+ START goes Bay Area +

+

+ Once a year, 20 selected STARTies go on a two-week trip to the San Francisco Bay Area to experience one of the world's most vibrant startup ecosystems firsthand. Through curated visits with partner companies, research labs, and VC firms, participants strengthen our ties with international founders, researchers, and investors. +

+ + Learn more + + + + +
+
+ + {/* Cambridge */} +
+
+ University Research +
+
+
+

+ Research Stay @ Cambridge +

+

+ Spend a research stay with our partners at the University of Cambridge and Technical University of Munich. Gain access to world-class academic resources, mentorship from leading researchers, and the opportunity to contribute to cutting-edge research. +

+
+ + + + + Cambridge + + + + + + TUM + +
+
+
+
+
+ + {/* Member Stories */} + + MEMBER STORIES + } + description="Real stories from our members who built successful startups with START Munich" + items={memberStories.map(story => ({ + id: story.id, + name: story.name, + role: story.role, + company: story.company, + image: story.image, + story: story.story, + logos: story.logos + }))} + /> + + {/* CTA */} + + +
+
+ + + + ) +} \ No newline at end of file diff --git a/app/member-journey/page.tsx b/app/member-journey/page.tsx index 39c0502..fa9a481 100644 --- a/app/member-journey/page.tsx +++ b/app/member-journey/page.tsx @@ -1,970 +1,21 @@ -"use client" - -import { useState, useEffect, useRef } from 'react' -import Script from 'next/script' -import { ScrollIndicator } from '@/components/EventComponents' -import Hero from "@/components/Hero" -import HeroCard from "@/components/HeroCard" -import TestimonialsSection from '@/components/TestimonialsSection' -import CTA from "@/components/CTA" -import { useAnimatedNumber } from '@/lib/hooks' +import type { Metadata } from 'next' +import MemberJourneyContent from './MemberJourneyContent' export const dynamic = 'force-dynamic' -interface TimelineEvent { - id: string - title: string - description: string - icon: string - image: string | string[] - details: string[] -} - -interface Department { - id: string - name: string - description: string - icon: string - responsibilities: string[] -} - -interface StartEvent { - id: string - title: string - description: string - category: string - frequency: string - icon: string - images: string[] -} - -interface MemberStory { - id: string - name: string - role: string - company: string - image: string - story: string - department: string - logos?: { src: string; url?: string }[] -} - -const placeholderImage = "/internalevents-opt.png" - -const timelineEvents: TimelineEvent[] = [ - { - id: "application", - title: "Application", - description: "Your entry into START Munich.", - icon: "📝", - image: "", - details: ["Apply in April or October", "Stage 1: Written Application", "Stage 2: Two Interviews (same day, 30 minutes each)"] - }, - { - id: "start-sprint", - title: "START Sprint", - description: "Your first month at START. Get to know and bond with ambitious people you wouldn't meet in your usual circles, and build a real product together.", - icon: "🚀", - image: [ - "https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=800&auto=format&fit=crop", - "https://images.unsplash.com/photo-1531482615713-2afd69097998?q=80&w=800&auto=format&fit=crop" - ], - details: ["Develop your own idea from concept to MVP in just 4 weeks", "Two workshops per week to learn foundations of building a startup", "Get to know key players within Munich startup ecosystem", "Hut weekend in Austria"] - }, - { - id: "department-selection", - title: "Department Selection", - description: "Develop yourself and shape the future of START.", - icon: "🎯", - image: "", - details: ["Choose one of five departments", "Work on real projects with visible impact inside and outside START", "Learn useful startup skills in practice", "Grow fast by taking ownership", "Initiate new formats"] - }, - { - id: "active-member", - title: "Active Member", - description: "Enjoy the benefits of being a STARTie and expand your network through exclusive opportunities.", - icon: "🌍", - image: placeholderImage, - details: [ - "Join the Bay Area trip, 2 weeks, 20+ curated visits to top startups, VCs, and labs", - "Research Stay @ Cambridge through direct research collaboration", - "Become part of the START Network, 20+ chapters worldwide", - "Find co-founders or start your own venture within a community of 70+ startups, including teams backed by Y Combinator" - ] - }, - { - id: "alumni", - title: "START Alumni", - description: "Once a STARTie, always a STARTie. Stay connected as you build your own path.", - icon: "⭐", - image: placeholderImage, - details: ["Become alumni after two active semesters", "Find co-founders, investors, and collaborators across the START Global Network", "Give back by mentoring, sharing, and supporting new STARTies", "Stay involved as much as you want, department work is optional"] - } -] - -const departments: Department[] = [ - { - id: "people", - name: "People", - description: "Learn how to spot the right talent, keep them motivated, and build a community that accelerates your startup journey.", - icon: "👥", - responsibilities: ["Run recruiting, interviews, and onboarding.", "Organize START Sprint and shape new batches.", "Keep members engaged and connected through formats like Hut Weekend, START Goes Eating, and more."] - }, - { - id: "marketing", - name: "Marketing", - description: "Learn strategy and how to highlight START's people, events, and achievements to reach millions and push the ecosystem forward.", - icon: "📢", - responsibilities: ["Create content, posts, and campaigns for LinkedIn and Instagram.", "Shoot and edit photos and videos from events.", "Build and maintain START's brand and image."] - }, - { - id: "finops", - name: "FinOps", - description: "Learn how to design and build custom tools and automate workflows to expand START's output and influence.", - icon: "💰", - responsibilities: ["Build internal tools like Members Platform or Financial Dashboard.", "Improve our core systems like Slack, n8n and Notion.", "Handle contract management, ensuring compliance."] - }, - { - id: "partnerships", - name: "Partnerships", - description: "Learn how to secure partners, close deals, and bring in the resources that multiply START's impact across the ecosystem.", - icon: "🤝", - responsibilities: ["Run persistent outreach, handle rejection, and keep going.", "Build and manage relationships that create long-term value.", "Close deals that fund START's projects."] - }, - { - id: "events", - name: "Events", - description: "Learn how to run flagship events where people meet, learn, and build across START, the Munich ecosystem, and beyond.", - icon: "🎉", - responsibilities: ["Organize and run large-scale events with 300+ attendees, like hackathons and summits.", "Organize workshops, startup visits, and other learning opportunities.", "Create experiences people remember and come back for."] - } -] - -const startEvents: StartEvent[] = [ - { - id: "monthly", - title: "The Monthly", - description: "Every month all Munich STARTies meet and get updated on START events or pitch their startups. Location: MTZ", - category: "Meeting", - frequency: "Monthly", - icon: "📅", - images: [ - "/memberJourney/monthly/2-opt.jpg", - "/memberJourney/monthly/3-opt.png", - "/memberJourney/monthly/4-opt.png", - ] +export const metadata: Metadata = { + title: 'Member Journey', + description: + 'Discover the START Munich member journey — how you grow from applicant to founder within Europe\'s most vibrant student entrepreneurship community.', + alternates: { canonical: 'https://www.startmunich.de/member-journey' }, + openGraph: { + url: 'https://www.startmunich.de/member-journey', + title: 'Member Journey | START Munich', + description: + 'Discover the START Munich member journey — how you grow from applicant to founder within Europe\'s most vibrant student entrepreneurship community.', }, - { - id: "department-work", - title: "Department Work", - description: "Work with your department. Meet weekly at the MTZ.", - category: "Department", - frequency: "Weekly", - icon: "💼", - images: [ - "/memberJourney/departmentwork/2-opt.jpg", - "/memberJourney/departmentwork/1-opt.jpg" - - ] - }, - { - id: "builders-weekend", - title: "Builders Weekend", - description: "Meet on the weekend to build your own startup.", - category: "Building", - frequency: "Monthly", - icon: "🔨", - images: [ - "/memberJourney/builderWeekend/1-opt.jpg" - ] - }, - { - id: "workshops", - title: "Member Workshops", - description: "Workshops with VCs and other professionals.", - category: "Learning", - frequency: "", - icon: "🎓", - images: [ - "/memberJourney/memberworkshop/1-opt.jpg", - "/memberJourney/memberworkshop/3-opt.jpeg" - ] - }, - { - id: "startup-visits", - title: "Startup Visits", - description: "Visit startups and learn from the experienced.", - category: "Networking", - frequency: "", - icon: "🏢", - images: [ - "/memberJourney/startupVisit/1-opt.png", - "/memberJourney/startupVisit/2-opt.png", - ] - } -] - -const memberStories: MemberStory[] = [ - { - id: "story-1", - name: "Felix Haas", - role: "Founder & Investor", - company: "IDNow | Bits & Pretzels", - image: "/memberJourney/alumni/FelixHaas-opt.png", - story: "At START Munich, I laid the foundation for my current network. From this starting point, I built several companies, invested in more than 80 start-ups and helped set up Bits & Pretzels.", - department: "Alumni", - logos: [ - { src: "https://cdn.prod.website-files.com/65f98ea7c70b10b668ccbeb3/65f98ea7c70b10b668ccbeea_Vectors-Wrapper.svg", url: "https://www.idnow.io/" }, - { src: "https://cdn.prod.website-files.com/65f98ea7c70b10b668ccbeb3/65f98ea7c70b10b668ccbece_logo.svg", url: "https://www.bitsandpretzels.com/" } - ] - }, - { - id: "story-3", - name: "Elisabeth Goebel", - role: "Early Operator", - company: "ZeitAI | CDTM", - image: "/memberJourney/alumni/Elisa-opt.png", - story: "START is where things actually happen. I co-founded ISAR Unfiltered, met people who think and move the way I do, and built a network that directly led me to where I am today: Early Operator at a YC-backed AI startup.", - department: "People", - logos: [ - { src: "https://cdn.prod.website-files.com/6902359088cc8683c4db0171/69249d98617b1b96682cca65_44a5d2ba9e6004a1281eed9068c62a95_zeitai-logo-opt.png", url: "https://www.zeit.ai/" }, - ] - }, - { - id: "story-2", - name: "Joshua Cornelius", - role: "Co-Founder", - company: "Freeletics | CDTM", - image: "/memberJourney/alumni/JoshuaCornelius-opt.png", - story: "Before we founded Freeletics, START Munich - in addition to CDTM - gave my co-founder and me the ideal opportunity to make first contacts in the Munich startup scene.", - department: "Alumni", - logos: [ - { src: "https://cdn.prod.website-files.com/65f98ea7c70b10b668ccbeb3/65f98ea7c70b10b668ccbeef_5eb3c929c8c4590004435152-opt.png", url: "https://www.freeletics.com/" } - ] - } -] +} export default function MemberJourneyPage() { - const [loading, setLoading] = useState(true) - const [activeDeptId, setActiveDeptId] = useState(departments[0].id) - const [eventImageIndex, setEventImageIndex] = useState(0) - const [currentEventIndex, setCurrentEventIndex] = useState(0) - const timelineSliderRef = useRef(null) - const isDraggingTimeline = useRef(false) - const dragStartX = useRef(0) - const dragStartScrollLeft = useRef(0) - const [scrollProgress, setScrollProgress] = useState(0) - const [hoveredEventId, setHoveredEventId] = useState(null) - const [lockedEventId, setLockedEventId] = useState(null) - const [isMoreHovered, setIsMoreHovered] = useState(false) - const [isMoreLocked, setIsMoreLocked] = useState(false) - const autoRotateTimerRef = useRef(null) - const eventImageRef = useRef(null) - const eventsSectionRef = useRef(null) - - // Unlock selection when clicking outside the events section - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (eventsSectionRef.current && !eventsSectionRef.current.contains(e.target as Node)) { - setLockedEventId(null) - setIsMoreLocked(false) - } - } - document.addEventListener('click', handleClickOutside) - return () => document.removeEventListener('click', handleClickOutside) - }, []) - - // Animated counter for hero stats - const semesterCount = useAnimatedNumber(2, loading, 500) - - // Images for "And a lot more..." section - const moreImages = [ - "https://images.unsplash.com/photo-1528605105345-5344ea20e269?q=80&w=800&auto=format&fit=crop", - "https://images.unsplash.com/photo-1511578314322-379afb476865?q=80&w=800&auto=format&fit=crop", - "https://images.unsplash.com/photo-1505373877841-8d25f7d46678?q=80&w=800&auto=format&fit=crop", - "https://images.unsplash.com/photo-1475721027785-f74eccf877e2?q=80&w=800&auto=format&fit=crop" - ] - - const activeEventId = lockedEventId || hoveredEventId - const activeMore = isMoreLocked || isMoreHovered - - const eventImages = activeEventId - ? startEvents - .find((event) => event.id === activeEventId) - ?.images.map((img) => ({ src: img, title: startEvents.find((e) => e.id === activeEventId)!.title })) || [] - : [] - - // Get current event for auto-rotation (only if nothing is active) - const currentEvent = !activeEventId && !activeMore && currentEventIndex < startEvents.length - ? startEvents[currentEventIndex] - : null - const currentEventImages = currentEvent - ? currentEvent.images.map((img) => ({ src: img, title: currentEvent.title })) - : [] - - const scrollToEventImage = () => { - if (window.innerWidth < 1024 && eventImageRef.current) { - const yOffset = -80 - const y = eventImageRef.current.getBoundingClientRect().top + window.scrollY + yOffset - window.scrollTo({ top: y, behavior: 'smooth' }) - } - } - - useEffect(() => { - setLoading(false) - }, []) - - useEffect(() => { - const slider = timelineSliderRef.current - if (!slider || loading) return - - const updateScroll = () => { - const maxScroll = slider.scrollWidth - slider.clientWidth - const progress = maxScroll > 0 ? (slider.scrollLeft / maxScroll) * 100 : 0 - setScrollProgress(progress) - } - - slider.addEventListener('scroll', updateScroll) - updateScroll() - setTimeout(updateScroll, 100) - - return () => slider.removeEventListener('scroll', updateScroll) - }, [loading]) - - // Auto-rotate events every 5 seconds - useEffect(() => { - if (loading || activeEventId || activeMore) { - if (autoRotateTimerRef.current) { - clearInterval(autoRotateTimerRef.current) - autoRotateTimerRef.current = null - } - return - } - - autoRotateTimerRef.current = setInterval(() => { - setCurrentEventIndex((prev) => (prev + 1) % (startEvents.length + 1)) - }, 3000) - - return () => { - if (autoRotateTimerRef.current) { - clearInterval(autoRotateTimerRef.current) - } - } - }, [loading, activeEventId, activeMore]) - - // Reset image index when event changes - useEffect(() => { - setEventImageIndex(0) - }, [activeEventId, activeMore, currentEventIndex]) - - // When hovering ends, continue auto-rotation from the active item - useEffect(() => { - if (!activeEventId && !activeMore) { - return - } - if (activeEventId) { - const index = startEvents.findIndex(e => e.id === activeEventId) - if (index !== -1) { - setCurrentEventIndex(index) - } - } else if (activeMore) { - setCurrentEventIndex(startEvents.length) - } - }, [activeEventId, activeMore]) - - if (loading) { - return ( -
-
-

Loading journey...

-
-
- ) - } - - return ( - <> - - -
- {/* Hero Section */} - - YOUR START MUNICH -
- JOURNEY - - } - description="Become a member and spend at least two active semesters contributing to the community" - > -
- -
- - {semesterCount} - - + -
-

Semesters

-
- - -
- - ∞ - -
-

Possibilities

-
-
-
- - {/* ═══ MEMBER JOURNEY TIMELINE ═══ */} -
-
- Your Path -

- MEMBER JOURNEY -

-

- The 5 milestones of your first two semesters at START Munich -

-
- -
-
{ - isDraggingTimeline.current = true - dragStartX.current = e.pageX - (timelineSliderRef.current?.offsetLeft ?? 0) - dragStartScrollLeft.current = timelineSliderRef.current?.scrollLeft ?? 0 - }} - onMouseMove={(e) => { - if (!isDraggingTimeline.current || !timelineSliderRef.current) return - e.preventDefault() - const x = e.pageX - (timelineSliderRef.current.offsetLeft ?? 0) - timelineSliderRef.current.scrollLeft = dragStartScrollLeft.current - (x - dragStartX.current) - }} - onMouseUp={() => { isDraggingTimeline.current = false }} - onMouseLeave={() => { isDraggingTimeline.current = false }} - > -
- {timelineEvents.map((event, index) => ( -
-
- {/* Pink accent top bar */} -
- -
- {/* Step number + divider */} -
- - 0{index + 1} - -
- {event.icon} -
- -

- {event.title} -

- -

- {event.description} -

- -
- {(event.details as string[]).map((detail, i) => ( -
-
- {detail} -
- ))} -
-
-
-
- ))} -
-
- -
- -
-
-
- - {/* ═══ MAIN CONTENT ═══ */} -
- - {/* ═══ DEPARTMENTS ═══ */} -
-
- Where You Fit -

- OUR DEPARTMENTS -

-

- Choose your department and contribute to our community -

-
- - {/* Desktop: arc layout */} - {(() => { - // Left-side half-circle "(" : center=(300,300), r=300 - // Arc from (300,0) at top, curving LEFT through (0,300), to (300,600) at bottom - const cx = 300, cy = 300, r = 300 - const toRad = (deg: number) => (deg * Math.PI) / 180 - // Standard-math angles (CCW from +x): 108°→top-left … 252°→bottom-left - const angles = [108, 144, 180, 216, 252] - // Per-dot label vertical nudge (positive = lower, negative = higher) - const labelYOffsets = [18, 6, 6, 6, -8] - const dots = departments.map((dept, i) => { - const a = toRad(angles[i]) - return { - dept, - x: cx + r * Math.cos(a), - y: cy - r * Math.sin(a), - labelYOffset: labelYOffsets[i], - } - }) - const activeDept = departments.find(d => d.id === activeDeptId) - - return ( - <> - {/* Desktop */} -
- {/* Detail panel — left */} -
- {activeDept && ( -
-
- {activeDept.icon} -
-

{activeDept.name.toUpperCase()}

-

{activeDept.description}

-
-

Responsibilities

-
- {activeDept.responsibilities.map((resp, i) => ( -
-
- {resp} -
- ))} -
-
-
- )} -
- - {/* Arc — right */} -
- - {/* Always-pink arc: (300,0) CCW through (0,300) to (300,600) */} - - - {/* Dots + labels */} - {dots.map(({ dept, x, y, labelYOffset }) => { - const isActive = activeDeptId === dept.id - return ( - setActiveDeptId(dept.id)} - className="cursor-pointer" - style={{ transition: 'all 0.3s' }} - > - {isActive && ( - - )} - - {isActive && } - {/* Label — to the right of dot, toward the opening */} - - {dept.name.toUpperCase()} - - - ) - })} - -
-
- - {/* Mobile: icon strip + card */} -
-
- {departments.map((dept) => ( - - ))} -
- {activeDept && ( -
-

{activeDept.name.toUpperCase()}

-

{activeDept.description}

-
-

Responsibilities

- {activeDept.responsibilities.map((resp, i) => ( -
-
- {resp} -
- ))} -
-
- )} -
- - ) - })()} -
- - {/* ═══ INTERNAL EVENTS ═══ */} -
-
- Community Life -

- INTERNAL EVENTS -

-

- Regular events and activities for our member community -

-
- -
- {/* Event list card */} -
{ - if (!lockedEventId && !isMoreLocked) { - setHoveredEventId(null) - setIsMoreHovered(false) - } - }} - > -
- {startEvents.map((event, index) => { - const isActive = activeEventId === event.id || (!activeEventId && !activeMore && currentEventIndex === index) - return ( -
{ if (!lockedEventId && !isMoreLocked) setHoveredEventId(event.id) }} - onMouseLeave={() => { if (!lockedEventId && !isMoreLocked) setHoveredEventId(null) }} - onClick={() => { setLockedEventId(event.id); setIsMoreLocked(false); setIsMoreHovered(false); setHoveredEventId(null); scrollToEventImage() }} - > -
- {event.icon} -
-
-
-

{event.title}

- {event.frequency && ( - {event.frequency} - )} -
-

- {event.description} -

-
-
- ) - })} - - {/* And a lot more */} - {(() => { - const isMoreActive = activeMore || (!activeEventId && !activeMore && currentEventIndex === startEvents.length) - return ( -
{ if (!lockedEventId && !isMoreLocked) setIsMoreHovered(true) }} - onMouseLeave={() => { if (!lockedEventId && !isMoreLocked) setIsMoreHovered(false) }} - onClick={() => { setIsMoreLocked(true); setLockedEventId(null); setHoveredEventId(null); setIsMoreHovered(false); scrollToEventImage() }} - > -
- -
-
-

And a lot more...

-

- Discover many more exciting events and opportunities. -

-
-
- ) - })()} -
-
- - {/* Image panel */} -
- {activeMore || (!activeEventId && !activeMore && currentEventIndex === startEvents.length) ? ( -
- {moreImages.map((img, i) => ( -
- {`More -
-
- ))} -
-
-

AND MORE...

-
-
-
- ) : (() => { - const images = activeEventId && eventImages.length > 0 - ? eventImages - : currentEventImages.length > 0 - ? currentEventImages - : null - if (!images) return null - const idx = eventImageIndex % images.length - return ( -
- {images[idx]?.title} -
-

{images[idx]?.title}

-
-
- - -
-
- {idx + 1} / {images.length} -
-
- ) - })()} -
-
-
- - {/* ═══ COMMUNITY SPECIALS ═══ */} -
-
- Exclusive Access -

- COMMUNITY SPECIALS -

-

- Unique opportunities exclusively for START Munich members -

-
- -
- {/* Bay Area */} - -
- San Francisco Bay Area -
-
-
-

- START goes Bay Area -

-

- A selective international exchange program connecting outstanding entrepreneurial talent from Europe with the innovation ecosystem of the San Francisco Bay Area. The program brings together a curated group of 20 participants and enables direct interaction with founders, researchers, and investors at leading technology and innovation organizations. -

- - Learn more - - - - -
-
- - {/* Cambridge */} -
-
- University Research -
-
-
-

- Research Stay @ Cambridge -

-

- Spend a research stay with our partners at the University of Cambridge and Technical University of Munich. Gain access to world-class academic resources, mentorship from leading researchers, and the opportunity to contribute to cutting-edge entrepreneurship research. -

-
- - - - - Cambridge - - - - - - TUM - -
-
-
-
-
- - {/* Member Stories */} - - MEMBER STORIES - } - description="Real stories from our members who built successful startups with START Munich" - items={memberStories.map(story => ({ - id: story.id, - name: story.name, - role: story.role, - company: story.company, - image: story.image, - story: story.story, - logos: story.logos - }))} - /> - - {/* CTA */} - - -
-
- - - - ) -} \ No newline at end of file + return +} diff --git a/app/member-network/MemberNetworkContent.tsx b/app/member-network/MemberNetworkContent.tsx new file mode 100644 index 0000000..d2e02dd --- /dev/null +++ b/app/member-network/MemberNetworkContent.tsx @@ -0,0 +1,142 @@ +"use client" + +import { useState, useEffect } from "react" +import Script from "next/script" +import Hero from "@/components/Hero" + +export const dynamic = 'force-dynamic' + +interface Company { + id: string + name: string + type: string + logoUrl: string +} + +async function fetchCompanies(): Promise { + try { + const baseUrl = typeof window !== 'undefined' + ? window.location.origin + : process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' + + const response = await fetch(`${baseUrl}/api/member-network`, { cache: 'no-store' }) + if (!response.ok) throw new Error('Failed to fetch') + return await response.json() + } catch (error) { + console.error('Error fetching member network:', error) + return [] + } +} + +function LogoCard({ company }: { company: Company }) { + const [imgFailed, setImgFailed] = useState(false) + const initials = company.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase() + + return ( +
+ {company.logoUrl && !imgFailed ? ( + {company.name} setImgFailed(true)} + /> + ) : ( +

{company.name}

+ )} +
+ ) +} + +export default function MemberNetworkPage() { + const [loading, setLoading] = useState(true) + const [companies, setCompanies] = useState([]) + + useEffect(() => { + fetchCompanies().then((data) => { + setCompanies(data) + setLoading(false) + }) + }, []) + + // Group by Type + const categories = Array.from(new Set(companies.map(c => c.type))) + .sort((a, b) => companies.filter(c => c.type === b).length - companies.filter(c => c.type === a).length) + + if (loading) { + return ( +
+
+

Loading member network...

+
+
+ ) + } + + return ( + <> + + +
+ + MEMBER +
+ NETWORK + + } + description="Discover where our talented members are making their mark across the industry" + /> + +
+ +
+
+
+
+

+ Where Our Members Work +

+

+ Our community spans some of the world's leading companies, research institutions, and startups. From global tech giants to early-stage ventures, START Munich members are building careers that make an impact. +

+
+
+ + {categories.map((category) => ( +
+

+ {category} +

+
+ {companies + .filter(c => c.type === category) + .map((company) => ( + + ))} +
+
+ ))} + +
+
+ + ) +} \ No newline at end of file diff --git a/app/member-network/page.tsx b/app/member-network/page.tsx index d2e02dd..2fe45df 100644 --- a/app/member-network/page.tsx +++ b/app/member-network/page.tsx @@ -1,142 +1,21 @@ -"use client" - -import { useState, useEffect } from "react" -import Script from "next/script" -import Hero from "@/components/Hero" +import type { Metadata } from 'next' +import MemberNetworkContent from './MemberNetworkContent' export const dynamic = 'force-dynamic' -interface Company { - id: string - name: string - type: string - logoUrl: string -} - -async function fetchCompanies(): Promise { - try { - const baseUrl = typeof window !== 'undefined' - ? window.location.origin - : process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' - - const response = await fetch(`${baseUrl}/api/member-network`, { cache: 'no-store' }) - if (!response.ok) throw new Error('Failed to fetch') - return await response.json() - } catch (error) { - console.error('Error fetching member network:', error) - return [] - } -} - -function LogoCard({ company }: { company: Company }) { - const [imgFailed, setImgFailed] = useState(false) - const initials = company.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase() - - return ( -
- {company.logoUrl && !imgFailed ? ( - {company.name} setImgFailed(true)} - /> - ) : ( -

{company.name}

- )} -
- ) +export const metadata: Metadata = { + title: 'Member Network', + description: + 'Explore the START Munich member network — connect with fellow students, alumni, and founders from TUM, LMU, and HM who are shaping the future.', + alternates: { canonical: 'https://www.startmunich.de/member-network' }, + openGraph: { + url: 'https://www.startmunich.de/member-network', + title: 'Member Network | START Munich', + description: + 'Explore the START Munich member network — connect with fellow students, alumni, and founders from TUM, LMU, and HM who are shaping the future.', + }, } export default function MemberNetworkPage() { - const [loading, setLoading] = useState(true) - const [companies, setCompanies] = useState([]) - - useEffect(() => { - fetchCompanies().then((data) => { - setCompanies(data) - setLoading(false) - }) - }, []) - - // Group by Type - const categories = Array.from(new Set(companies.map(c => c.type))) - .sort((a, b) => companies.filter(c => c.type === b).length - companies.filter(c => c.type === a).length) - - if (loading) { - return ( -
-
-

Loading member network...

-
-
- ) - } - - return ( - <> - - -
- - MEMBER -
- NETWORK - - } - description="Discover where our talented members are making their mark across the industry" - /> - -
- -
-
-
-
-

- Where Our Members Work -

-

- Our community spans some of the world's leading companies, research institutions, and startups. From global tech giants to early-stage ventures, START Munich members are building careers that make an impact. -

-
-
- - {categories.map((category) => ( -
-

- {category} -

-
- {companies - .filter(c => c.type === category) - .map((company) => ( - - ))} -
-
- ))} - -
-
- - ) -} \ No newline at end of file + return +} diff --git a/app/members/MembersContent.tsx b/app/members/MembersContent.tsx new file mode 100644 index 0000000..e3d40f3 --- /dev/null +++ b/app/members/MembersContent.tsx @@ -0,0 +1,743 @@ +"use client" + +import { useState, useEffect, useRef, useCallback } from "react" +import Image from "next/image" +import Script from "next/script" +import Hero from "@/components/Hero" +import HeroCard from "@/components/HeroCard" +import { useAnimatedNumber } from "@/lib/useAnimatedNumber" +import { useInView } from "@/lib/hooks" + +export const dynamic = 'force-dynamic' + +interface Member { + id: number + name: string + batch: string + role: string + study?: string + university?: string + company?: string + linkedinUrl?: string + imageUrl: string + profileImage?: string + bio?: string + expertise?: string[] + achievements?: string + gender?: string +} + +interface BoardMember { + name: string + role: string + imageUrl: string + profileImage?: string + linkedinUrl?: string + _hasMatch?: boolean +} + +interface Board { + id: string + name: string + year: string + imageUrl: string + executiveBoard: BoardMember[] + departmentBoard: BoardMember[] +} + +async function fetchMembers(): Promise { + try { + const response = await fetch('/api/members') + if (!response.ok) throw new Error('Failed to fetch members') + return await response.json() + } catch (error) { + console.error('Error fetching members:', error) + return [] + } +} + +export default function MembersPage() { + const [members, setMembers] = useState([]) + const [expandedBatch, setExpandedBatch] = useState(null) + const [expandedBoard, setExpandedBoard] = useState(null) + const [batchMembers, setBatchMembers] = useState([]) + const [loadingBatch, setLoadingBatch] = useState(false) + const [boardLoading, setBoardLoading] = useState(false) + const batchContentRef = useRef(null) + const [boards, setBoards] = useState([ + { + id: '25-26', name: 'Board 25-26', year: '2025-2026', imageUrl: '/ourMembers/boads/boad26.jpeg', + executiveBoard: [ + { name: 'BOARD MEMBER', role: 'CFO', imageUrl: '/ourMembers/hero-opt.png' }, + { name: 'BOARD MEMBER', role: 'President', imageUrl: '/ourMembers/hero-opt.png' }, + { name: 'BOARD MEMBER', role: 'Vice President', imageUrl: '/ourMembers/hero-opt.png' }, + ], + departmentBoard: [ + { name: 'BOARD MEMBER', role: 'MD Events', imageUrl: '/ourMembers/hero-opt.png' }, + { name: 'BOARD MEMBER', role: 'MD Marketing', imageUrl: '/ourMembers/hero-opt.png' }, + { name: 'BOARD MEMBER', role: 'MD People', imageUrl: '/ourMembers/hero-opt.png' }, + { name: 'BOARD MEMBER', role: 'MD Finance & Operations', imageUrl: '/ourMembers/hero-opt.png' }, + { name: 'BOARD MEMBER', role: 'MD Partnerships', imageUrl: '/ourMembers/hero-opt.png' }, + ], + }, + { + id: '24-25', name: 'Board 24-25', year: '2024-2025', imageUrl: '', + executiveBoard: [ + { name: 'SIMON BURMER', role: 'CFO', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/simon-burmer/' }, + { name: 'ALI SERAG EL DIN', role: 'President', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/ali-serag-el-din/' }, + { name: 'DEFNE AYTUNA', role: 'Vice President', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/defne-aytuna/' }, + ], + departmentBoard: [ + { name: 'MOHAMMED THABIT', role: 'MD Events', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/mohammed-thabit/' }, + { name: 'PIOTR NOBIS', role: 'MD Marketing', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/piotr-nobis/' }, + { name: 'ANNA HELETYCH', role: 'MD People', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/anna-heletych/' }, + { name: 'NIKLAS SIMAKOV', role: 'MD Finance & Operations', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/niklas-simakov/' }, + { name: 'MARIUS HEUMADER', role: 'MD Partnerships', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/marius-heumader/' }, + ], + }, + ]) + + const analyticsView = useInView(0.1) + const boardsView = useInView(0.1) + const batchesView = useInView(0.1) + + const animatedActiveMembers = useAnimatedNumber(70, false, 1000) + const animatedAlumniCount = useAnimatedNumber(600, false, 1000) + + const getInitials = (name: string) => { + const words = name.trim().split(/\s+/) + if (words.length === 0) return '' + if (words.length === 1) return words[0].slice(0, 2).toUpperCase() + return (words[0][0] + words[words.length - 1][0]).toUpperCase() + } + + const isPlaceholderImage = (url?: string) => { + if (!url) return true + const normalized = url.toLowerCase().trim() + return normalized === '/batch-opt.jpeg' || normalized.endsWith('/batch-opt.jpeg') || + normalized === '/batch-opt.jpg' || normalized.endsWith('/batch-opt.jpg') || + normalized === '/batch-opt.png' || normalized.endsWith('/batch-opt.png') || + normalized === '/example-opt.png' || normalized.endsWith('/example-opt.png') || + normalized === '/example.png' || normalized.endsWith('/example.png') || + normalized === '/ourmembers/hero-opt.png' || normalized.endsWith('/ourmembers/hero-opt.png') + } + + useEffect(() => { + const loadMembers = async () => { + const data = await fetchMembers() + setMembers(data) + } + loadMembers() + }, []) + + useEffect(() => { + if (expandedBatch) { + const loadBatchMembers = async () => { + setLoadingBatch(true) + try { + const response = await fetch(`/api/members/batch/${encodeURIComponent(expandedBatch)}`) + if (response.ok) { + const data = await response.json() + if (Array.isArray(data) && data.length > 0) { + const transformedData = data.map((member: Member) => ({ + ...member, + profileImage: isPlaceholderImage(member.imageUrl) ? undefined : member.imageUrl + })) + setBatchMembers(transformedData) + } else { + setBatchMembers(members.filter(m => m.batch === expandedBatch)) + } + } else { + setBatchMembers(members.filter(m => m.batch === expandedBatch)) + } + } catch (error) { + console.error('Error fetching batch members:', error) + setBatchMembers(members.filter(m => m.batch === expandedBatch)) + } + setLoadingBatch(false) + } + loadBatchMembers() + } else { + setBatchMembers([]) + } + }, [expandedBatch, members]) + + const normalize = (text: string) => + text.toLowerCase().replace(/[\.\-&\/]/g, ' ').replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim() + + const roleSynonyms: Record = { + cfo: ['chief financial officer', 'chief financial officer (cfo)'], + 'vice president': ['vice-president', 'vice president', 'vp'], + 'md finance operations': ['md finance & operations', 'md finance and operations'], + 'md partnerships': ['md partnerships', 'md partnership'], + 'md marketing': ['md marketing'], + 'md people': ['md people'], + 'md events': ['md events'], + president: ['president'], + } + + const roleFallbackImages: Record = { + cfo: '/ourMembers/hero-opt.png', president: '/ourMembers/hero-opt.png', 'vice president': '/ourMembers/hero-opt.png', + 'md events': '/ourMembers/hero-opt.png', 'md marketing': '/ourMembers/hero-opt.png', 'md people': '/ourMembers/hero-opt.png', + 'md finance operations': '/ourMembers/hero-opt.png', 'md finance & operations': '/ourMembers/hero-opt.png', 'md partnerships': '/ourMembers/hero-opt.png', + } + + const getFallbackImageByRole = (role: string) => + roleFallbackImages[normalize(role)] || + Object.entries(roleFallbackImages).find(([key]) => normalize(role).includes(key))?.[1] || + '/ourMembers/hero-opt.png' + + const termStartYearFromBoard = (board: Board) => { + if (board.year && board.year.includes('-')) { + const [from] = board.year.split('-').map(v => v.trim()) + if (/^\d{4}$/.test(from)) return from + } + const parts = board.id.split('-').map(p => Number(p.trim())) + if (parts.length === 2 && !Number.isNaN(parts[0])) { + const from = parts[0] < 100 ? 2000 + parts[0] : parts[0] + return `${from}` + } + return '2024' + } + + const findByRole = (normalizedBoardMembers: any[]) => (role: string) => { + if (!role) return null + const normalizedRole = normalize(role) + let match = normalizedBoardMembers.find(m => normalize(m.role || '') === normalizedRole) + if (match) return match + const normalizedRoleKey = Object.keys(roleSynonyms).find(key => + key === normalizedRole || roleSynonyms[key].includes(normalizedRole) + ) + if (normalizedRoleKey) { + const aliases = [normalizedRoleKey, ...(roleSynonyms[normalizedRoleKey] || [])].map(normalize) + match = normalizedBoardMembers.find(m => { + const candidate = normalize(m.role || '') + return aliases.some(alias => candidate.includes(alias) || alias.includes(candidate)) + }) + if (match) return match + } + return normalizedBoardMembers.find(m => { + const candidateRole = normalize(m.role || '') + return candidateRole.includes(normalizedRole) || normalizedRole.includes(candidateRole) + }) || null + } + + const loadBoardMembers = async (boardId: string) => { + setBoardLoading(true) + try { + const board = boards.find(b => b.id === boardId) + if (!board) return + const termStartYear = termStartYearFromBoard(board) + const response = await fetch(`/api/board?termStartYears=${encodeURIComponent(termStartYear)}`) + if (!response.ok) return + const data = await response.json() + if (!data) return + const candidateData = Array.isArray(data) ? data : data?.data && Array.isArray(data.data) ? data.data : [] + const rawMembers: any[] = [] + candidateData.forEach((item: any) => { + if (item && Array.isArray(item.members)) rawMembers.push(...item.members) + else if (item && Array.isArray(item.boardMembers)) rawMembers.push(...item.boardMembers) + else if (item && Array.isArray(item.memberList)) rawMembers.push(...item.memberList) + else if (item && item.name && item.role) rawMembers.push(item) + }) + if (rawMembers.length === 0) return + const normalizedBoardMembers = rawMembers.flatMap((member: any) => { + const normalizedName = member.name || member.fullName || member.displayName || 'Board Member' + const normalizedRole = member.role || member.position || member.title || '' + const profileImage = member.profileImage || '' + const splitRoles = normalizedRole.split(',').map((r: string) => r.trim()).filter((r: string) => r.length > 0) + if (splitRoles.length <= 1) return [{ ...member, name: normalizedName, role: normalizedRole, profileImage }] + return splitRoles.map((rolePart: string) => ({ ...member, name: normalizedName, role: rolePart, profileImage })) + }) + const matchByRole = findByRole(normalizedBoardMembers) + setBoards(prev => prev.map(boardItem => { + if (boardItem.id !== boardId) return boardItem + return { + ...boardItem, + executiveBoard: boardItem.executiveBoard.map(member => { + const match = matchByRole(member.role) + const fallback = getFallbackImageByRole(member.role) + const hasMatch = !!match + return { ...member, name: match?.name || (hasMatch ? member.name : 'N/A'), profileImage: match?.profileImage || member.profileImage || '', imageUrl: match?.profileImage || member.profileImage || member.imageUrl || fallback, linkedinUrl: match?.linkedinUrl || member.linkedinUrl, _hasMatch: hasMatch } + }), + departmentBoard: boardItem.departmentBoard.map(member => { + const match = matchByRole(member.role) + const fallback = getFallbackImageByRole(member.role) + const hasMatch = !!match + return { ...member, name: match?.name || (hasMatch ? member.name : 'N/A'), profileImage: match?.profileImage || member.profileImage || '', imageUrl: match?.profileImage || member.profileImage || member.imageUrl || fallback, linkedinUrl: match?.linkedinUrl || member.linkedinUrl, _hasMatch: hasMatch } + }), + } + })) + } catch (error) { + console.error('Error fetching board data:', error) + } finally { + setBoardLoading(false) + } + } + + useEffect(() => { + if (expandedBoard) loadBoardMembers(expandedBoard) + }, [expandedBoard]) + + useEffect(() => { + void (async () => { + for (const board of boards) await loadBoardMembers(board.id) + })() + }, []) + + const handleOutsideClick = useCallback((e: MouseEvent) => { + if (expandedBatch && batchContentRef.current && !batchContentRef.current.contains(e.target as Node)) { + setExpandedBatch(null) + } + }, [expandedBatch]) + + useEffect(() => { + document.addEventListener('mousedown', handleOutsideClick) + return () => document.removeEventListener('mousedown', handleOutsideClick) + }, [handleOutsideClick]) + + const defaultBatches = [ + 'Winter 2025', 'Summer 2025', + 'Winter 2024', 'Summer 2024', + 'Winter 2023', 'Summer 2023', + 'Winter 2022', 'Summer 2022', + 'Winter 2021', + ] + const allBatches = Array.from(new Set([...members.map(m => m.batch), ...defaultBatches])) + .filter(Boolean).sort().reverse() + + const malePercentage = 59 + const femalePercentage = 41 + + const topStudies = [ + { study: 'Computer Science', percentage: 40 }, + { study: 'Business Administration', percentage: 30 }, + { study: 'Engineering', percentage: 16 }, + { study: 'Others', percentage: 14 }, + ] + + const topUniversities = [ + { university: 'TUM', percentage: 65 }, + { university: 'LMU', percentage: 20 }, + { university: 'HM', percentage: 7 }, + { university: 'Others', percentage: 8 }, + ] + + const batchImageMap: Record = { + ws21: 'WS21-opt.jpg', ws22: 'WS22-opt.jpg', ws23: 'WS23-opt.jpg', ws24: 'WS24-opt.jpg', ws25: 'WS25-opt.jpg', + ss22: 'SS22-opt.jpg', ss23: 'SS23-opt.jpg', ss24: 'SS24-opt.jpg', ss25: 'SS25-opt.jpg', + } + + function getBatchImageKey(batchName: string): string | null { + const normalized = batchName.toLowerCase().trim() + const fullMatch = normalized.match(/^(winter|summer)\s+(\d{4})$/) + if (fullMatch) return `${fullMatch[1] === 'winter' ? 'ws' : 'ss'}${fullMatch[2].slice(-2)}` + const fourDigitMatch = normalized.match(/^(ws|ss)\s*(\d{4})$/) + if (fourDigitMatch) return `${fourDigitMatch[1]}${fourDigitMatch[2].slice(-2)}` + const shortMatch = normalized.match(/^(ws|ss)\s*(\d{2})$/) + if (shortMatch) return `${shortMatch[1]}${shortMatch[2]}` + return null + } + + function isAfterWS21(batchName: string): boolean { + const normalized = batchName.toLowerCase().trim() + let year: number | null = null + let isWinter = false + const fullMatch = normalized.match(/^(winter|summer)\s+(\d{4})$/) + if (fullMatch) { year = parseInt(fullMatch[2].slice(-2)); isWinter = fullMatch[1] === 'winter' } + const shortMatch = normalized.match(/^(ws|ss)\s*(\d{2})$/) + if (shortMatch) { year = parseInt(shortMatch[2]); isWinter = shortMatch[1] === 'ws' } + if (year === null) return false + if (year > 21) return true + if (year === 21 && isWinter) return true + return false + } + + const sortedBatches = allBatches.map(batchName => { + const batchKey = getBatchImageKey(batchName) + const shouldShowImage = isAfterWS21(batchName) + let groupImageUrl = '/ourMembers/hero-opt.png' + if (shouldShowImage && batchKey && batchImageMap[batchKey]) { + groupImageUrl = `/ourMembers/batches_group_pictures/${batchImageMap[batchKey]}` + } + return { + name: batchName, + semester: batchName.split(' ')[0] || 'Batch', + year: batchName.split(' ')[1] || '', + groupImageUrl, + memberCount: members.filter(m => m.batch === batchName).length, + } + }).sort((a, b) => { + const yearDiff = parseInt(b.year) - parseInt(a.year) + if (yearDiff !== 0) return yearDiff + if (a.semester.toLowerCase().includes('winter') || a.semester.toLowerCase().startsWith('w')) return -1 + if (b.semester.toLowerCase().includes('winter') || b.semester.toLowerCase().startsWith('w')) return 1 + return 0 + }) + + + return ( + <> + + +
+ + {/* ═══ HERO ═══ */} + + START MUNICH +
+ MEMBERS + + } + description="Meet the ambitious student entrepreneurs building the future of technology and innovation." + > +
+ +
+ + {Math.floor(animatedActiveMembers)} + + + +
+

Active Members

+
+ +
+ + {Math.floor(animatedAlumniCount)} + + + +
+

Alumni

+
+
+
+ +
+ + {/* ═══ COMMUNITY ANALYTICS ═══ */} +
+
+ By the Numbers +

COMMUNITY ANALYTICS

+
+ +
+ {/* Gender */} +
+
+
+ + + +
+ Gender Distribution +
+
+
+
+ Male + {malePercentage}% +
+
+
+
+
+
+
+ Female + {femalePercentage}% +
+
+
+
+
+
+
+ + {/* Study Fields */} +
+
+
+ + + +
+ Top Study Fields +
+
+ {topStudies.map(({ study, percentage }) => ( +
+ {study} + {percentage}% +
+ ))} +
+
+ + {/* Universities */} +
+
+
+ + + +
+ Top Universities +
+
+ {topUniversities.map(({ university, percentage }) => ( +
+ {university} + {percentage}% +
+ ))} +
+
+
+
+ + {/* Divider */} +
+ + {/* ═══ THE BOARDS ═══ */} +
+
+ Leadership +

THE BOARDS

+
+ + {expandedBoard ? ( +
+ + + {boards.filter(b => b.id === expandedBoard).map(board => ( +
+ {/* Executive Board */} +
+
+
+ Executive Board +
+ + + {/* Department Board */} + + ) : ( +
+ {boards.map((board, i) => ( + + ))} +
+ )} +
+ + {/* Divider */} +
+ + {/* ═══ OUR BATCHES ═══ */} +
+
+ Our Community +

OUR BATCHES

+
+ + {expandedBatch ? ( +
+ + + {sortedBatches.filter(b => b.name === expandedBatch).map(batch => ( +
+

{batch.name}

+ +
setExpandedBatch(null)} + className="w-full relative rounded-3xl overflow-hidden border border-white/10 cursor-pointer hover:border-brand-pink/30 transition-all duration-300" + > +
+ {batch.name} +
+
+
+ +
+ {loadingBatch ? ( +
+
+
+ ) : ( +
+ {[...batchMembers].sort((a, b) => { + const aHasImage = a.profileImage ? 0 : 1 + const bHasImage = b.profileImage ? 0 : 1 + return aHasImage - bHasImage + }).map(member => ( + + + )} +
+
+ ))} +
+ ) : ( +
+ {sortedBatches.map((batch, i) => ( +
+ +
+ ))} +
+ )} +
+ +
+
+ + ) +} diff --git a/app/members/page.tsx b/app/members/page.tsx index b39f30e..97011f0 100644 --- a/app/members/page.tsx +++ b/app/members/page.tsx @@ -1,737 +1,21 @@ -"use client" - -import { useState, useEffect, useRef, useCallback } from "react" -import Image from "next/image" -import Script from "next/script" -import Hero from "@/components/Hero" -import HeroCard from "@/components/HeroCard" -import { useAnimatedNumber } from "@/lib/useAnimatedNumber" -import { useInView } from "@/lib/hooks" +import type { Metadata } from 'next' +import MembersContent from './MembersContent' export const dynamic = 'force-dynamic' -interface Member { - id: number - name: string - batch: string - role: string - study?: string - university?: string - company?: string - linkedinUrl?: string - imageUrl: string - profileImage?: string - bio?: string - expertise?: string[] - achievements?: string - gender?: string -} - -interface BoardMember { - name: string - role: string - imageUrl: string - profileImage?: string - linkedinUrl?: string - _hasMatch?: boolean -} - -interface Board { - id: string - name: string - year: string - imageUrl: string - executiveBoard: BoardMember[] - departmentBoard: BoardMember[] -} - -async function fetchMembers(): Promise { - try { - const response = await fetch('/api/members') - if (!response.ok) throw new Error('Failed to fetch members') - return await response.json() - } catch (error) { - console.error('Error fetching members:', error) - return [] - } +export const metadata: Metadata = { + title: 'Members', + description: + 'Meet START Munich members — 70+ active members and 600+ alumni from TUM, LMU, and HM building the companies of tomorrow.', + alternates: { canonical: 'https://www.startmunich.de/members' }, + openGraph: { + url: 'https://www.startmunich.de/members', + title: 'Members | START Munich', + description: + 'Meet START Munich members — 70+ active members and 600+ alumni from TUM, LMU, and HM building the companies of tomorrow.', + }, } export default function MembersPage() { - const [members, setMembers] = useState([]) - const [expandedBatch, setExpandedBatch] = useState(null) - const [expandedBoard, setExpandedBoard] = useState(null) - const [batchMembers, setBatchMembers] = useState([]) - const [loadingBatch, setLoadingBatch] = useState(false) - const [boardLoading, setBoardLoading] = useState(false) - const batchContentRef = useRef(null) - const [boards, setBoards] = useState([ - { - id: '25-26', name: 'Board 25-26', year: '2025-2026', imageUrl: '/ourMembers/boads/boad26.jpeg', - executiveBoard: [ - { name: 'BOARD MEMBER', role: 'CFO', imageUrl: '/ourMembers/hero-opt.png' }, - { name: 'BOARD MEMBER', role: 'President', imageUrl: '/ourMembers/hero-opt.png' }, - { name: 'BOARD MEMBER', role: 'Vice President', imageUrl: '/ourMembers/hero-opt.png' }, - ], - departmentBoard: [ - { name: 'BOARD MEMBER', role: 'MD Events', imageUrl: '/ourMembers/hero-opt.png' }, - { name: 'BOARD MEMBER', role: 'MD Marketing', imageUrl: '/ourMembers/hero-opt.png' }, - { name: 'BOARD MEMBER', role: 'MD People', imageUrl: '/ourMembers/hero-opt.png' }, - { name: 'BOARD MEMBER', role: 'MD Finance & Operations', imageUrl: '/ourMembers/hero-opt.png' }, - { name: 'BOARD MEMBER', role: 'MD Partnerships', imageUrl: '/ourMembers/hero-opt.png' }, - ], - }, - { - id: '24-25', name: 'Board 24-25', year: '2024-2025', imageUrl: '', - executiveBoard: [ - { name: 'SIMON BURMER', role: 'CFO', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/simon-burmer/' }, - { name: 'ALI SERAG EL DIN', role: 'President', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/ali-serag-el-din/' }, - { name: 'DEFNE AYTUNA', role: 'Vice President', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/defne-aytuna/' }, - ], - departmentBoard: [ - { name: 'MOHAMMED THABIT', role: 'MD Events', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/mohammed-thabit/' }, - { name: 'PIOTR NOBIS', role: 'MD Marketing', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/piotr-nobis/' }, - { name: 'ANNA HELETYCH', role: 'MD People', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/anna-heletych/' }, - { name: 'NIKLAS SIMAKOV', role: 'MD Finance & Operations', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/niklas-simakov/' }, - { name: 'MARIUS HEUMADER', role: 'MD Partnerships', imageUrl: '/ourMembers/hero-opt.png', linkedinUrl: 'https://www.linkedin.com/in/marius-heumader/' }, - ], - }, - ]) - - const analyticsView = useInView(0.1) - const boardsView = useInView(0.1) - const batchesView = useInView(0.1) - - const animatedActiveMembers = useAnimatedNumber(70, false, 1000) - const animatedAlumniCount = useAnimatedNumber(600, false, 1000) - - const getInitials = (name: string) => { - const words = name.trim().split(/\s+/) - if (words.length === 0) return '' - if (words.length === 1) return words[0].slice(0, 2).toUpperCase() - return (words[0][0] + words[words.length - 1][0]).toUpperCase() - } - - const isPlaceholderImage = (url?: string) => { - if (!url) return true - const normalized = url.toLowerCase().trim() - return normalized === '/batch-opt.jpeg' || normalized.endsWith('/batch-opt.jpeg') || - normalized === '/batch-opt.jpg' || normalized.endsWith('/batch-opt.jpg') || - normalized === '/batch-opt.png' || normalized.endsWith('/batch-opt.png') || - normalized === '/example-opt.png' || normalized.endsWith('/example-opt.png') || - normalized === '/example.png' || normalized.endsWith('/example.png') || - normalized === '/ourmembers/hero-opt.png' || normalized.endsWith('/ourmembers/hero-opt.png') - } - - useEffect(() => { - const loadMembers = async () => { - const data = await fetchMembers() - setMembers(data) - } - loadMembers() - }, []) - - useEffect(() => { - if (expandedBatch) { - const loadBatchMembers = async () => { - setLoadingBatch(true) - try { - const response = await fetch(`/api/members/batch/${encodeURIComponent(expandedBatch)}`) - if (response.ok) { - const data = await response.json() - if (Array.isArray(data) && data.length > 0) { - const transformedData = data.map((member: Member) => ({ - ...member, - profileImage: isPlaceholderImage(member.imageUrl) ? undefined : member.imageUrl - })) - setBatchMembers(transformedData) - } else { - setBatchMembers(members.filter(m => m.batch === expandedBatch)) - } - } else { - setBatchMembers(members.filter(m => m.batch === expandedBatch)) - } - } catch (error) { - console.error('Error fetching batch members:', error) - setBatchMembers(members.filter(m => m.batch === expandedBatch)) - } - setLoadingBatch(false) - } - loadBatchMembers() - } else { - setBatchMembers([]) - } - }, [expandedBatch, members]) - - const normalize = (text: string) => - text.toLowerCase().replace(/[\.\-&\/]/g, ' ').replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim() - - const roleSynonyms: Record = { - cfo: ['chief financial officer', 'chief financial officer (cfo)'], - 'vice president': ['vice-president', 'vice president', 'vp'], - 'md finance operations': ['md finance & operations', 'md finance and operations'], - 'md partnerships': ['md partnerships', 'md partnership'], - 'md marketing': ['md marketing'], - 'md people': ['md people'], - 'md events': ['md events'], - president: ['president'], - } - - const roleFallbackImages: Record = { - cfo: '/ourMembers/hero-opt.png', president: '/ourMembers/hero-opt.png', 'vice president': '/ourMembers/hero-opt.png', - 'md events': '/ourMembers/hero-opt.png', 'md marketing': '/ourMembers/hero-opt.png', 'md people': '/ourMembers/hero-opt.png', - 'md finance operations': '/ourMembers/hero-opt.png', 'md finance & operations': '/ourMembers/hero-opt.png', 'md partnerships': '/ourMembers/hero-opt.png', - } - - const getFallbackImageByRole = (role: string) => - roleFallbackImages[normalize(role)] || - Object.entries(roleFallbackImages).find(([key]) => normalize(role).includes(key))?.[1] || - '/ourMembers/hero-opt.png' - - const termStartYearFromBoard = (board: Board) => { - if (board.year && board.year.includes('-')) { - const [from] = board.year.split('-').map(v => v.trim()) - if (/^\d{4}$/.test(from)) return from - } - const parts = board.id.split('-').map(p => Number(p.trim())) - if (parts.length === 2 && !Number.isNaN(parts[0])) { - const from = parts[0] < 100 ? 2000 + parts[0] : parts[0] - return `${from}` - } - return '2024' - } - - const findByRole = (normalizedBoardMembers: any[]) => (role: string) => { - if (!role) return null - const normalizedRole = normalize(role) - let match = normalizedBoardMembers.find(m => normalize(m.role || '') === normalizedRole) - if (match) return match - const normalizedRoleKey = Object.keys(roleSynonyms).find(key => - key === normalizedRole || roleSynonyms[key].includes(normalizedRole) - ) - if (normalizedRoleKey) { - const aliases = [normalizedRoleKey, ...(roleSynonyms[normalizedRoleKey] || [])].map(normalize) - match = normalizedBoardMembers.find(m => { - const candidate = normalize(m.role || '') - return aliases.some(alias => candidate.includes(alias) || alias.includes(candidate)) - }) - if (match) return match - } - return normalizedBoardMembers.find(m => { - const candidateRole = normalize(m.role || '') - return candidateRole.includes(normalizedRole) || normalizedRole.includes(candidateRole) - }) || null - } - - const loadBoardMembers = async (boardId: string) => { - setBoardLoading(true) - try { - const board = boards.find(b => b.id === boardId) - if (!board) return - const termStartYear = termStartYearFromBoard(board) - const response = await fetch(`/api/board?termStartYears=${encodeURIComponent(termStartYear)}`) - if (!response.ok) return - const data = await response.json() - if (!data) return - const candidateData = Array.isArray(data) ? data : data?.data && Array.isArray(data.data) ? data.data : [] - const rawMembers: any[] = [] - candidateData.forEach((item: any) => { - if (item && Array.isArray(item.members)) rawMembers.push(...item.members) - else if (item && Array.isArray(item.boardMembers)) rawMembers.push(...item.boardMembers) - else if (item && Array.isArray(item.memberList)) rawMembers.push(...item.memberList) - else if (item && item.name && item.role) rawMembers.push(item) - }) - if (rawMembers.length === 0) return - const normalizedBoardMembers = rawMembers.flatMap((member: any) => { - const normalizedName = member.name || member.fullName || member.displayName || 'Board Member' - const normalizedRole = member.role || member.position || member.title || '' - const profileImage = member.profileImage || '' - const splitRoles = normalizedRole.split(',').map((r: string) => r.trim()).filter((r: string) => r.length > 0) - if (splitRoles.length <= 1) return [{ ...member, name: normalizedName, role: normalizedRole, profileImage }] - return splitRoles.map((rolePart: string) => ({ ...member, name: normalizedName, role: rolePart, profileImage })) - }) - const matchByRole = findByRole(normalizedBoardMembers) - setBoards(prev => prev.map(boardItem => { - if (boardItem.id !== boardId) return boardItem - return { - ...boardItem, - executiveBoard: boardItem.executiveBoard.map(member => { - const match = matchByRole(member.role) - const fallback = getFallbackImageByRole(member.role) - const hasMatch = !!match - return { ...member, name: match?.name || (hasMatch ? member.name : 'N/A'), profileImage: match?.profileImage || member.profileImage || '', imageUrl: match?.profileImage || member.profileImage || member.imageUrl || fallback, linkedinUrl: match?.linkedinUrl || member.linkedinUrl, _hasMatch: hasMatch } - }), - departmentBoard: boardItem.departmentBoard.map(member => { - const match = matchByRole(member.role) - const fallback = getFallbackImageByRole(member.role) - const hasMatch = !!match - return { ...member, name: match?.name || (hasMatch ? member.name : 'N/A'), profileImage: match?.profileImage || member.profileImage || '', imageUrl: match?.profileImage || member.profileImage || member.imageUrl || fallback, linkedinUrl: match?.linkedinUrl || member.linkedinUrl, _hasMatch: hasMatch } - }), - } - })) - } catch (error) { - console.error('Error fetching board data:', error) - } finally { - setBoardLoading(false) - } - } - - useEffect(() => { - if (expandedBoard) loadBoardMembers(expandedBoard) - }, [expandedBoard]) - - useEffect(() => { - void (async () => { - for (const board of boards) await loadBoardMembers(board.id) - })() - }, []) - - const handleOutsideClick = useCallback((e: MouseEvent) => { - if (expandedBatch && batchContentRef.current && !batchContentRef.current.contains(e.target as Node)) { - setExpandedBatch(null) - } - }, [expandedBatch]) - - useEffect(() => { - document.addEventListener('mousedown', handleOutsideClick) - return () => document.removeEventListener('mousedown', handleOutsideClick) - }, [handleOutsideClick]) - - const defaultBatches = ['Winter 2021', 'Winter 2022', 'Summer 2022'] - const allBatches = Array.from(new Set([...members.map(m => m.batch), ...defaultBatches])) - .filter(Boolean).sort().reverse() - - const malePercentage = 59 - const femalePercentage = 41 - - const topStudies = [ - { study: 'Computer Science', percentage: 40 }, - { study: 'Business Administration', percentage: 30 }, - { study: 'Engineering', percentage: 16 }, - { study: 'Others', percentage: 14 }, - ] - - const topUniversities = [ - { university: 'TUM', percentage: 65 }, - { university: 'LMU', percentage: 20 }, - { university: 'HM', percentage: 7 }, - { university: 'Others', percentage: 8 }, - ] - - const batchImageMap: Record = { - ws21: 'WS21-opt.jpg', ws22: 'WS22-opt.jpg', ws23: 'WS23-opt.jpg', ws24: 'WS24-opt.jpg', ws25: 'WS25-opt.jpg', - ss22: 'SS22-opt.jpg', ss23: 'SS23-opt.jpg', ss24: 'SS24-opt.jpg', ss25: 'SS25-opt.jpg', - } - - function getBatchImageKey(batchName: string): string | null { - const normalized = batchName.toLowerCase().trim() - const fullMatch = normalized.match(/^(winter|summer)\s+(\d{4})$/) - if (fullMatch) return `${fullMatch[1] === 'winter' ? 'ws' : 'ss'}${fullMatch[2].slice(-2)}` - const fourDigitMatch = normalized.match(/^(ws|ss)\s*(\d{4})$/) - if (fourDigitMatch) return `${fourDigitMatch[1]}${fourDigitMatch[2].slice(-2)}` - const shortMatch = normalized.match(/^(ws|ss)\s*(\d{2})$/) - if (shortMatch) return `${shortMatch[1]}${shortMatch[2]}` - return null - } - - function isAfterWS21(batchName: string): boolean { - const normalized = batchName.toLowerCase().trim() - let year: number | null = null - let isWinter = false - const fullMatch = normalized.match(/^(winter|summer)\s+(\d{4})$/) - if (fullMatch) { year = parseInt(fullMatch[2].slice(-2)); isWinter = fullMatch[1] === 'winter' } - const shortMatch = normalized.match(/^(ws|ss)\s*(\d{2})$/) - if (shortMatch) { year = parseInt(shortMatch[2]); isWinter = shortMatch[1] === 'ws' } - if (year === null) return false - if (year > 21) return true - if (year === 21 && isWinter) return true - return false - } - - const sortedBatches = allBatches.map(batchName => { - const batchKey = getBatchImageKey(batchName) - const shouldShowImage = isAfterWS21(batchName) - let groupImageUrl = '/ourMembers/hero-opt.png' - if (shouldShowImage && batchKey && batchImageMap[batchKey]) { - groupImageUrl = `/ourMembers/batches_group_pictures/${batchImageMap[batchKey]}` - } - return { - name: batchName, - semester: batchName.split(' ')[0] || 'Batch', - year: batchName.split(' ')[1] || '', - groupImageUrl, - memberCount: members.filter(m => m.batch === batchName).length, - } - }).sort((a, b) => { - const yearDiff = parseInt(b.year) - parseInt(a.year) - if (yearDiff !== 0) return yearDiff - if (a.semester.toLowerCase().includes('winter') || a.semester.toLowerCase().startsWith('w')) return -1 - if (b.semester.toLowerCase().includes('winter') || b.semester.toLowerCase().startsWith('w')) return 1 - return 0 - }) - - - return ( - <> - - -
- - {/* ═══ HERO ═══ */} - - START MUNICH -
- MEMBERS - - } - description="Meet the ambitious student entrepreneurs building the future of technology and innovation." - > -
- -
- - {Math.floor(animatedActiveMembers)} - - + -
-

Active Members

-
- -
- - {Math.floor(animatedAlumniCount)} - - + -
-

Alumni

-
-
-
- -
- - {/* ═══ COMMUNITY ANALYTICS ═══ */} -
-
- By the Numbers -

COMMUNITY ANALYTICS

-
- -
- {/* Gender */} -
-
-
- - - -
- Gender Distribution -
-
-
-
- Male - {malePercentage}% -
-
-
-
-
-
-
- Female - {femalePercentage}% -
-
-
-
-
-
-
- - {/* Study Fields */} -
-
-
- - - -
- Top Study Fields -
-
- {topStudies.map(({ study, percentage }) => ( -
- {study} - {percentage}% -
- ))} -
-
- - {/* Universities */} -
-
-
- - - -
- Top Universities -
-
- {topUniversities.map(({ university, percentage }) => ( -
- {university} - {percentage}% -
- ))} -
-
-
-
- - {/* Divider */} -
- - {/* ═══ THE BOARDS ═══ */} -
-
- Leadership -

THE BOARDS

-
- - {expandedBoard ? ( -
- - - {boards.filter(b => b.id === expandedBoard).map(board => ( -
- {/* Executive Board */} -
-
-
- Executive Board -
- - - {/* Department Board */} - - ) : ( -
- {boards.map((board, i) => ( - - ))} -
- )} -
- - {/* Divider */} -
- - {/* ═══ OUR BATCHES ═══ */} -
-
- Our Community -

OUR BATCHES

-
- - {expandedBatch ? ( -
- - - {sortedBatches.filter(b => b.name === expandedBatch).map(batch => ( -
-

{batch.name}

- -
setExpandedBatch(null)} - className="w-full relative rounded-3xl overflow-hidden border border-white/10 cursor-pointer hover:border-brand-pink/30 transition-all duration-300" - > -
- {batch.name} -
-
-
- -
- {loadingBatch ? ( -
-
-
- ) : ( -
- {[...batchMembers].sort((a, b) => { - const aHasImage = a.profileImage ? 0 : 1 - const bHasImage = b.profileImage ? 0 : 1 - return aHasImage - bHasImage - }).map(member => ( - - - )} -
-
- ))} -
- ) : ( -
- {sortedBatches.map((batch, i) => ( -
- -
- ))} -
- )} -
- -
-
- - ) + return } diff --git a/app/our-mission/page.tsx b/app/our-mission/page.tsx deleted file mode 100644 index 10a4f8b..0000000 --- a/app/our-mission/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from 'next/navigation' - -export default function OurMissionRedirect() { - redirect('/about-us') -} diff --git a/app/page.tsx b/app/page.tsx index ff6d01c..197e6d0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,132 @@ -import { redirect } from 'next/navigation' +/** + * Home page — Server Component + * + * Fetches featured partners and startups directly from NocoDB at request time. + * Next.js ISR caches the result for 1 hour (`revalidate = 3600`), so the + * database is only hit once per hour across all visitors. The pre-fetched data + * is passed as props to HomeClient, which means logos are available on the + * initial HTML render — no client-side fetch or flash of missing content. + */ +import type { Metadata } from 'next' +import HomeClient from './home/HomeClient' +import type { Partner, Startup } from '@/lib/types' -export default function RootPage() { - redirect('/home') +export const metadata: Metadata = { + title: 'START Munich – Student Entrepreneurship Community', + description: + 'START Munich is the largest student-run entrepreneurship community in Munich. We empower the next generation of founders to dare, build, and belong.', + alternates: { canonical: 'https://www.startmunich.de/' }, + openGraph: { + url: 'https://www.startmunich.de/', + title: 'START Munich – Student Entrepreneurship Community', + description: + 'START Munich is the largest student-run entrepreneurship community in Munich. We empower the next generation of founders to dare, build, and belong.', + }, +} + +// Revalidate the cached page every hour +export const revalidate = 3600 + +const NOCODB_API_TOKEN = process.env.NOCODB_API_TOKEN +const NOCODB_BASE_URL = process.env.NOCODB_BASE_URL || 'https://ndb.startmunich.de' +const NOCODB_PARTNERS_TABLE_ID = process.env.NOCODB_PARTNERS_TABLE_ID +const NOCODB_STARTUPS_TABLE_ID = process.env.NOCODB_STARTUPS_TABLE_ID + +async function fetchFeaturedPartners(): Promise { + if (!NOCODB_API_TOKEN || !NOCODB_PARTNERS_TABLE_ID) return [] + try { + const res = await fetch( + `${NOCODB_BASE_URL}/api/v2/tables/${NOCODB_PARTNERS_TABLE_ID}/records?limit=1000`, + { + headers: { 'xc-token': NOCODB_API_TOKEN, 'Content-Type': 'application/json' }, + next: { revalidate: 3600 }, + } + ) + if (!res.ok) return [] + const data = await res.json() + return (data.list || []) + .filter((r: any) => { + const show = r.Show + const featured = r.Featured + return (show === true || show === 1 || String(show).toLowerCase() === 'true') && + (featured === true || featured === 1 || String(featured).toLowerCase() === 'true') + }) + .map((r: any) => { + const logos: any[] = r.LogoNoBackground || [] + const logo = logos.length > 0 ? logos[logos.length - 1] : null + const logoUrl = logo?.signedPath + ? `${NOCODB_BASE_URL}/${logo.signedPath}` + : `https://ui-avatars.com/api/?name=${encodeURIComponent(r.Name || 'Partner')}&size=300&background=4f46e5&color=fff&bold=true&font-size=0.4` + return { id: r.Id || String(Math.random()), name: r.Name || 'Partner', category: r.Categrory || 'Other', logoUrl, featured: true } + }) + } catch { + return [] + } +} + +async function fetchFeaturedStartups(): Promise { + if (!NOCODB_API_TOKEN || !NOCODB_STARTUPS_TABLE_ID) return [] + try { + const res = await fetch( + `${NOCODB_BASE_URL}/api/v2/tables/${NOCODB_STARTUPS_TABLE_ID}/records?limit=1000`, + { + headers: { 'xc-token': NOCODB_API_TOKEN, 'Content-Type': 'application/json' }, + next: { revalidate: 3600 }, + } + ) + if (!res.ok) return [] + const data = await res.json() + return (data.list || []) + .filter((r: any) => + r['Featured Startup']?.toLowerCase() === 'yes' || + r['Y Combinator Alumni']?.toLowerCase() === 'yes' || + r['EWOR']?.toLowerCase() === 'yes' + ) + .map((r: any) => { + let logoUrl = `https://ui-avatars.com/api/?name=${encodeURIComponent(r['Startup Name'] || 'Startup')}&size=300&background=00002c&color=fff&bold=true&font-size=0.4` + if (r['Company Logo']?.[0]?.signedPath) logoUrl = `${NOCODB_BASE_URL}/${r['Company Logo'][0].signedPath}` + return { + id: r.Id || r.id, + name: r['Startup Name'] || 'Startup', + logoUrl, + isSpotlight: r['Featured Startup']?.toLowerCase() === 'yes', + isYCombinator: r['Y Combinator Alumni']?.toLowerCase() === 'yes', + isEWOR: r['EWOR']?.toLowerCase() === 'yes', + } + }) + } catch { + return [] + } +} + +export default async function HomePage() { + const [partners, startups] = await Promise.all([ + fetchFeaturedPartners(), + fetchFeaturedStartups(), + ]) + + const organizationJsonLd = { + '@context': 'https://schema.org', + '@type': 'Organization', + name: 'START Munich', + url: 'https://www.startmunich.de', + logo: 'https://www.startmunich.de/startIcon.png', + description: + 'START Munich is the largest student-run entrepreneurship community in Munich.', + foundingDate: '2003', + sameAs: [ + 'https://www.linkedin.com/company/start-munich', + 'https://www.instagram.com/startmunich', + ], + } + + return ( + <> + + +
+ + {/* Hero Section - Restored Old Style with Stats Added */} + + OUR +
+ PARTNERS + + } + description="Powering the next generation of entrepreneurs through world-class collaboration." + > +
+ {/** Stat 1 **/} + +
+ + {Math.floor(animatedPartners)} + + + +
+

Partners

+
+ + {/** Stat 2 **/} + +
+ + {Math.floor(animatedCategories)} + +
+

Industries

+
+
+
+ + {/* Sticky Filter & Search Bar */} +
+
+
+ + {/* Category Pills */} +
+ + {sortedCategories.map((category) => ( + + ))} +
+ + {/* Search Input */} +
+
+ + + +
+ setSearchQuery(e.target.value)} + /> +
+
+
+
+ + {/* Content Below Hero */} +
+ + {sortedCategories.map((categoryName) => { + const partnersToShow = filteredPartners(categoryName); + if (partnersToShow.length === 0) return null; + + return ( +
{ + categoryRefs.current[categoryName] = el; + }} + className="scroll-mt-44" + > +
+

+ {categoryName.toUpperCase().split(' ')[0]}{' '} + + {categoryName.toUpperCase().split(' ').slice(1).join(' ') || ''} + +

+ + ({partnersToShow.length}) + +
+ + {/* Partners Grid */} +
+ {partnersToShow.map((partner: Partner) => ( +
+ {/* Logo Card - Reverted to Original Styling */} +
+ {partner.name} { + // Fallback to initials if logo fails to load + const target = e.target as HTMLImageElement + target.style.display = 'none' + const parent = target.parentElement + if (parent) { + const fallback = document.createElement('div') + fallback.className = 'text-2xl font-bold text-gray-600' + fallback.textContent = partner.name.split(' ').map((w: string) => w[0]).join('').slice(0, 2) + parent.appendChild(fallback) + } + }} + /> +
+ + {/* Partner Name */} +
+

+ {partner.name} +

+
+
+ ))} +
+
+ ); + })} + + {/* Become a Partner CTA */} + + +
+
+ + ) +} diff --git a/app/partners/page.tsx b/app/partners/page.tsx index 6819c49..0fbce4d 100644 --- a/app/partners/page.tsx +++ b/app/partners/page.tsx @@ -1,369 +1,21 @@ -"use client" - -import { useState, useEffect, useRef } from 'react' -import Script from 'next/script' -import { cn } from "@/lib/utils" -import Hero from "@/components/Hero" -import HeroCard from "@/components/HeroCard" -import CTA from "@/components/CTA" +import type { Metadata } from 'next' +import PartnersContent from './PartnersContent' export const dynamic = 'force-dynamic' -interface Partner { - id: string - name: string - category: string - logoUrl: string -} - -// Fetch partners from API -async function fetchPartners(): Promise { - try { - // Use absolute URL in production, relative in development - const baseUrl = typeof window !== 'undefined' - ? window.location.origin - : process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; - - const response = await fetch(`${baseUrl}/api/partners`, { - cache: 'no-store', // Ensure fresh data - }); - - if (!response.ok) { - console.error(`API error: ${response.status} ${response.statusText}`); - throw new Error('Failed to fetch partners'); - } - - return await response.json(); - } catch (error) { - console.error('Error fetching partners:', error); - return []; - } +export const metadata: Metadata = { + title: 'Our Partners', + description: + 'Meet START Munich\'s partners — leading companies, VCs, research institutions, and ecosystem players that empower our student entrepreneur community.', + alternates: { canonical: 'https://www.startmunich.de/partners' }, + openGraph: { + url: 'https://www.startmunich.de/partners', + title: 'Our Partners | START Munich', + description: + 'Meet START Munich\'s partners — leading companies, VCs, research institutions, and ecosystem players that empower our student entrepreneur community.', + }, } -interface PartnersByCategory { - [category: string]: Partner[] -} - -const CATEGORY_ORDER = [ - 'TECHNOLOGY', - "RESEARCH", - 'VENTURE CAPITAL', - 'ECOSYSTEM', - 'INITIATIVES', - 'STARTUP', - 'INDUSTRY', - 'OTHER' -]; - export default function PartnersPage() { - const [loading, setLoading] = useState(true) - const [partners, setPartners] = useState([]) - const [partnersByCategory, setPartnersByCategory] = useState({}) - const [activeCategory, setActiveCategory] = useState('ALL') - const [searchQuery, setSearchQuery] = useState('') - const categoryRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}) - - // Animation states for numbers - const [animatedPartners, setAnimatedPartners] = useState(0) - const [animatedCategories, setAnimatedCategories] = useState(0) - const hasAnimatedRef = useRef(false) - - useEffect(() => { - const loadPartners = async () => { - setLoading(true) - const data = await fetchPartners() - setPartners(data) - - // Group partners by category - const grouped = data.reduce((acc: PartnersByCategory, partner) => { - const category = partner.category ? partner.category.toUpperCase() : 'OTHER' - if (!acc[category]) { - acc[category] = [] - } - acc[category].push(partner) - return acc - }, {}) - - setPartnersByCategory(grouped) - setLoading(false) - } - - loadPartners() - }, []) - - // Calculate stats - const totalPartners = partners.length; - // Count unique categories actually present in data - const totalCategories = Object.keys(partnersByCategory).length; - - // Animate numbers - useEffect(() => { - if (!loading && !hasAnimatedRef.current && totalPartners > 0) { - hasAnimatedRef.current = true - - const duration = 1500 // 1.5 seconds - const steps = 60 - const interval = duration / steps - - const partnersIncrement = totalPartners / steps - const categoriesIncrement = totalCategories / steps - - let partnersCurrent = 0 - let categoriesCurrent = 0 - let step = 0 - - const timer = setInterval(() => { - step++ - partnersCurrent += partnersIncrement - categoriesCurrent += categoriesIncrement - - if (step >= steps) { - setAnimatedPartners(totalPartners) - setAnimatedCategories(totalCategories) - clearInterval(timer) - } else { - setAnimatedPartners(partnersCurrent) - setAnimatedCategories(categoriesCurrent) - } - }, interval) - - return () => clearInterval(timer) - } - }, [loading, totalPartners, totalCategories]) - - // Sort categories based on predefined order - const sortedCategories = Object.keys(partnersByCategory).sort((a, b) => { - const indexA = CATEGORY_ORDER.indexOf(a); - const indexB = CATEGORY_ORDER.indexOf(b); - - if (indexA !== -1 && indexB !== -1) return indexA - indexB; - if (indexA !== -1) return -1; - if (indexB !== -1) return 1; - return a.localeCompare(b); - }); - - const scrollToCategory = (category: string) => { - setActiveCategory(category) - if (category === 'ALL') { - window.scrollTo({ top: 0, behavior: 'smooth' }) - return - } - const element = categoryRefs.current[category] - if (element) { - const yOffset = -180; // Offset for sticky navigation (80px) + filter bar (~100px) - const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ top: y, behavior: 'smooth' }); - } - } - - const filteredPartners = (category: string) => { - const categoryPartners = partnersByCategory[category] || []; - if (!searchQuery) return categoryPartners; - return categoryPartners.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase())); - } - - if (loading) { - return ( -
-
-
-

LOADING PARTNERS...

-
-
- ) - } - - return ( - <> - - -
- - {/* Hero Section - Restored Old Style with Stats Added */} - - OUR -
- PARTNERS - - } - description="Powering the next generation of entrepreneurs through world-class collaboration." - > -
- {/** Stat 1 **/} - -
- - {Math.floor(animatedPartners)} - - + -
-

Partners

-
- - {/** Stat 2 **/} - -
- - {Math.floor(animatedCategories)} - -
-

Industries

-
-
-
- - {/* Sticky Filter & Search Bar */} -
-
-
- - {/* Category Pills */} -
- - {sortedCategories.map((category) => ( - - ))} -
- - {/* Search Input */} -
-
- - - -
- setSearchQuery(e.target.value)} - /> -
-
-
-
- - {/* Content Below Hero */} -
- - {sortedCategories.map((categoryName) => { - const partnersToShow = filteredPartners(categoryName); - if (partnersToShow.length === 0) return null; - - return ( -
{ - categoryRefs.current[categoryName] = el; - }} - className="scroll-mt-44" - > -
-

- {categoryName.toUpperCase().split(' ')[0]}{' '} - - {categoryName.toUpperCase().split(' ').slice(1).join(' ') || ''} - -

- - ({partnersToShow.length}) - -
- - {/* Partners Grid */} -
- {partnersToShow.map((partner: Partner) => ( -
- {/* Logo Card - Reverted to Original Styling */} -
- {partner.name} { - // Fallback to initials if logo fails to load - const target = e.target as HTMLImageElement - target.style.display = 'none' - const parent = target.parentElement - if (parent) { - const fallback = document.createElement('div') - fallback.className = 'text-2xl font-bold text-gray-600' - fallback.textContent = partner.name.split(' ').map((w: string) => w[0]).join('').slice(0, 2) - parent.appendChild(fallback) - } - }} - /> -
- - {/* Partner Name */} -
-

- {partner.name} -

-
-
- ))} -
-
- ); - })} - - {/* Become a Partner CTA */} - - -
-
- - ) + return } diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..74bbd0a --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,11 @@ +import { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + }, + sitemap: 'https://www.startmunich.de/sitemap.xml', + } +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..e8efcaa --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,21 @@ +import { MetadataRoute } from 'next' + +const BASE = 'https://www.startmunich.de' + +// Static last-modified date — update when content changes significantly +const LAST_MODIFIED = '2025-04-06' + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { url: `${BASE}/`, lastModified: LAST_MODIFIED, changeFrequency: 'weekly', priority: 1.0 }, + { url: `${BASE}/about-us`, lastModified: LAST_MODIFIED, changeFrequency: 'monthly', priority: 0.8 }, + { url: `${BASE}/events`, lastModified: LAST_MODIFIED, changeFrequency: 'weekly', priority: 0.9 }, + { url: `${BASE}/startups`, lastModified: LAST_MODIFIED, changeFrequency: 'weekly', priority: 0.8 }, + { url: `${BASE}/partners`, lastModified: LAST_MODIFIED, changeFrequency: 'monthly', priority: 0.7 }, + { url: `${BASE}/for-partners`, lastModified: LAST_MODIFIED, changeFrequency: 'monthly', priority: 0.7 }, + { url: `${BASE}/members`, lastModified: LAST_MODIFIED, changeFrequency: 'monthly', priority: 0.7 }, + { url: `${BASE}/member-journey`, lastModified: LAST_MODIFIED, changeFrequency: 'monthly', priority: 0.7 }, + { url: `${BASE}/member-network`, lastModified: LAST_MODIFIED, changeFrequency: 'monthly', priority: 0.6 }, + { url: `${BASE}/apply`, lastModified: LAST_MODIFIED, changeFrequency: 'weekly', priority: 0.8 }, + ] +} diff --git a/app/startup-details/[id]/StartupDetailsContent.tsx b/app/startup-details/[id]/StartupDetailsContent.tsx new file mode 100644 index 0000000..bcf08e1 --- /dev/null +++ b/app/startup-details/[id]/StartupDetailsContent.tsx @@ -0,0 +1,263 @@ +"use client" + +import { useState, useEffect, use } from "react" +import Link from "next/link" +import { notFound, useRouter } from "next/navigation" +import type { Company } from "@/lib/types" + +// Fetch companies from API +async function fetchCompanies(): Promise { + try { + const response = await fetch('/api/startups'); + if (!response.ok) throw new Error('Failed to fetch startups'); + return await response.json(); + } catch (error) { + console.error('Error fetching startups:', error); + return []; + } +} + +export default function StartupDetailsPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) + const router = useRouter() + const [company, setCompany] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const loadCompany = async () => { + setLoading(true) + const companies = await fetchCompanies() + const foundCompany = companies.find(c => c.id.toString() === id) + + if (!foundCompany) { + notFound() + return + } + + setCompany(foundCompany) + setLoading(false) + } + loadCompany() + }, [id]) + + if (loading) { + return ( +
+
+

Loading startup details...

+
+
+ ) + } + + if (!company) { + return notFound() + } + + return ( +
+
+ {/* Company Details */} +
+ {/* Close Button */} + + +
+ {/* Header with Logo */} +
+
+ {`${company.name} +
+ +
+

{company.name}

+ + {/* Categories */} +
+ {company.category.map((cat, idx) => ( + + {cat} + + ))} +
+ + {/* Quick Info */} +
+ + {company.lastUpdated && ( + <> + Infromation Updated {new Date(company.lastUpdated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })} + + )} +
+ {/* Links */} +
+ + + + + Visit Website + + {company.companyLinkedin && ( + + + + + Visit LinkedIn + + )} +
+
+
+ + {/* Description */} +
+

About

+

{company.description}

+
+ + {/* Supporting Programs */} + {company.supportingPrograms && ( +
+

Programs

+
+ {company.supportingPrograms.split(',').filter(p => p.trim()).map((program, idx) => ( + + {program.trim()} + + ))} +
+
+ )} + + {/* Company Info */} +
+

Company Info

+
+
+

Founded

+

{company.foundingYear}

+
+
+
+ + + {/* Founders */} + {company.founders.length > 0 && ( +
+

+ {company.founders.length > 1 ? 'Founders' : 'Founder'} +

+
+ {company.founders.map((founder, idx) => ( +
+ {founder.linkedinUrl ? ( + + {founder.name} + + ) : ( + {founder.name} + )} +
+

+ {founder.name} + {founder.batch && ( + + (Batch: {founder.batch}) + + )} +

+

{founder.role}

+
+ {founder.linkedinUrl && ( + + + + + + )} +
+ ))} +
+
+ )} + + + {/* Footer Links */} + {/*
+ + Visit Website + + + + + {company.companyLinkedin && ( + + + + + LinkedIn + + )} +
*/} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/startup-details/[id]/page.tsx b/app/startup-details/[id]/page.tsx index bcf08e1..78c3be0 100644 --- a/app/startup-details/[id]/page.tsx +++ b/app/startup-details/[id]/page.tsx @@ -1,263 +1,54 @@ -"use client" +import type { Metadata } from 'next' +import StartupDetailsContent from './StartupDetailsContent' -import { useState, useEffect, use } from "react" -import Link from "next/link" -import { notFound, useRouter } from "next/navigation" -import type { Company } from "@/lib/types" +const NOCODB_API_TOKEN = process.env.NOCODB_API_TOKEN +const NOCODB_BASE_URL = process.env.NOCODB_BASE_URL || 'https://ndb.startmunich.de' +const NOCODB_STARTUPS_TABLE_ID = process.env.NOCODB_STARTUPS_TABLE_ID -// Fetch companies from API -async function fetchCompanies(): Promise { - try { - const response = await fetch('/api/startups'); - if (!response.ok) throw new Error('Failed to fetch startups'); - return await response.json(); - } catch (error) { - console.error('Error fetching startups:', error); - return []; - } -} - -export default function StartupDetailsPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = use(params) - const router = useRouter() - const [company, setCompany] = useState(null) - const [loading, setLoading] = useState(true) +export async function generateMetadata( + { params }: { params: Promise<{ id: string }> } +): Promise { + const { id } = await params - useEffect(() => { - const loadCompany = async () => { - setLoading(true) - const companies = await fetchCompanies() - const foundCompany = companies.find(c => c.id.toString() === id) - - if (!foundCompany) { - notFound() - return + try { + if (NOCODB_API_TOKEN && NOCODB_STARTUPS_TABLE_ID) { + const res = await fetch( + `${NOCODB_BASE_URL}/api/v2/tables/${NOCODB_STARTUPS_TABLE_ID}/records?where=(Id,eq,${id})&limit=1`, + { + headers: { 'xc-token': NOCODB_API_TOKEN }, + next: { revalidate: 3600 }, + } + ) + if (res.ok) { + const data = await res.json() + const startup = data?.list?.[0] + if (startup) { + const name = startup['Startup Name'] || 'Startup' + const description = startup['Description'] || `Learn about ${name}, a startup founded by START Munich alumni.` + return { + title: name, + description, + alternates: { canonical: `https://www.startmunich.de/startup-details/${id}` }, + openGraph: { + url: `https://www.startmunich.de/startup-details/${id}`, + title: `${name} | START Munich`, + description, + }, + } + } } - - setCompany(foundCompany) - setLoading(false) } - loadCompany() - }, [id]) - - if (loading) { - return ( -
-
-

Loading startup details...

-
-
- ) + } catch { + // fall through to default } - if (!company) { - return notFound() + return { + title: 'Startup', + description: 'A startup founded by START Munich alumni.', + alternates: { canonical: `https://www.startmunich.de/startup-details/${id}` }, } +} - return ( -
-
- {/* Company Details */} -
- {/* Close Button */} - - -
- {/* Header with Logo */} -
-
- {`${company.name} -
- -
-

{company.name}

- - {/* Categories */} -
- {company.category.map((cat, idx) => ( - - {cat} - - ))} -
- - {/* Quick Info */} -
- - {company.lastUpdated && ( - <> - Infromation Updated {new Date(company.lastUpdated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })} - - )} -
- {/* Links */} -
- - - - - Visit Website - - {company.companyLinkedin && ( - - - - - Visit LinkedIn - - )} -
-
-
- - {/* Description */} -
-

About

-

{company.description}

-
- - {/* Supporting Programs */} - {company.supportingPrograms && ( -
-

Programs

-
- {company.supportingPrograms.split(',').filter(p => p.trim()).map((program, idx) => ( - - {program.trim()} - - ))} -
-
- )} - - {/* Company Info */} -
-

Company Info

-
-
-

Founded

-

{company.foundingYear}

-
-
-
- - - {/* Founders */} - {company.founders.length > 0 && ( -
-

- {company.founders.length > 1 ? 'Founders' : 'Founder'} -

-
- {company.founders.map((founder, idx) => ( -
- {founder.linkedinUrl ? ( - - {founder.name} - - ) : ( - {founder.name} - )} -
-

- {founder.name} - {founder.batch && ( - - (Batch: {founder.batch}) - - )} -

-

{founder.role}

-
- {founder.linkedinUrl && ( - - - - - - )} -
- ))} -
-
- )} - - - {/* Footer Links */} - {/*
- - Visit Website - - - - - {company.companyLinkedin && ( - - - - - LinkedIn - - )} -
*/} -
-
-
-
- ) -} \ No newline at end of file +export default function StartupDetailsPage({ params }: { params: Promise<{ id: string }> }) { + return +} diff --git a/components/StartupCard.tsx b/app/startups/StartupCard.tsx similarity index 100% rename from components/StartupCard.tsx rename to app/startups/StartupCard.tsx diff --git a/app/startups/StartupsContent.tsx b/app/startups/StartupsContent.tsx new file mode 100644 index 0000000..048f3fc --- /dev/null +++ b/app/startups/StartupsContent.tsx @@ -0,0 +1,668 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" +import type { Company } from "@/lib/types" +import StartupCard from "./StartupCard" +import Hero from "@/components/Hero" +import HeroCard from "@/components/HeroCard" +import CTA from "@/components/CTA" +import { useAnimatedNumber } from "@/lib/useAnimatedNumber" + +export const dynamic = 'force-dynamic' +import Image from "next/image" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import Script from "next/script" + +// Fetch companies from API +async function fetchCompanies(): Promise { + try { + // Use absolute URL in production, relative in development + const baseUrl = typeof window !== 'undefined' + ? window.location.origin + : process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; + + const response = await fetch(`${baseUrl}/api/startups`, { + cache: 'no-store', // Ensure fresh data + }); + + if (!response.ok) { + console.error(`API error: ${response.status} ${response.statusText}`); + throw new Error('Failed to fetch startups'); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching startups:', error); + return []; + } +} + +// Helper function to get preview text (first 30 words) +function getPreviewText(text: string): string { + if (!text) return ''; + + const words = text.split(/\s+/); + const maxWords = 30; + + if (words.length <= maxWords) { + return text; + } + + return words.slice(0, maxWords).join(' ') + '...'; +} + +export default function StartupsPage() { + const [companies, setCompanies] = useState([]) + const [loading, setLoading] = useState(true) + const [selectedCategory, setSelectedCategory] = useState("all") + const [selectedYear, setSelectedYear] = useState("all") + const [selectedProgram, setSelectedProgram] = useState("all") + const [searchQuery, setSearchQuery] = useState("") + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 12 // Reduced to approximate 3000px height (about 5-6 cards) + + // Tooltip states for section explanations + const [showFeaturedInfo, setShowFeaturedInfo] = useState(false) + const [showYCInfo, setShowYCInfo] = useState(false) + const [showEWORInfo, setShowEWORInfo] = useState(false) + + // Load companies on mount + useEffect(() => { + const loadCompanies = async () => { + setLoading(true) + const data = await fetchCompanies() + setCompanies(data) + setLoading(false) + + // Restore scroll position after returning from startup detail + const savedScroll = sessionStorage.getItem('startups-scroll') + if (savedScroll) { + requestAnimationFrame(() => { + window.scrollTo(0, parseInt(savedScroll)) + sessionStorage.removeItem('startups-scroll') + }) + } + } + loadCompanies() + }, []) + + // Extract unique categories + const allCategories = Array.from( + new Set( + companies.flatMap(company => company.category) + ) + ).sort() + + // Extract unique founding years + const allYears = Array.from( + new Set( + companies.map(company => company.foundingYear.toString()) + ) + ).sort() + + // Extract unique supporting programs + const allPrograms = Array.from( + new Set( + companies + .map(company => company.supportingPrograms) + .filter((program): program is string => program !== undefined && program.trim() !== '') + .flatMap(program => program.split(',').map(p => p.trim())) + ) + ).sort() + + // Filter companies based on all selected filters + const filteredCompanies = companies + .filter(company => { + const matchesCategory = selectedCategory === "all" || + company.category.some(cat => cat.toLowerCase().includes(selectedCategory.toLowerCase())) + + const matchesYear = selectedYear === "all" || + company.foundingYear.toString() === selectedYear + + const matchesProgram = selectedProgram === "all" || + (company.supportingPrograms && + company.supportingPrograms.split(',').some(program => + program.trim().toLowerCase().includes(selectedProgram.toLowerCase()) + )) + + const matchesSearch = searchQuery === "" || + company.name.toLowerCase().includes(searchQuery.toLowerCase()) || + company.founders.some(founder => founder.name.toLowerCase().includes(searchQuery.toLowerCase())) + + return matchesCategory && matchesYear && matchesProgram && matchesSearch + }) + + // Pagination calculations + const totalPages = Math.ceil(filteredCompanies.length / itemsPerPage) + const startIndex = (currentPage - 1) * itemsPerPage + const endIndex = startIndex + itemsPerPage + const paginatedCompanies = filteredCompanies.slice(startIndex, endIndex) + + // Reset to page 1 when filters change + useEffect(() => { + setCurrentPage(1) + }, [selectedCategory, selectedYear, selectedProgram, searchQuery]) + + // Calculate total statistics + const totalStartups = companies.length + + // Get spotlight startups (show all featured startups) + const spotlightStartups = companies.filter(company => company.isSpotlight) + + // Get Y Combinator startups (show all YC alumni) + const yCombinatorStartups = companies.filter(company => company.isYCombinator) + + // Get EWOR startups + const eworStartups = companies.filter(company => company.isEWOR) + + // Use animated number hook for statistics - hardcoded 3B+ funding + const animatedStartups = useAnimatedNumber(totalStartups, loading) + const animatedFunding = useAnimatedNumber(3, loading) + + if (loading) { + return ( +
+
+

Loading startups...

+
+
+ ) + } + + return ( + <> + + +
+ {/* Hero Section with Full-Width Image */} + + START MUNICH +
+ STARTUPS + + } + description="Discover the innovative companies built by our community of ambitious student entrepreneurs" + > +
+ {/** Stat 1 **/} + +
+ + {Math.floor(animatedStartups)} + + + +
+

Companies

+
+ + {/** Stat 2 **/} + +
+ + + {Math.floor(animatedFunding)} + + B+ +
+

Funding

+
+
+
+ + + {/* Content Below Hero */} +
+ + {/* Growth Champions Section */} + {spotlightStartups.length > 0 && ( +
+
+
+

+ Featured Startups +

+
setShowFeaturedInfo(true)} onMouseLeave={() => setShowFeaturedInfo(false)}> + + {showFeaturedInfo && ( +
+
+
+
+
+
+

Featured Startups

+
+

+ These startups are super successful and have achieved a valuation of over €10 million, showcasing exceptional growth and market impact. +

+
+
+ )} +
+
+
+
+ {spotlightStartups.map((company, index) => ( + + ))} +
+
+ )} + + {/* MTZ Location Partner Info Card */} +
+ {/* Decorative Elements */} +
+
+ +
+
+ {/* Left Side - Content */} +
+
+
+ + + + +
+

+ Our Location Partner: MTZ +

+
+ +
+

+ Startups with the MTZ label are located at our location partner, the MTZ (Münchner Technologiezentrum), one of Munich's leading innovation hubs. START Munich is also located at the MTZ, fostering a vibrant community of entrepreneurs and innovators. +

+ +
+
+ + + + Agnes-Pockels-Bogen 1, 80992 München +
+
+ + + + Home to START Munich and innovative startups +
+
+
+
+ + {/* Right Side - Map */} +
+ +
+
+
+
+ + {/* Y Combinator Section */} + {yCombinatorStartups.length > 0 && ( +
+
+
+

+ Y Combinator Alumni +

+
setShowYCInfo(true)} onMouseLeave={() => setShowYCInfo(false)}> + + {showYCInfo && ( +
+
+
+
+
+
+

Y Combinator

+
+

+ Y Combinator is the world's most prestigious startup accelerator, having funded over 4,000 companies including Airbnb, Dropbox, Stripe, and Reddit. These alumni have gone through YC's intensive 3-month program. +

+
+
+ )} +
+
+
+
+ {yCombinatorStartups.map((company, index) => ( + + ))} +
+
+ )} + + {/* EWOR Section */} + {eworStartups.length > 0 && ( +
+
+
+

+ EWOR Alumni +

+
setShowEWORInfo(true)} onMouseLeave={() => setShowEWORInfo(false)}> + + {showEWORInfo && ( +
+
+
+
+
+
+

EWOR

+
+

+ EWOR is a global fellowship program that supports ambitious founders by providing funding, mentorship, and a community of exceptional entrepreneurs building the next generation of impactful companies. +

+
+
+ )} +
+
+
+
+ {eworStartups.map((company, index) => ( + + ))} +
+
+ )} + + {/* Join Our Startups - Innovative CTA Section */} +
+
+ +
+ {/* Floating animated orbs */} +
+
+ +
+ {/* Left Side - Content */} +
+
+ We're Hiring +
+ +

+ Join the{" "} + + Next Big Thing + +

+ +

+ Our startups are looking for talented individuals to join their teams. + Explore open positions and be part of building innovative products that matter. +

+
+ + {/* Right Side - CTA Button */} + +
+
+
+
+ + {/* Filter Section */} +
+
+ {/* Category Filter */} +
+ + +
+ + {/* Founding Year Filter */} +
+ + +
+
+ + {/* Search Bar and Clear Filters */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Type startup or founder name..." + className="w-full px-4 py-2.5 bg-white/5 border border-white/20 text-white placeholder-gray-500 focus:ring-1 focus:ring-white/30 hover:bg-white/10 transition-all rounded focus:outline-none" + /> +
+
+ +
+
+
+ + {/* Company List - Grid Layout */} +
+ {paginatedCompanies.map((company) => ( + + ))} +
+ + {filteredCompanies.length === 0 && ( +
+
+

No Results Found

+

Try adjusting your filters to see more startups

+
+
+ )} + + {/* Pagination Controls */} + {filteredCompanies.length > 0 && totalPages > 1 && ( +
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + ))} +
+ + +
+ )} +
+ + {/* Footer CTA Section */} +
+ Join START Munich and get the support, network, and resources you need to turn your idea into reality. Our community has helped launch {companies.length}+ startups — yours could be next.} + buttons={[ + { label: "Discover the Member Journey", href: "/member-journey" }, + { label: "Apply Now", href: "/join-start/2026", variant: "secondary" } + ]} + /> +
+
+ + ) +} \ No newline at end of file diff --git a/app/startups/page.tsx b/app/startups/page.tsx index 4f0868d..a2dff00 100644 --- a/app/startups/page.tsx +++ b/app/startups/page.tsx @@ -1,668 +1,21 @@ -"use client" - -import { useState, useEffect } from "react" -import Link from "next/link" -import type { Company } from "@/lib/types" -import StartupCard from "@/components/StartupCard" -import Hero from "@/components/Hero" -import HeroCard from "@/components/HeroCard" -import CTA from "@/components/CTA" -import { useAnimatedNumber } from "@/lib/useAnimatedNumber" +import type { Metadata } from 'next' +import StartupsContent from './StartupsContent' export const dynamic = 'force-dynamic' -import Image from "next/image" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import Script from "next/script" - -// Fetch companies from API -async function fetchCompanies(): Promise { - try { - // Use absolute URL in production, relative in development - const baseUrl = typeof window !== 'undefined' - ? window.location.origin - : process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; - - const response = await fetch(`${baseUrl}/api/startups`, { - cache: 'no-store', // Ensure fresh data - }); - - if (!response.ok) { - console.error(`API error: ${response.status} ${response.statusText}`); - throw new Error('Failed to fetch startups'); - } - return await response.json(); - } catch (error) { - console.error('Error fetching startups:', error); - return []; - } -} - -// Helper function to get preview text (first 30 words) -function getPreviewText(text: string): string { - if (!text) return ''; - - const words = text.split(/\s+/); - const maxWords = 30; - - if (words.length <= maxWords) { - return text; - } - - return words.slice(0, maxWords).join(' ') + '...'; +export const metadata: Metadata = { + title: 'Startups', + description: + 'Explore startups founded by START Munich alumni — from Y Combinator companies to cutting-edge deep tech. Discover the next generation of Munich founders.', + alternates: { canonical: 'https://www.startmunich.de/startups' }, + openGraph: { + url: 'https://www.startmunich.de/startups', + title: 'Startups | START Munich', + description: + 'Explore startups founded by START Munich alumni — from Y Combinator companies to cutting-edge deep tech. Discover the next generation of Munich founders.', + }, } export default function StartupsPage() { - const [companies, setCompanies] = useState([]) - const [loading, setLoading] = useState(true) - const [selectedCategory, setSelectedCategory] = useState("all") - const [selectedYear, setSelectedYear] = useState("all") - const [selectedProgram, setSelectedProgram] = useState("all") - const [searchQuery, setSearchQuery] = useState("") - const [currentPage, setCurrentPage] = useState(1) - const itemsPerPage = 12 // Reduced to approximate 3000px height (about 5-6 cards) - - // Tooltip states for section explanations - const [showFeaturedInfo, setShowFeaturedInfo] = useState(false) - const [showYCInfo, setShowYCInfo] = useState(false) - const [showEWORInfo, setShowEWORInfo] = useState(false) - - // Load companies on mount - useEffect(() => { - const loadCompanies = async () => { - setLoading(true) - const data = await fetchCompanies() - setCompanies(data) - setLoading(false) - - // Restore scroll position after returning from startup detail - const savedScroll = sessionStorage.getItem('startups-scroll') - if (savedScroll) { - requestAnimationFrame(() => { - window.scrollTo(0, parseInt(savedScroll)) - sessionStorage.removeItem('startups-scroll') - }) - } - } - loadCompanies() - }, []) - - // Extract unique categories - const allCategories = Array.from( - new Set( - companies.flatMap(company => company.category) - ) - ).sort() - - // Extract unique founding years - const allYears = Array.from( - new Set( - companies.map(company => company.foundingYear.toString()) - ) - ).sort() - - // Extract unique supporting programs - const allPrograms = Array.from( - new Set( - companies - .map(company => company.supportingPrograms) - .filter((program): program is string => program !== undefined && program.trim() !== '') - .flatMap(program => program.split(',').map(p => p.trim())) - ) - ).sort() - - // Filter companies based on all selected filters - const filteredCompanies = companies - .filter(company => { - const matchesCategory = selectedCategory === "all" || - company.category.some(cat => cat.toLowerCase().includes(selectedCategory.toLowerCase())) - - const matchesYear = selectedYear === "all" || - company.foundingYear.toString() === selectedYear - - const matchesProgram = selectedProgram === "all" || - (company.supportingPrograms && - company.supportingPrograms.split(',').some(program => - program.trim().toLowerCase().includes(selectedProgram.toLowerCase()) - )) - - const matchesSearch = searchQuery === "" || - company.name.toLowerCase().includes(searchQuery.toLowerCase()) || - company.founders.some(founder => founder.name.toLowerCase().includes(searchQuery.toLowerCase())) - - return matchesCategory && matchesYear && matchesProgram && matchesSearch - }) - - // Pagination calculations - const totalPages = Math.ceil(filteredCompanies.length / itemsPerPage) - const startIndex = (currentPage - 1) * itemsPerPage - const endIndex = startIndex + itemsPerPage - const paginatedCompanies = filteredCompanies.slice(startIndex, endIndex) - - // Reset to page 1 when filters change - useEffect(() => { - setCurrentPage(1) - }, [selectedCategory, selectedYear, selectedProgram, searchQuery]) - - // Calculate total statistics - const totalStartups = companies.length - - // Get spotlight startups (show all featured startups) - const spotlightStartups = companies.filter(company => company.isSpotlight) - - // Get Y Combinator startups (show all YC alumni) - const yCombinatorStartups = companies.filter(company => company.isYCombinator) - - // Get EWOR startups - const eworStartups = companies.filter(company => company.isEWOR) - - // Use animated number hook for statistics - hardcoded 3B+ funding - const animatedStartups = useAnimatedNumber(totalStartups, loading) - const animatedFunding = useAnimatedNumber(3, loading) - - if (loading) { - return ( -
-
-

Loading startups...

-
-
- ) - } - - return ( - <> - - -
- {/* Hero Section with Full-Width Image */} - - START MUNICH -
- STARTUPS - - } - description="Discover the innovative companies built by our community of ambitious student entrepreneurs" - > -
- {/** Stat 1 **/} - -
- - {Math.floor(animatedStartups)} - - + -
-

Companies

-
- - {/** Stat 2 **/} - -
- - - {Math.floor(animatedFunding)} - - B+ -
-

Funding

-
-
-
- - - {/* Content Below Hero */} -
- - {/* Growth Champions Section */} - {spotlightStartups.length > 0 && ( -
-
-
-

- Featured Startups -

-
setShowFeaturedInfo(true)} onMouseLeave={() => setShowFeaturedInfo(false)}> - - {showFeaturedInfo && ( -
-
-
-
-
-
-

Featured Startups

-
-

- These startups are super successful and have achieved a valuation of over €10 million, showcasing exceptional growth and market impact. -

-
-
- )} -
-
-
-
- {spotlightStartups.map((company, index) => ( - - ))} -
-
- )} - - {/* MTZ Location Partner Info Card */} -
- {/* Decorative Elements */} -
-
- -
-
- {/* Left Side - Content */} -
-
-
- - - - -
-

- Our Location Partner: MTZ -

-
- -
-

- Startups with the MTZ label are located at our location partner, the MTZ (Münchner Technologiezentrum), one of Munich's leading innovation hubs. START Munich is also located at the MTZ, fostering a vibrant community of entrepreneurs and innovators. -

- -
-
- - - - Agnes-Pockels-Bogen 1, 80992 München -
-
- - - - Home to START Munich and innovative startups -
-
-
-
- - {/* Right Side - Map */} -
- -
-
-
-
- - {/* Y Combinator Section */} - {yCombinatorStartups.length > 0 && ( -
-
-
-

- Y Combinator Alumni -

-
setShowYCInfo(true)} onMouseLeave={() => setShowYCInfo(false)}> - - {showYCInfo && ( -
-
-
-
-
-
-

Y Combinator

-
-

- Y Combinator is the world's most prestigious startup accelerator, having funded over 4,000 companies including Airbnb, Dropbox, Stripe, and Reddit. These alumni have gone through YC's intensive 3-month program. -

-
-
- )} -
-
-
-
- {yCombinatorStartups.map((company, index) => ( - - ))} -
-
- )} - - {/* EWOR Section */} - {eworStartups.length > 0 && ( -
-
-
-

- EWOR Alumni -

-
setShowEWORInfo(true)} onMouseLeave={() => setShowEWORInfo(false)}> - - {showEWORInfo && ( -
-
-
-
-
-
-

EWOR

-
-

- EWOR is a global fellowship program that supports ambitious founders by providing funding, mentorship, and a community of exceptional entrepreneurs building the next generation of impactful companies. -

-
-
- )} -
-
-
-
- {eworStartups.map((company, index) => ( - - ))} -
-
- )} - - {/* Join Our Startups - Innovative CTA Section */} -
-
- -
- {/* Floating animated orbs */} -
-
- -
- {/* Left Side - Content */} -
-
- We're Hiring -
- -

- Join the{" "} - - Next Big Thing - -

- -

- Our startups are looking for talented individuals to join their teams. - Explore open positions and be part of building innovative products that matter. -

-
- - {/* Right Side - CTA Button */} - -
-
-
-
- - {/* Filter Section */} -
-
- {/* Category Filter */} -
- - -
- - {/* Founding Year Filter */} -
- - -
-
- - {/* Search Bar and Clear Filters */} -
-
- - setSearchQuery(e.target.value)} - placeholder="Type startup or founder name..." - className="w-full px-4 py-2.5 bg-white/5 border border-white/20 text-white placeholder-gray-500 focus:ring-1 focus:ring-white/30 hover:bg-white/10 transition-all rounded focus:outline-none" - /> -
-
- -
-
-
- - {/* Company List - Grid Layout */} -
- {paginatedCompanies.map((company) => ( - - ))} -
- - {filteredCompanies.length === 0 && ( -
-
-

No Results Found

-

Try adjusting your filters to see more startups

-
-
- )} - - {/* Pagination Controls */} - {filteredCompanies.length > 0 && totalPages > 1 && ( -
- - -
- {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( - - ))} -
- - -
- )} -
- - {/* Footer CTA Section */} -
- Join START Munich and get the support, network, and resources you need to turn your idea into reality. Our community has helped launch {companies.length}+ startups — yours could be next.} - buttons={[ - { label: "Discover the Member Journey", href: "/member-journey" }, - { label: "Apply Now", href: "/join-start/2026", variant: "secondary" } - ]} - /> -
-
- - ) -} \ No newline at end of file + return +} diff --git a/components/CTA.tsx b/components/CTA.tsx index 8d9beb9..cbe773f 100644 --- a/components/CTA.tsx +++ b/components/CTA.tsx @@ -26,7 +26,7 @@ export default function CTA({ }: CTAProps) { const renderButton = (button: CTAButton, index: number) => { const isPrimary = button.variant === 'primary' || (index === 0 && !button.variant) - const baseClasses = "px-8 py-3 font-bold rounded-lg transition-all duration-300" + const baseClasses = "px-8 py-3 font-bold rounded-full transition-all duration-300" const primaryClasses = "bg-[#d0006f] hover:bg-[#d0006f]/90 text-white hover:shadow-lg hover:shadow-[#d0006f]/50" const secondaryClasses = "border border-[#d0006f] text-[#d0006f] hover:bg-[#d0006f]/10" diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 2a178bf..e011d14 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -35,7 +35,7 @@ export default function Navigation() {
{/* Logo */} - + START Munich HOME @@ -115,7 +115,7 @@ export default function Navigation() { setIsCommunityOpen(false)} className="group block px-6 py-3.5 text-white text-base font-bold hover:bg-brand-pink transition-all duration-200" > @@ -243,7 +243,7 @@ export default function Navigation() {
{/* Header row with logo and close button */}
- setIsMobileMenuOpen(false)}> + setIsMobileMenuOpen(false)}> START Munich setIsMobileMenuOpen(false)} > @@ -314,7 +314,7 @@ export default function Navigation() { Our Members setIsMobileMenuOpen(false)} > diff --git a/lib/mockMembers.ts b/lib/mockMembers.ts deleted file mode 100644 index 31fb3c9..0000000 --- a/lib/mockMembers.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Mock member data for development and testing -// Base members that are replicated across all batches - -export interface Member { - id: number - name: string - batch: string - role: string - study?: string - company?: string - linkedinUrl?: string - imageUrl: string - bio?: string - expertise?: string[] - achievements?: string - gender?: string -} - -// Base member templates (8 unique members) -const baseMemberTemplates = [ - { - name: "Sarah Chen", - role: "CEO & Co-Founder", - study: "Computer Science", - company: "TechFlow AI", - linkedinUrl: "https://linkedin.com/in/sarahchen", - imageUrl: "/example.png", - bio: "Passionate about using AI to solve real-world problems. Previously at Google and Meta.", - expertise: ["Machine Learning", "Product Strategy", "Leadership"], - achievements: "Raised $5M seed round, 50K+ users in first year", - gender: "female" - }, - { - name: "Marcus Weber", - role: "CTO & Co-Founder", - study: "Computer Science", - company: "CloudSync", - linkedinUrl: "https://linkedin.com/in/marcusweber", - imageUrl: "/example.png", - bio: "Building scalable cloud infrastructure. Former principal engineer at AWS.", - expertise: ["Cloud Architecture", "DevOps", "Distributed Systems"], - achievements: "Built systems serving 10M+ daily active users", - gender: "male" - }, - { - name: "Priya Patel", - role: "CEO & Founder", - study: "Medicine", - company: "HealthTech Solutions", - linkedinUrl: "https://linkedin.com/in/priyapatel", - imageUrl: "/example.png", - bio: "Revolutionizing healthcare delivery with technology. MD turned entrepreneur.", - expertise: ["Healthcare", "Digital Health", "Product Development"], - achievements: "Y Combinator W24, Partnership with 20+ hospitals", - gender: "female" - }, - { - name: "David Müller", - role: "CPO & Co-Founder", - study: "Design", - company: "DesignHub", - linkedinUrl: "https://linkedin.com/in/davidmuller", - imageUrl: "/example.png", - bio: "Creating beautiful and functional design tools for the next generation of creators.", - expertise: ["Product Design", "UX/UI", "Creative Tools"], - achievements: "15K designers using platform, Featured in ProductHunt top 5", - gender: "male" - }, - { - name: "Lisa Anderson", - role: "CEO & Co-Founder", - study: "Business Administration", - company: "EduTech Pro", - linkedinUrl: "https://linkedin.com/in/lisaanderson", - imageUrl: "/example.png", - bio: "Making quality education accessible to everyone. Former teacher and education consultant.", - expertise: ["EdTech", "Growth Marketing", "Business Development"], - achievements: "100K+ students, 15 countries, €2M ARR", - gender: "female" - }, - { - name: "Alex Thompson", - role: "CTO & Co-Founder", - study: "Engineering", - company: "FinanceFlow", - linkedinUrl: "https://linkedin.com/in/alexthompson", - imageUrl: "/example.png", - bio: "Building modern financial infrastructure for SMEs. Ex-Goldman Sachs and Stripe.", - expertise: ["FinTech", "Backend Engineering", "Security"], - achievements: "€10M Series A, Processing €50M monthly", - gender: "male" - }, - { - name: "Nina Kowalski", - role: "CEO & Founder", - study: "Environmental Science", - company: "GreenTech Innovations", - linkedinUrl: "https://linkedin.com/in/ninakowalski", - imageUrl: "/example.png", - bio: "Fighting climate change through innovative sustainability solutions. Environmental scientist and entrepreneur.", - expertise: ["Sustainability", "Climate Tech", "Impact Investing"], - achievements: "B Corp certified, Reduced 50K tons CO2, €3M raised", - gender: "female" - }, - { - name: "James Park", - role: "CEO & Co-Founder", - study: "Marketing", - company: "FoodTech Labs", - linkedinUrl: "https://linkedin.com/in/jamespark", - imageUrl: "/example.png", - bio: "Reimagining the food industry with sustainable alternatives. Former Michelin-starred chef.", - expertise: ["Food Science", "Supply Chain", "Operations"], - achievements: "Partnership with 100+ restaurants, €1.5M seed", - gender: "male" - } -] - -// All batches -const allBatches = [ - "Winter 2025", - "Summer 2025", - "Winter 2024", - "Summer 2024", - "Winter 2023", - "Summer 2023" -] - -// Generate members: replicate base members across all batches (about 30 per batch) -let idCounter = 1 -export const mockMembers: Member[] = [] - -allBatches.forEach(batch => { - // Add each base member 4 times to simulate ~32 members per batch - for (let repeat = 0; repeat < 4; repeat++) { - baseMemberTemplates.forEach(template => { - mockMembers.push({ - id: idCounter++, - batch, - ...template - }) - }) - } -}) - -// Helper function to get members by batch -export function getMembersByBatch(batch: string): Member[] { - return mockMembers.filter(member => member.batch === batch) -} - -// Helper function to get members by role -export function getMembersByRole(role: string): Member[] { - return mockMembers.filter(member => member.role === role) -} - -// Helper function to get all unique batches -export function getAllBatches(): string[] { - return Array.from(new Set(mockMembers.map(m => m.batch))).sort().reverse() -} - -// Helper function to get all unique roles -export function getAllRoles(): string[] { - return Array.from(new Set(mockMembers.map(m => m.role))).sort() -} diff --git a/lib/types.ts b/lib/types.ts index 8f77b54..fc69e5d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,22 @@ // Shared type definitions for the application +export interface Partner { + id: string + name: string + category: string + logoUrl: string + featured?: boolean +} + +export interface Startup { + id: string + name: string + logoUrl: string + isSpotlight?: boolean + isYCombinator?: boolean + isEWOR?: boolean +} + export interface Founder { name: string role: string