diff --git a/.agents/react-doctor/AGENTS.md b/.agents/react-doctor/AGENTS.md new file mode 100644 index 0000000..3db6436 --- /dev/null +++ b/.agents/react-doctor/AGENTS.md @@ -0,0 +1,15 @@ +# React Doctor + +Run after making React changes to catch issues early. Use when reviewing code, finishing a feature, or fixing bugs in a React project. + +Scans your React codebase for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics. + +## Usage + +```bash +npx -y react-doctor@latest . --verbose --diff +``` + +## Workflow + +Run after making changes to catch issues early. Fix errors first, then re-run to verify the score improved. diff --git a/.agents/react-doctor/SKILL.md b/.agents/react-doctor/SKILL.md new file mode 100644 index 0000000..8cc27cf --- /dev/null +++ b/.agents/react-doctor/SKILL.md @@ -0,0 +1,19 @@ +--- +name: react-doctor +description: Run after making React changes to catch issues early. Use when reviewing code, finishing a feature, or fixing bugs in a React project. +version: 1.0.0 +--- + +# React Doctor + +Scans your React codebase for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics. + +## Usage + +```bash +npx -y react-doctor@latest . --verbose --diff +``` + +## Workflow + +Run after making changes to catch issues early. Fix errors first, then re-run to verify the score improved. diff --git a/app/auth/page.tsx b/app/auth/page.tsx index dc86275..5e0d033 100644 --- a/app/auth/page.tsx +++ b/app/auth/page.tsx @@ -2,6 +2,11 @@ import GoogleAuth from "@/components/auth/google-auth"; import Header from "@/components/header"; import Footer from "@/components/footer"; +export const metadata = { + title: "Sign In - V1 at Michigan", + description: "Sign in to access the V1 community portal and manage your profile.", +}; + export default function AuthPage() { return (
diff --git a/app/north-star/layout.tsx b/app/north-star/layout.tsx index 375014a..9030a44 100644 --- a/app/north-star/layout.tsx +++ b/app/north-star/layout.tsx @@ -1,5 +1,10 @@ import type React from "react"; +export const metadata = { + title: "North Star Experience - V1 at Michigan", + description: "Explore the North Star experience, showcasing innovation and creativity at V1.", +}; + export default function NorthStarLayout({ children }: { children: React.ReactNode }) { return
{children}
; } diff --git a/app/north-star/portfolio/layout.tsx b/app/north-star/portfolio/layout.tsx new file mode 100644 index 0000000..736460b --- /dev/null +++ b/app/north-star/portfolio/layout.tsx @@ -0,0 +1,14 @@ +import type React from "react"; + +export const metadata = { + title: "North Star Portfolio - V1 at Michigan", + description: "Explore the portfolio of successful startups and companies from V1's North Star program.", +}; + +export default function NorthStarPortfolioLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/page.tsx b/app/page.tsx index 2b82bef..fc9931a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,6 +5,11 @@ import WhatWeDoSection from "@/components/what-we-do-section"; import ProgramSection from "@/components/program-section"; import Footer from "@/components/footer"; +export const metadata = { + title: "V1 at Michigan - Building the Future of Innovation", + description: "V1 is a student-run venture fund and accelerator building the next generation of Michigan entrepreneurs. Join our community of builders.", +}; + export default function LandingPage() { return (
diff --git a/app/people/edit/layout.tsx b/app/people/edit/layout.tsx new file mode 100644 index 0000000..c96eeb5 --- /dev/null +++ b/app/people/edit/layout.tsx @@ -0,0 +1,14 @@ +import type React from "react"; + +export const metadata = { + title: "Edit Profile - V1 at Michigan", + description: "Edit your profile information in the V1 community directory.", +}; + +export default function EditPersonLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/people/edit/page.tsx b/app/people/edit/page.tsx index 2f8bbab..5581e6a 100644 --- a/app/people/edit/page.tsx +++ b/app/people/edit/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import type React from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/components/auth/auth-provider"; import Header from "@/components/header"; @@ -328,9 +329,9 @@ export default function EditPersonPage() {

Current tags:

- {formData.tags.map((tag, index) => ( + {formData.tags.map((tag) => ( {tag} diff --git a/app/people/page.tsx b/app/people/page.tsx index e4da471..473eed9 100644 --- a/app/people/page.tsx +++ b/app/people/page.tsx @@ -4,6 +4,11 @@ import Footer from "@/components/footer"; import { Input } from "@/components/ui/input"; import PeopleContent from "@/components/people-content"; +export const metadata = { + title: "People @ V1 - Our Community of Builders", + description: "A curated directory of builders, engineers, designers, and operators from the V1 ecosystem at the University of Michigan.", +}; + function PeopleFallback() { return (
diff --git a/app/projects/ProjectDirectoryContent.tsx b/app/projects/ProjectDirectoryContent.tsx new file mode 100644 index 0000000..3e9292b --- /dev/null +++ b/app/projects/ProjectDirectoryContent.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { useEffect, useState, useMemo, useCallback, Suspense } from "react" +import { useSearchParams, useRouter } from "next/navigation" +import Header from "@/components/header" +import Footer from "@/components/footer" +import ProjectModal from "@/components/project-modal" +import ProjectDirectoryLayout from "./components/ProjectDirectoryLayout" +import type { Project } from "@/types/project" +import { useProjects } from "@/hooks/useProjects" + +function ProjectDirectoryContent() { + const searchParams = useSearchParams() + const router = useRouter() + const [selectedProject, setSelectedProject] = useState(null) + const [isModalOpen, setIsModalOpen] = useState(false) + + const urlSearchQuery = searchParams?.get("search") || "" + const [localSearchQuery, setLocalSearchQuery] = useState(urlSearchQuery) + + const [localFilters, setLocalFilters] = useState({ + searchQuery: urlSearchQuery, + fundingSources: searchParams?.getAll("funding") || [], + cohorts: searchParams?.getAll("cohort") || [], + categories: searchParams?.getAll("category") || [], + }) + + const [isRefetching, setIsRefetching] = useState(false) + const [cachedFilterOptions, setCachedFilterOptions] = useState({ + fundingSources: [] as string[], + cohorts: [] as string[], + categories: [] as string[], + }) + + useEffect(() => { + setLocalFilters({ + searchQuery: urlSearchQuery, + fundingSources: searchParams?.getAll("funding") || [], + cohorts: searchParams?.getAll("cohort") || [], + categories: searchParams?.getAll("category") || [], + }) + }, [urlSearchQuery, searchParams]) + + const { projects, filterOptions, isLoading, error } = useProjects(localFilters) + + useEffect(() => { + if (filterOptions.fundingSources.length > 0 || filterOptions.cohorts.length > 0 || filterOptions.categories.length > 0) { + setCachedFilterOptions(filterOptions) + } + }, [filterOptions]) + + const debouncedUpdateURL = useMemo(() => { + let timeoutId: ReturnType + return (value: string) => { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => { + const params = new URLSearchParams(searchParams?.toString() || "") + if (value.trim()) { + params.set("search", value.trim()) + } else { + params.delete("search") + } + router.replace(`?${params.toString()}`, { scroll: false }) + }, 300) + } + }, [searchParams, router]) + + const setSearchQuery = useCallback((query: string) => { + setLocalSearchQuery(query) + setLocalFilters(prev => ({ ...prev, searchQuery: query })) + debouncedUpdateURL(query) + }, [debouncedUpdateURL]) + + useEffect(() => { + if (isLoading && !isRefetching) { + setIsRefetching(true) + } else if (!isLoading && isRefetching) { + setIsRefetching(false) + } + }, [isLoading, isRefetching]) + + const isFilterLoading = isRefetching || isLoading + + const updateFilters = useCallback((updates: { + fundingSources?: string[] + cohorts?: string[] + categories?: string[] + }) => { + const params = new URLSearchParams(searchParams?.toString() || "") + + if (updates.fundingSources !== undefined) { + params.delete("funding") + updates.fundingSources.forEach(f => params.append("funding", f)) + } + + if (updates.cohorts !== undefined) { + params.delete("cohort") + updates.cohorts.forEach(c => params.append("cohort", c)) + } + + if (updates.categories !== undefined) { + params.delete("category") + updates.categories.forEach(c => params.append("category", c)) + } + + router.push(`?${params.toString()}`, { scroll: false }) + }, [searchParams, router]) + + const toggleFundingSource = useCallback((source: string) => { + setIsRefetching(true) + const current = localFilters.fundingSources + const updated = current.includes(source) + ? current.filter((s: string) => s !== source) + : [...current, source] + setLocalFilters(prev => ({ ...prev, fundingSources: updated })) + updateFilters({ fundingSources: updated }) + }, [localFilters.fundingSources, updateFilters]) + + const toggleCohort = useCallback((cohort: string) => { + setIsRefetching(true) + const current = localFilters.cohorts + const updated = current.includes(cohort) + ? current.filter((c: string) => c !== cohort) + : [...current, cohort] + setLocalFilters(prev => ({ ...prev, cohorts: updated })) + updateFilters({ cohorts: updated }) + }, [localFilters.cohorts, updateFilters]) + + const toggleCategory = useCallback((category: string) => { + setIsRefetching(true) + const current = localFilters.categories + const updated = current.includes(category) + ? current.filter((c: string) => c !== category) + : [...current, category] + setLocalFilters(prev => ({ ...prev, categories: updated })) + updateFilters({ categories: updated }) + }, [localFilters.categories, updateFilters]) + + const openProjectModal = useCallback((project: Project) => { + setSelectedProject(project) + setIsModalOpen(true) + }, []) + + const closeProjectModal = useCallback(() => { + setIsModalOpen(false) + setTimeout(() => setSelectedProject(null), 200) + }, []) + + const clearAllFilters = useCallback(() => { + setIsRefetching(true) + setLocalSearchQuery("") + setLocalFilters({ + searchQuery: "", + fundingSources: [], + cohorts: [], + categories: [], + }) + router.push("/projects", { scroll: false }) + }, [router]) + + return ( +
+
+ +
+ {/* Header */} +
+

Projects

+

+ A curated showcase of innovative startups and products built by founders and teams from V1 ecosystem. +

+
+ + {error ? ( +
+
+

Error

+

{error.message}

+ +
+
+ ) : ( + /* Main Layout */ + + )} +
+ + + +
+
+ ) +} + +export default ProjectDirectoryContent diff --git a/app/projects/components/ProjectCard.tsx b/app/projects/components/ProjectCard.tsx index 53c7f9f..5d9179e 100644 --- a/app/projects/components/ProjectCard.tsx +++ b/app/projects/components/ProjectCard.tsx @@ -60,8 +60,11 @@ export default function ProjectCard({ project, onClick }: ProjectCardProps) { return (
e.key === 'Enter' && onClick()} >
{/* Company Logo/Image */} @@ -70,6 +73,7 @@ export default function ProjectCard({ project, onClick }: ProjectCardProps) { src={project.imageSrc} alt={project.companyName} fill + sizes="(max-width: 640px) 64px, 64px" className="object-cover" />
@@ -172,6 +176,7 @@ export default function ProjectCard({ project, onClick }: ProjectCardProps) { src={founder.imageSrc} alt={founder.name} fill + sizes="24px" className="object-cover" /> )} diff --git a/app/projects/layout.tsx b/app/projects/layout.tsx new file mode 100644 index 0000000..455d2b4 --- /dev/null +++ b/app/projects/layout.tsx @@ -0,0 +1,14 @@ +import type React from "react"; + +export const metadata = { + title: "Projects - V1 at Michigan", + description: "A curated showcase of innovative startups and products built by founders and teams from the V1 ecosystem.", +}; + +export default function ProjectsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 94ac630..4a9f68b 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,216 +1,15 @@ -"use client"; +import { Suspense } from "react" +import ProjectDirectoryContent from "./ProjectDirectoryContent" -import { useEffect, useState, useMemo, useCallback } from "react" -import { useSearchParams, useRouter } from "next/navigation" -import Header from "@/components/header" -import Footer from "@/components/footer" -import ProjectModal from "@/components/project-modal" -import ProjectDirectoryLayout from "./components/ProjectDirectoryLayout" -import type { Project } from "@/types/project" -import { useProjects } from "@/hooks/useProjects" +export const metadata = { + title: "Projects | V1 at Michigan", + description: "A curated showcase of innovative startups and products built by founders and teams from V1 ecosystem.", +} export default function ProjectDirectoryPage() { - const searchParams = useSearchParams() - const router = useRouter() - const [selectedProject, setSelectedProject] = useState(null) - const [isModalOpen, setIsModalOpen] = useState(false) - - const urlSearchQuery = searchParams?.get("search") || "" - const [localSearchQuery, setLocalSearchQuery] = useState(urlSearchQuery) - - useEffect(() => { - setLocalSearchQuery(urlSearchQuery) - }, [urlSearchQuery]) - - const [localFilters, setLocalFilters] = useState({ - searchQuery: urlSearchQuery, - fundingSources: searchParams?.getAll("funding") || [], - cohorts: searchParams?.getAll("cohort") || [], - categories: searchParams?.getAll("category") || [], - }) - - const [isRefetching, setIsRefetching] = useState(false) - const [cachedFilterOptions, setCachedFilterOptions] = useState({ - fundingSources: [] as string[], - cohorts: [] as string[], - categories: [] as string[], - }) - - useEffect(() => { - setLocalFilters({ - searchQuery: urlSearchQuery, - fundingSources: searchParams?.getAll("funding") || [], - cohorts: searchParams?.getAll("cohort") || [], - categories: searchParams?.getAll("category") || [], - }) - }, [urlSearchQuery, searchParams]) - - const { projects, filterOptions, isLoading, error } = useProjects(localFilters) - - useEffect(() => { - if (filterOptions.fundingSources.length > 0 || filterOptions.cohorts.length > 0 || filterOptions.categories.length > 0) { - setCachedFilterOptions(filterOptions) - } - }, [filterOptions]) - - const debouncedUpdateURL = useMemo(() => { - let timeoutId: ReturnType - return (value: string) => { - clearTimeout(timeoutId) - timeoutId = setTimeout(() => { - const params = new URLSearchParams(searchParams?.toString() || "") - if (value.trim()) { - params.set("search", value.trim()) - } else { - params.delete("search") - } - router.replace(`?${params.toString()}`, { scroll: false }) - }, 300) - } - }, [searchParams, router]) - - const setSearchQuery = useCallback((query: string) => { - setLocalSearchQuery(query) - setLocalFilters(prev => ({ ...prev, searchQuery: query })) - debouncedUpdateURL(query) - }, [debouncedUpdateURL]) - - useEffect(() => { - if (isLoading && !isRefetching) { - setIsRefetching(true) - } else if (!isLoading && isRefetching) { - setIsRefetching(false) - } - }, [isLoading, isRefetching]) - - const isFilterLoading = isRefetching || isLoading - - const updateFilters = useCallback((updates: { - fundingSources?: string[] - cohorts?: string[] - categories?: string[] - }) => { - const params = new URLSearchParams(searchParams?.toString() || "") - - if (updates.fundingSources !== undefined) { - params.delete("funding") - updates.fundingSources.forEach(f => params.append("funding", f)) - } - - if (updates.cohorts !== undefined) { - params.delete("cohort") - updates.cohorts.forEach(c => params.append("cohort", c)) - } - - if (updates.categories !== undefined) { - params.delete("category") - updates.categories.forEach(c => params.append("category", c)) - } - - router.push(`?${params.toString()}`, { scroll: false }) - }, [searchParams, router]) - - const toggleFundingSource = useCallback((source: string) => { - setIsRefetching(true) - const current = localFilters.fundingSources - const updated = current.includes(source) - ? current.filter((s: string) => s !== source) - : [...current, source] - setLocalFilters(prev => ({ ...prev, fundingSources: updated })) - updateFilters({ fundingSources: updated }) - }, [localFilters.fundingSources, updateFilters]) - - const toggleCohort = useCallback((cohort: string) => { - setIsRefetching(true) - const current = localFilters.cohorts - const updated = current.includes(cohort) - ? current.filter((c: string) => c !== cohort) - : [...current, cohort] - setLocalFilters(prev => ({ ...prev, cohorts: updated })) - updateFilters({ cohorts: updated }) - }, [localFilters.cohorts, updateFilters]) - - const toggleCategory = useCallback((category: string) => { - setIsRefetching(true) - const current = localFilters.categories - const updated = current.includes(category) - ? current.filter((c: string) => c !== category) - : [...current, category] - setLocalFilters(prev => ({ ...prev, categories: updated })) - updateFilters({ categories: updated }) - }, [localFilters.categories, updateFilters]) - - const openProjectModal = useCallback((project: Project) => { - setSelectedProject(project) - setIsModalOpen(true) - }, []) - - const closeProjectModal = useCallback(() => { - setIsModalOpen(false) - setTimeout(() => setSelectedProject(null), 200) - }, []) - - const clearAllFilters = useCallback(() => { - setIsRefetching(true) - setLocalSearchQuery("") - setLocalFilters({ - searchQuery: "", - fundingSources: [], - cohorts: [], - categories: [], - }) - router.push("/projects", { scroll: false }) - }, [router]) - return ( -
-
- -
- {/* Header */} -
-

Projects

-

- A curated showcase of innovative startups and products built by founders and teams from V1 ecosystem. -

-
- - {error ? ( -
-
-

Error

-

{error.message}

- -
-
- ) : ( - /* Main Layout */ - - )} -
- - - -
-
+ Loading...
}> + + ) } diff --git a/app/startup-week/layout.tsx b/app/startup-week/layout.tsx new file mode 100644 index 0000000..6cecd2b --- /dev/null +++ b/app/startup-week/layout.tsx @@ -0,0 +1,14 @@ +import type React from "react"; + +export const metadata = { + title: "Startup Week - V1 at Michigan", + description: "Join V1 Startup Week for tech talks, events, and networking opportunities with Michigan's startup community.", +}; + +export default function StartupWeekLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/store/cancel/page.tsx b/app/store/cancel/page.tsx index b102fd7..8fe4097 100644 --- a/app/store/cancel/page.tsx +++ b/app/store/cancel/page.tsx @@ -4,6 +4,11 @@ import Header from "@/components/header"; import Footer from "@/components/footer"; import { Button } from "@/components/ui/button"; +export const metadata = { + title: "Checkout Cancelled - V1 Store", + description: "Your checkout was cancelled. You can return to the store to complete your purchase.", +}; + export default function CancelPage() { return (
diff --git a/app/store/layout.tsx b/app/store/layout.tsx index cb8df98..3610516 100644 --- a/app/store/layout.tsx +++ b/app/store/layout.tsx @@ -2,6 +2,11 @@ import React from "react" import { CartProvider } from "@/components/store/cart-provider"; import { CartSheet } from "@/components/store/cart-sheet"; +export const metadata = { + title: "V1 Store - Official Merchandise", + description: "Shop official V1 at Michigan merchandise including t-shirts, hoodies, and more. Support student entrepreneurship.", +}; + export default function StoreLayout({ children, }: { diff --git a/app/valentines/2026/layout.tsx b/app/valentines/2026/layout.tsx new file mode 100644 index 0000000..10c661a --- /dev/null +++ b/app/valentines/2026/layout.tsx @@ -0,0 +1,14 @@ +import type React from "react"; + +export const metadata = { + title: "Valentine's Cards 2026 - V1 at Michigan", + description: "Create and share beautiful Valentine's cards for 2026 with the V1 community.", +}; + +export default function Valentines2026Layout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/valentines/page.tsx b/app/valentines/page.tsx index b057eef..3a5e81a 100644 --- a/app/valentines/page.tsx +++ b/app/valentines/page.tsx @@ -3,6 +3,11 @@ import { Heart } from "lucide-react"; import Header from "@/components/header"; import Footer from "@/components/footer"; +export const metadata = { + title: "Valentine's Cards - V1 at Michigan", + description: "Create and share beautiful Valentine's cards with the V1 community.", +}; + const YEARS = [ { year: 2026, diff --git a/app/welcome/layout.tsx b/app/welcome/layout.tsx new file mode 100644 index 0000000..ad99641 --- /dev/null +++ b/app/welcome/layout.tsx @@ -0,0 +1,14 @@ +import type React from "react"; + +export const metadata = { + title: "Welcome - V1 at Michigan", + description: "Welcome to V1 at Michigan. Redirecting to the home page...", +}; + +export default function WelcomeLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx index ce88693..cbf2e5b 100644 --- a/app/welcome/page.tsx +++ b/app/welcome/page.tsx @@ -1,22 +1,10 @@ -"use client"; +import { redirect } from "next/navigation"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; +export const metadata = { + title: "Welcome | V1 at Michigan", + description: "V1 at Michigan - Building the next generation of founders and innovators.", +} export default function WelcomePage() { - const router = useRouter(); - - useEffect(() => { - // Redirect to the index page - router.push("/"); - }, [router]); - - return ( -
-
-
-

Welcome! Redirecting...

-
-
- ); + redirect("/"); } diff --git a/components/Redirect.tsx b/components/Redirect.tsx index a1ee50a..029b523 100644 --- a/components/Redirect.tsx +++ b/components/Redirect.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { useEffect } from "react"; interface RedirectProps { diff --git a/components/event-card.tsx b/components/event-card.tsx index fb82c9d..32879cf 100644 --- a/components/event-card.tsx +++ b/components/event-card.tsx @@ -19,6 +19,7 @@ export default function EventCard({ src={image || "/placeholder.svg"} alt={title} fill + sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" className="rounded-md object-cover" />
diff --git a/components/events-sections.tsx b/components/events-sections.tsx index 034b109..db9396f 100644 --- a/components/events-sections.tsx +++ b/components/events-sections.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { LazyMotion, m, domAnimation } from "framer-motion"; import EventCard from "./event-card"; interface Event { @@ -30,23 +30,25 @@ export default function EventsSection({ events }: EventsSectionProps) { }; return (
- - {events.map((event, index) => ( - - - - ))} - + + + {events.map((event) => ( + + + + ))} + +
); } diff --git a/components/faq-section.tsx b/components/faq-section.tsx index 18ca6c3..a6af7a6 100644 --- a/components/faq-section.tsx +++ b/components/faq-section.tsx @@ -55,7 +55,7 @@ export default function FAQSection() {
{faqData.map((faq, index) => (
- {name} + {name}

{name}

diff --git a/components/person-modal.tsx b/components/person-modal.tsx index 1bfc519..293249a 100644 --- a/components/person-modal.tsx +++ b/components/person-modal.tsx @@ -29,6 +29,7 @@ export default function PersonModal({ person, open, onOpenChange }: PersonModalP src={person.imageSrc || "/placeholder.svg"} alt={person.name} fill + sizes="(max-width: 640px) 160px, 160px" className="object-cover" />
diff --git a/components/posthog-provider.tsx b/components/posthog-provider.tsx index 0228b03..4f090ea 100644 --- a/components/posthog-provider.tsx +++ b/components/posthog-provider.tsx @@ -3,6 +3,7 @@ import posthog from 'posthog-js'; import { PostHogProvider } from 'posthog-js/react'; import { useEffect } from 'react'; +import type React from 'react'; export function CSPostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { diff --git a/components/program-card.tsx b/components/program-card.tsx index 08731d8..30409c5 100644 --- a/components/program-card.tsx +++ b/components/program-card.tsx @@ -13,7 +13,7 @@ export default function ProgramCard({ title, description, imageSrc, href }: Prog return (
- {`${title} + {`${title}
+
e.key === 'Enter' && onClick()} + >
- {title} + {title}

{title}

diff --git a/components/project-modal.tsx b/components/project-modal.tsx index ba4a33d..46983f4 100644 --- a/components/project-modal.tsx +++ b/components/project-modal.tsx @@ -78,8 +78,8 @@ export default function ProjectModal({ project, isOpen, onClose }: ProjectModalP )}
- {project.categories.map((category, index) => ( - + {project.categories.map((category) => ( + {category} ))} diff --git a/components/startup-card.tsx b/components/startup-card.tsx index 31c6e1a..dbda01f 100644 --- a/components/startup-card.tsx +++ b/components/startup-card.tsx @@ -1,3 +1,5 @@ +import Image from "next/image"; + interface StartupCardProps { image: string; name: string; @@ -7,12 +9,14 @@ interface StartupCardProps { export default function StartupCard({ image, name, domain }: StartupCardProps) { return (
-
+
{image ? ( - {`${name} ) : (
diff --git a/components/startups-grid.tsx b/components/startups-grid.tsx index 4acfff0..c6f0024 100644 --- a/components/startups-grid.tsx +++ b/components/startups-grid.tsx @@ -1,9 +1,8 @@ "use client"; import { useState } from "react"; -import { motion } from "framer-motion"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import StartupCard from "./startup-card"; +import StartupsGridControls from "./startups/StartupsGridControls"; +import StartupsGridContent from "./startups/StartupsGridContent"; export default function StartupsGrid() { const companies = [ @@ -117,28 +116,11 @@ export default function StartupsGrid() { ...placeholderCompanies, ]; - const containerVariants = { - enter: (direction: number) => ({ - x: direction > 0 ? 300 : -300, - opacity: 0, - }), - center: { x: 0, opacity: 1 }, - exit: (direction: number) => ({ - x: direction > 0 ? -300 : 300, - opacity: 0, - }), - }; - - const itemVariants = { - hidden: { opacity: 0, y: 8 }, - visible: { - opacity: 1, - y: 0, - transition: { duration: 0.3 }, - }, - }; + const years = [ + { year: "FALL 2024", companies: extendedCompanies }, + { year: "FALL 2025", companies: fall2025PlaceholderCompanies }, + ]; - // 0 = Fall 2024, 1 = Fall 2025 const [pageIndex, setPageIndex] = useState(0); const [direction, setDirection] = useState(0); @@ -148,11 +130,6 @@ export default function StartupsGrid() { setPageIndex(newIndex); } - const years = [ - { year: "FALL 2024", companies: extendedCompanies }, - { year: "FALL 2025", companies: fall2025PlaceholderCompanies }, - ]; - return (
- {/* - - */} -
- - -
-

- {years[pageIndex].year} -

-
-
-
- 12 -
-
- Top startups -
-
-
-
- 250+ -
-
- Top students -
-
-
-
- - -
- - {/* Company Grid with animation */} -
-
-
-
- - {years[pageIndex].companies - .slice(0, 3 * 4) - .map((company, index) => ( - - - - ))} - - - {/* Coming Soon Overlay for Fall 2025 */} - {pageIndex === 1 && ( -
-
-
- Coming Soon -
-
- Amazing startups will be announced soon -
-
-
- )} -
-
-
-
- - {/* Bottom gradient overlay */} - {/*
*/} + + +
); } diff --git a/components/startups/StartupsGridContent.tsx b/components/startups/StartupsGridContent.tsx new file mode 100644 index 0000000..5324ed8 --- /dev/null +++ b/components/startups/StartupsGridContent.tsx @@ -0,0 +1,91 @@ +import { LazyMotion, m, domAnimation } from "framer-motion"; +import StartupCard from "../startup-card"; + +interface Company { + name: string; + domain: string; + image: string; + isComingSoon?: boolean; +} + +interface StartupsGridContentProps { + pageIndex: number; + direction: number; + companies: Company[]; +} + +export default function StartupsGridContent({ + pageIndex, + direction, + companies, +}: StartupsGridContentProps) { + const containerVariants = { + enter: (direction: number) => ({ + x: direction > 0 ? 300 : -300, + opacity: 0, + }), + center: { x: 0, opacity: 1 }, + exit: (direction: number) => ({ + x: direction > 0 ? -300 : 300, + opacity: 0, + }), + }; + + const itemVariants = { + hidden: { opacity: 0, y: 8 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.3 }, + }, + }; + + return ( +
+
+
+
+ + + {companies + .slice(0, 3 * 4) + .map((company) => ( + + + + ))} + + + + {/* Coming Soon Overlay for Fall 2025 */} + {pageIndex === 1 && ( +
+
+
+ Coming Soon +
+
+ Amazing startups will be announced soon +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/components/startups/StartupsGridControls.tsx b/components/startups/StartupsGridControls.tsx new file mode 100644 index 0000000..5012643 --- /dev/null +++ b/components/startups/StartupsGridControls.tsx @@ -0,0 +1,63 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; + +interface StartupsGridControlsProps { + pageIndex: number; + years: Array<{ year: string; companies: any[] }>; + onPaginate: (newIndex: number) => void; +} + +export default function StartupsGridControls({ + pageIndex, + years, + onPaginate, +}: StartupsGridControlsProps) { + return ( +
+ + +
+

+ {years[pageIndex].year} +

+
+
+
+ 12 +
+
+ Top startups +
+
+
+
+ 250+ +
+
+ Top students +
+
+
+
+ + +
+ ); +} diff --git a/components/store/cart-sheet.tsx b/components/store/cart-sheet.tsx index a438853..cc555bc 100644 --- a/components/store/cart-sheet.tsx +++ b/components/store/cart-sheet.tsx @@ -90,6 +90,7 @@ export function CartSheet() { src={item.product.image} alt={item.product.name} fill + sizes="80px" className="object-cover" />
diff --git a/components/store/product-card.tsx b/components/store/product-card.tsx index 9c45d49..8586260 100644 --- a/components/store/product-card.tsx +++ b/components/store/product-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import Image from "next/image"; import { MerchProduct } from "@/types/merch"; import { useCart } from "./cart-provider"; @@ -24,10 +24,6 @@ export function ProductCard({ product }: ProductCardProps) { return product.images || [product.image]; }, [selectedColor, product.images, product.image]); - useEffect(() => { - setCurrentImageIndex(0); - }, [selectedColor]); - const handleAddToCart = () => { addItem(product, selectedSize, selectedColor.name); setIsAdded(true); @@ -53,7 +49,7 @@ export function ProductCard({ product }: ProductCardProps) { {/* Product Image */}
{product.name} - {images.map((_, idx) => ( + {images.map((image, idx) => (
diff --git a/components/team-section.tsx b/components/team-section.tsx index 836c89c..2369a35 100644 --- a/components/team-section.tsx +++ b/components/team-section.tsx @@ -20,7 +20,7 @@ export default function TeamSection({ teamMembers }: TeamSectionProps) {
{teamMembers.map((member, index) => ( []; } +interface TimelineRailState { + activeIndex: number; + filledDotTop: number; + railLeft: number; + markerTops: number[]; +} + +type TimelineRailAction = + | { type: "SET_ACTIVE_INDEX"; payload: number } + | { type: "SET_FILLED_DOT_TOP"; payload: number } + | { type: "SET_RAIL_LEFT"; payload: number } + | { type: "SET_MARKER_TOPS"; payload: number[] } + | { type: "UPDATE_LAYOUT"; payload: { railLeft: number; markerTops: number[] } } + | { type: "UPDATE_POSITIONS"; payload: { activeIndex: number; filledDotTop: number } }; + +const initialState: TimelineRailState = { + activeIndex: 0, + filledDotTop: 0, + railLeft: 64, + markerTops: [], +}; + +function timelineRailReducer( + state: TimelineRailState, + action: TimelineRailAction +): TimelineRailState { + switch (action.type) { + case "SET_ACTIVE_INDEX": + return { ...state, activeIndex: action.payload }; + case "SET_FILLED_DOT_TOP": + return { ...state, filledDotTop: action.payload }; + case "SET_RAIL_LEFT": + return { ...state, railLeft: action.payload }; + case "SET_MARKER_TOPS": + return { ...state, markerTops: action.payload }; + case "UPDATE_LAYOUT": + return { + ...state, + railLeft: action.payload.railLeft, + markerTops: action.payload.markerTops, + }; + case "UPDATE_POSITIONS": + return { + ...state, + activeIndex: action.payload.activeIndex, + filledDotTop: action.payload.filledDotTop, + }; + default: + return state; + } +} + export default function TimelineRail({ sectionRefs }: TimelineRailProps) { - const [activeIndex, setActiveIndex] = useState(0); - const [filledDotTop, setFilledDotTop] = useState(0); - const [railLeft, setRailLeft] = useState(64); // default ~4rem - const [markerTops, setMarkerTops] = useState([]); + const [state, dispatch] = useReducer(timelineRailReducer, initialState); - // Calculate positions of empty circles based on section title positions const computeLayout = () => { const positions: number[] = []; - // Align rail with the left edge of the first section container const first = sectionRefs[0]?.current; if (first) { const left = first.getBoundingClientRect().left; - setRailLeft(left); + dispatch({ type: "SET_RAIL_LEFT", payload: left }); } sectionRefs.forEach((ref) => { @@ -28,17 +75,15 @@ export default function TimelineRail({ sectionRefs }: TimelineRailProps) { const titleEl = el.querySelector("h2"); if (!titleEl) return; const titleRect = titleEl.getBoundingClientRect(); - // Use viewport-relative top for fixed container positions.push(titleRect.top); }); - setMarkerTops(positions); + dispatch({ type: "SET_MARKER_TOPS", payload: positions }); }; useEffect(() => { const update = () => { if (sectionRefs.length === 0) return; - // Determine which section is most visible in viewport const viewportTop = window.scrollY; const viewportBottom = viewportTop + window.innerHeight; @@ -53,7 +98,6 @@ export default function TimelineRail({ sectionRefs }: TimelineRailProps) { const sectionTop = rect.top + window.scrollY; const sectionBottom = sectionTop + rect.height; - // Calculate how much of the section is visible const visibleTop = Math.max(viewportTop, sectionTop); const visibleBottom = Math.min(viewportBottom, sectionBottom); const visibleHeight = Math.max(0, visibleBottom - visibleTop); @@ -65,20 +109,22 @@ export default function TimelineRail({ sectionRefs }: TimelineRailProps) { } }); - setActiveIndex(closestIdx); - - // Get the current section's title position (viewport-relative) const currentSection = sectionRefs[closestIdx]?.current; if (currentSection) { const titleEl = currentSection.querySelector("h2"); if (titleEl) { const titleRect = titleEl.getBoundingClientRect(); - setFilledDotTop(titleRect.top); + dispatch({ + type: "UPDATE_POSITIONS", + payload: { + activeIndex: closestIdx, + filledDotTop: titleRect.top, + }, + }); } } }; - // Initial layout + update setTimeout(() => { computeLayout(); update(); @@ -97,8 +143,6 @@ export default function TimelineRail({ sectionRefs }: TimelineRailProps) { }; }, [sectionRefs]); - // Marker positions are kept in state (viewport-relative) - return (
{/* Empty circles at each section title */} - {markerTops.map((topPos, idx) => ( + {state.markerTops.map((topPos) => (
))} {/* Moving filled circle */}
); diff --git a/components/ui/resizable.tsx b/components/ui/resizable.tsx index f4bc558..7afee7a 100644 --- a/components/ui/resizable.tsx +++ b/components/ui/resizable.tsx @@ -2,6 +2,7 @@ import { GripVertical } from "lucide-react" import * as ResizablePrimitive from "react-resizable-panels" +import type React from "react" import { cn } from "@/lib/utils" diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index eeb2d7a..4227b15 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -26,7 +26,7 @@ const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b" -type SidebarContext = { +type SidebarContextType = { state: "expanded" | "collapsed" open: boolean setOpen: (open: boolean) => void @@ -36,7 +36,7 @@ type SidebarContext = { toggleSidebar: () => void } -const SidebarContext = React.createContext(null) +const SidebarContext = React.createContext(null) function useSidebar() { const context = React.useContext(SidebarContext) @@ -116,7 +116,7 @@ const SidebarProvider = React.forwardRef< // This makes it easier to style the sidebar with Tailwind classes. const state = open ? "expanded" : "collapsed" - const contextValue = React.useMemo( + const contextValue = React.useMemo( () => ({ state, open, diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx index 01b8b6d..f402a31 100644 --- a/components/ui/skeleton.tsx +++ b/components/ui/skeleton.tsx @@ -1,3 +1,4 @@ +import type React from "react" import { cn } from "@/lib/utils" function Skeleton({ diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx index 452f4d9..828b3d1 100644 --- a/components/ui/sonner.tsx +++ b/components/ui/sonner.tsx @@ -2,6 +2,7 @@ import { useTheme } from "next-themes" import { Toaster as Sonner } from "sonner" +import type React from "react" type ToasterProps = React.ComponentProps diff --git a/components/v1-team-roster.tsx b/components/v1-team-roster.tsx index 3f1b091..3be59e3 100644 --- a/components/v1-team-roster.tsx +++ b/components/v1-team-roster.tsx @@ -76,6 +76,7 @@ function TeamMember({ src={image || "/placeholder.svg"} alt={name} fill + sizes="(max-width: 640px) 50px, (max-width: 1024px) 64px, 64px" className="rounded-full object-cover border-2 border-white shadow-sm cursor-pointer" />
@@ -102,7 +103,7 @@ export default function V1TeamRoster() {
{startupWeekTeam.map((member, index) => (