Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .agents/react-doctor/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions .agents/react-doctor/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="min-h-screen bg-[#FAF7F2]">
Expand Down
5 changes: 5 additions & 0 deletions app/north-star/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className="min-h-dvh w-screen bg-black text-white font-instrument">{children}</div>;
}
Expand Down
14 changes: 14 additions & 0 deletions app/north-star/portfolio/layout.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
}
5 changes: 5 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="min-h-screen bg-[#FEF9F5] snap-y snap-mandatory">
Expand Down
14 changes: 14 additions & 0 deletions app/people/edit/layout.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
}
5 changes: 3 additions & 2 deletions app/people/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -328,9 +329,9 @@ export default function EditPersonPage() {
<div className="mt-2">
<p className="font-medium">Current tags:</p>
<div className="flex flex-wrap gap-1 mt-1">
{formData.tags.map((tag, index) => (
{formData.tags.map((tag) => (
<span
key={index}
key={tag}
className="inline-block rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800"
>
{tag}
Expand Down
5 changes: 5 additions & 0 deletions app/people/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<main className="mx-auto max-w-6xl px-4 pb-16 pt-10 sm:px-6 lg:px-8">
Expand Down
214 changes: 214 additions & 0 deletions app/projects/ProjectDirectoryContent.tsx
Original file line number Diff line number Diff line change
@@ -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<Project | null>(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<typeof setTimeout>
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 (
<div className="min-h-screen bg-[#FAF7F2]">
<Header />

<main className="mx-auto px-4 py-8 md:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<h1 className="font-instrument text-4xl font-bold text-[#444]">Projects</h1>
<p className="mt-2 max-w-3xl text-sm text-gray-700">
A curated showcase of innovative startups and products built by founders and teams from V1 ecosystem.
</p>
</div>

{error ? (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-center">
<h3 className="text-lg font-medium text-red-600">Error</h3>
<p className="mt-1 text-sm text-gray-700">{error.message}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
>
Try Again
</button>
</div>
</div>
) : (
/* Main Layout */
<ProjectDirectoryLayout
projects={projects}
filters={{
...localFilters,
searchQuery: localSearchQuery
}}
filterOptions={cachedFilterOptions}
onSearchChange={setSearchQuery}
onToggleFunding={toggleFundingSource}
onToggleCohort={toggleCohort}
onToggleCategory={toggleCategory}
onProjectClick={openProjectModal}
onClearFilters={clearAllFilters}
isLoading={isFilterLoading}
/>
)}
</main>

<ProjectModal project={selectedProject} isOpen={isModalOpen} onClose={closeProjectModal} />

<Footer />
</div>
)
}

export default ProjectDirectoryContent
5 changes: 5 additions & 0 deletions app/projects/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ export default function ProjectCard({ project, onClick }: ProjectCardProps) {

return (
<div
role="button"
tabIndex={0}
className="group cursor-pointer rounded-lg border border-gray-300 bg-white p-6 transition-all hover:shadow-md hover:border-gray-400"
onClick={onClick}
onKeyDown={(e) => e.key === 'Enter' && onClick()}
>
<div className="flex items-start gap-4">
{/* Company Logo/Image */}
Expand All @@ -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"
/>
</div>
Expand Down Expand Up @@ -172,6 +176,7 @@ export default function ProjectCard({ project, onClick }: ProjectCardProps) {
src={founder.imageSrc}
alt={founder.name}
fill
sizes="24px"
className="object-cover"
/>
)}
Expand Down
14 changes: 14 additions & 0 deletions app/projects/layout.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
}
Loading