diff --git a/.gitignore b/.gitignore index 6f82a38..653e780 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ next-env.d.ts .env.local .vscode/ tsconfig.tsbuildinfo +playwright-report/.last-run.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cfa5d91 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,116 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +BSL Platform is a Next.js 15 web application for Big Strategy Labs, an invite-only startup accelerator. It manages startup applications, events, team leadership profiles, and provides an admin dashboard for reviewing applications. + +## Tech Stack + +- **Framework:** Next.js 15 (App Router) +- **Database:** MongoDB with Prisma ORM +- **Authentication:** NextAuth v5 (Google OAuth, JWT sessions) +- **Styling:** Tailwind CSS + Shadcn/Radix UI components +- **Validation:** Zod + +## Commands + +```bash +npm run dev # Start development server (port 3000) +npm run build # Production build +npm run lint # Run ESLint +npm run lint-fix # Fix lint issues and format with Prettier +npm run lint-check # Check lint and formatting without fixing +npm run format # Format all files with Prettier +npm run test # Run Playwright E2E tests +npm run seed # Seed user roles (edit prisma/seed.ts first) +npx prisma generate # Generate Prisma client after schema changes +npx prisma db push # Push schema changes to MongoDB +``` + +## Architecture + +### App Router Structure + +``` +app/ +├── api/ +│ ├── auth/[...nextauth]/ # NextAuth handler +│ ├── applications/startup/ # POST startup applications +│ ├── events/ # GET public events +│ └── admin/ # Admin API (applications, events CRUD) +├── apply/ # Application forms (startup, org, team) +├── admin/ # Admin dashboard and application review +├── leaders/ # Team leadership profiles +├── events/ # Public events page +└── about/ # About/mission page +``` + +### Key Directories + +- **`components/ui/`** - Shadcn/Radix primitives (Button, Input, Card, etc.) +- **`components/layout/`** - PublicLayout wrapper with navbar/footer +- **`components/admin/`** - AdminLayout with sidebar +- **`lib/`** - Prisma client singleton, Mongoose connection, utilities +- **`services/`** - Data access layer (demo products, mock applications) + +### Database (Prisma/MongoDB) + +Three main models in `prisma/schema.prisma`: +- **User** - Auth with RBAC roles (USER, REVIEWER, AMBASSADOR, SUPER_ADMIN) +- **Application** - Form submissions with JSON payload, status tracking +- **Event** - Admin-managed events with creator relationship + +### Authentication Flow + +- NextAuth configured in `auth.ts` with Google provider +- Session provider wraps app in root `layout.tsx` +- Auth button component at `components/auth/AuthButton.tsx` +- RBAC enforced in API routes via requireRole() in lib/auth-helpers.ts + +### Seeding User Roles + +Edit `prisma/seed.ts` to assign roles for testing: +```ts +const SEED_USERS = [ + { email: "your-email@gmail.com", role: "SUPER_ADMIN", name: "Your Name" }, +]; +``` +Then run `npm run seed`. Safe to re-run (uses upsert). + +### Path Aliases + +Use `@/*` to import from project root (configured in tsconfig.json). + +```typescript +import { prisma } from "@/lib/prisma"; +import { Button } from "@/components/ui/button"; +``` + +## Environment Variables + +Required in `.env` (see `.env.example`): +``` +DATABASE_URL=mongodb://localhost:27017/test-db +AUTH_SECRET= # openssl rand -base64 32 +AUTH_GOOGLE_ID= # Google Cloud Console +AUTH_GOOGLE_SECRET= +``` + +## Code Patterns + +- Client components use `"use client"` directive +- API routes return `NextResponse.json()` with appropriate status codes +- Prisma client is singleton-cached in `lib/prisma.ts` to prevent hot-reload leaks +- Tailwind classes merged with `cn()` utility from `lib/utils.ts` +- Forms use client-side state with POST to API routes + +## Testing + +Playwright E2E tests in `tests/e2e/`. The config auto-starts dev server on port 3000. + +```bash +npx playwright test # Run all tests +npx playwright test tests/e2e/demo.spec.ts # Run specific test +``` diff --git a/app/leaders/page.tsx b/app/leaders/page.tsx index a9a039b..60a99f9 100644 --- a/app/leaders/page.tsx +++ b/app/leaders/page.tsx @@ -9,49 +9,49 @@ const team = [ name: "Sarah Johnson", title: "Founder & CEO", bio: "Former co-founder of TechVentures. Early investor at Google and Amazon.", - image: "woman.jpg", + image: "/woman.jpg", }, { name: "Michael Chen", title: "Investment Director", bio: "Lead investor at Sequoia Capital. Former partner at Y Combinator.", - image: "men.jpg", + image: "/men.jpg", }, { name: "Emily Rodriguez", title: "Head of Programs", bio: "Built accelerator programs at Techstars. Former founder of EdTech startup.", - image: "woman.jpg", + image: "/woman.jpg", }, { name: "David Park", title: "Technical Advisor", bio: "Former CTO at Stripe. Led engineering at Facebook and Airbnb.", - image: "men.jpg", + image: "/men.jpg", }, { name: "Alex Thompson", title: "Operations Manager", bio: "Scaled operations at Uber. Former consultant at McKinsey.", - image: "men.jpg", + image: "/men.jpg", }, { name: "Maria Garcia", title: "Venture Partner", bio: "15 years in venture capital. Board member at multiple unicorns.", - image: "woman.jpg", + image: "/woman.jpg", }, { name: "Ryan Mitchell", title: "Community Lead", bio: "Built communities at Reddit. Former developer relations at GitHub.", - image: "men.jpg", + image: "/men.jpg", }, { name: "Sharad Aggarwal", title: "VC", bio: "Global head of AI Strategy at Google Cloud.", - image: "men.jpg", + image: "/men.jpg", }, ]; diff --git a/components/ui/LeaderCard2.tsx b/components/ui/LeaderCard2.tsx index 16473e7..cafcb3a 100644 --- a/components/ui/LeaderCard2.tsx +++ b/components/ui/LeaderCard2.tsx @@ -9,32 +9,23 @@ interface LeaderCardProps { reverse?: boolean; } -export default function LeaderCard2({ name, title, bio, image, reverse = false }: LeaderCardProps) { +export default function LeaderCard2({ name, title, bio, image }: LeaderCardProps) { return (
- {/* Image — overflows card top/bottom */} + {/* Image */}
{/* Text content */} -
-

+
+

{name}

-

+

{title}

-

+

{bio}

diff --git a/package-lock.json b/package-lock.json index a27b9a8..77232b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", - "dotenv": "^17.2.3", + "dotenv": "^17.3.1", "eslint": "^8", "eslint-config-next": "^15.0.3", "eslint-config-prettier": "^9.1.0", @@ -37,7 +37,7 @@ "husky": "^9.1.7", "postcss": "^8", "prettier": "^3.3.3", - "prisma": "^6.19.0", + "prisma": "^6.19.2", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "tsx": "^4.21.0", @@ -1484,6 +1484,7 @@ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.58.2" }, @@ -1665,6 +1666,7 @@ "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1731,6 +1733,7 @@ "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.15.0", "@typescript-eslint/types": "8.15.0", @@ -1901,278 +1904,13 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2998,9 +2736,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3276,6 +3014,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3452,6 +3191,7 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -5738,6 +5478,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -5894,45 +5635,6 @@ "preact": ">=10" } }, - "node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/preact-render-to-string": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", - "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", - "license": "MIT", - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/preact-render-to-string": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", - "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", - "license": "MIT", - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5964,6 +5666,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.2", "@prisma/engines": "6.19.2" @@ -6055,6 +5758,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6064,6 +5768,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7013,6 +6718,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/tests/e2e/admin.spec.ts b/tests/e2e/admin.spec.ts new file mode 100644 index 0000000..1255605 --- /dev/null +++ b/tests/e2e/admin.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from "@playwright/test"; + +test("redirects unauthenticated users from admin dashboard", async ({ + page, +}) => { + await page.goto("/admin"); + + // Should redirect to login or access-denied page (depends on auth state) + await expect(page).toHaveURL(/\/admin\/(login|access-denied)/); +}); + +test("admin login page shows Google sign-in button", async ({ page }) => { + await page.goto("/admin/login"); + + await expect( + page.getByRole("button", { name: /sign in with google/i }) + ).toBeVisible(); +}); + +test("admin login page displays correct heading", async ({ page }) => { + await page.goto("/admin/login"); + + await expect(page.getByText("Admin Login")).toBeVisible(); + await expect(page.getByText("Sign in to access the dashboard")).toBeVisible(); +}); diff --git a/tests/e2e/apply.spec.ts b/tests/e2e/apply.spec.ts new file mode 100644 index 0000000..67a98b6 --- /dev/null +++ b/tests/e2e/apply.spec.ts @@ -0,0 +1,158 @@ +import { expect, test } from "@playwright/test"; + +test("startup application form loads correctly", async ({ page }) => { + await page.goto("/apply/startup"); + + // Verify form elements are present + await expect( + page.getByRole("heading", { name: "Startup Application" }) + ).toBeVisible(); + await expect(page.locator("#name")).toBeVisible(); + await expect(page.locator("#description")).toBeVisible(); + await expect(page.locator("#deckUrl")).toBeVisible(); + await expect(page.locator("#fundingGoal")).toBeVisible(); + await expect(page.locator("#fundingSiteUrl")).toBeVisible(); + await expect(page.locator("#contact")).toBeVisible(); + await expect(page.getByRole("button", { name: "Apply" })).toBeVisible(); +}); + +test("startup application form can be filled", async ({ page }) => { + await page.goto("/apply/startup"); + + // Fill form fields + await page.fill("#name", "Test Startup Inc"); + await page.fill("#description", "A test startup for E2E testing"); + await page.fill("#deckUrl", "https://example.com/deck.pdf"); + await page.fill("#fundingGoal", "500000"); + await page.fill("#fundingSiteUrl", "https://example.com/funding"); + await page.fill("#contact", "test@example.com"); + + // Verify values were entered + await expect(page.locator("#name")).toHaveValue("Test Startup Inc"); + await expect(page.locator("#contact")).toHaveValue("test@example.com"); +}); + +test("startup application submit button is enabled when only required fields are filled", async ({ + page, +}) => { + await page.goto("/apply/startup"); + + // Fill required form fields + await page.fill("#name", "Test Startup Inc"); + await page.fill("#description", "A test startup for E2E testing"); + await page.fill("#deckUrl", "https://example.com/deck.pdf"); + await page.fill("#fundingGoal", "500000"); + await page.fill("#contact", "test@example.com"); + + // Verify submit button is enabled and clickable + const submitButton = page.getByRole("button", { name: "Apply" }); + await expect(submitButton).toBeEnabled(); +}); + +// Team Application Tests +test("team application form loads correctly", async ({ page }) => { + await page.goto("/apply/team"); + + // Verify form elements are present + await expect( + page.getByRole("heading", { name: "Team Application" }) + ).toBeVisible(); + await expect(page.locator("#submitterName")).toBeVisible(); + await expect(page.locator("#submitterEmail")).toBeVisible(); + await expect(page.locator("#teamName")).toBeVisible(); + await expect(page.locator("#teamSize")).toBeVisible(); + await expect(page.locator("#skills")).toBeVisible(); + await expect(page.locator("#projectPreferences")).toBeVisible(); + await expect(page.locator("#description")).toBeVisible(); + await expect(page.getByRole("button", { name: "Apply" })).toBeVisible(); +}); + +test("team application form can be filled", async ({ page }) => { + await page.goto("/apply/team"); + + // Fill form fields + await page.fill("#submitterName", "John Doe"); + await page.fill("#submitterEmail", "john@example.com"); + await page.fill("#teamName", "Test Team"); + await page.fill("#teamSize", "4"); + await page.fill("#skills", "React, Node.js, Python"); + await page.fill("#projectPreferences", "Web apps, AI/ML"); + await page.fill("#description", "A talented team for E2E testing"); + + // Verify values were entered + await expect(page.locator("#teamName")).toHaveValue("Test Team"); + await expect(page.locator("#submitterEmail")).toHaveValue("john@example.com"); +}); + +test("team application submit button is enabled when required fields are filled", async ({ + page, +}) => { + await page.goto("/apply/team"); + + // Fill required form fields + await page.fill("#submitterName", "John Doe"); + await page.fill("#submitterEmail", "john@example.com"); + await page.fill("#teamName", "Test Team"); + await page.fill("#teamSize", "4"); + await page.fill("#skills", "React, Node.js"); + await page.fill("#projectPreferences", "Web apps"); + await page.fill("#description", "A talented team for E2E testing"); + + // Verify submit button is enabled + const submitButton = page.getByRole("button", { name: "Apply" }); + await expect(submitButton).toBeEnabled(); +}); + +// Organization Application Tests +test("organization application form loads correctly", async ({ page }) => { + await page.goto("/apply/organization"); + + // Verify form elements are present + await expect( + page.getByRole("heading", { name: "New Company Project" }) + ).toBeVisible(); + await expect(page.locator("#submitterName")).toBeVisible(); + await expect(page.locator("#submitterEmail")).toBeVisible(); + await expect(page.locator("#companyName")).toBeVisible(); + await expect(page.locator("#projectTitle")).toBeVisible(); + await expect(page.locator("#budget")).toBeVisible(); + await expect(page.locator("#skillsNeeded")).toBeVisible(); + await expect(page.locator("#description")).toBeVisible(); + await expect(page.getByRole("button", { name: "Apply" })).toBeVisible(); +}); + +test("organization application form can be filled", async ({ page }) => { + await page.goto("/apply/organization"); + + // Fill form fields + await page.fill("#submitterName", "Jane Smith"); + await page.fill("#submitterEmail", "jane@company.com"); + await page.fill("#companyName", "Test Corp"); + await page.fill("#projectTitle", "E2E Test Project"); + await page.fill("#budget", "$50,000"); + await page.fill("#skillsNeeded", "React, UI/UX Design"); + await page.fill("#description", "A test project for E2E testing"); + + // Verify values were entered + await expect(page.locator("#companyName")).toHaveValue("Test Corp"); + await expect(page.locator("#submitterEmail")).toHaveValue("jane@company.com"); +}); + +test("organization application submit button is enabled when required fields are filled", async ({ + page, +}) => { + await page.goto("/apply/organization"); + + // Fill required form fields + await page.fill("#submitterName", "Jane Smith"); + await page.fill("#submitterEmail", "jane@company.com"); + await page.fill("#companyName", "Test Corp"); + await page.fill("#projectTitle", "E2E Test Project"); + await page.fill("#budget", "$50,000"); + await page.fill("#skillsNeeded", "React, UI/UX Design"); + await page.fill("#description", "A test project for E2E testing"); + + // Verify submit button is enabled + const submitButton = page.getByRole("button", { name: "Apply" }); + await expect(submitButton).toBeEnabled(); +}); diff --git a/tests/e2e/public-nav.spec.ts b/tests/e2e/public-nav.spec.ts new file mode 100644 index 0000000..6292cbd --- /dev/null +++ b/tests/e2e/public-nav.spec.ts @@ -0,0 +1,37 @@ +import { expect, test } from "@playwright/test"; + +test.describe("public pages navigation", () => { + test("home page loads", async ({ page }) => { + await page.goto("/"); + await expect( + page.getByRole("heading", { name: /BIG STRATEGY LABS/i, level: 1 }) + ).toBeVisible(); + }); + + test("about page loads", async ({ page }) => { + await page.goto("/about"); + await expect( + page.getByRole("heading", { name: /Our Mission/i }) + ).toBeVisible(); + }); + + test("leaders page loads", async ({ page }) => { + await page.goto("/leaders"); + // Check for the main heading text that spans multiple lines + await expect( + page.getByRole("heading", { name: /We are the people/i }) + ).toBeVisible(); + }); + + test("events page loads", async ({ page }) => { + await page.goto("/events"); + await expect(page.getByRole("heading", { name: /Events/i })).toBeVisible(); + }); + + test("apply page loads", async ({ page }) => { + await page.goto("/apply"); + await expect( + page.getByRole("heading", { name: /Apply to Join/i }) + ).toBeVisible(); + }); +});