Skip to content

Commit 920e0bb

Browse files
authored
Merge pull request #54 from hack4impact/dev
Sprint 1 & 2 & 3
2 parents 54c47a7 + 43805ed commit 920e0bb

59 files changed

Lines changed: 27834 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
2+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_your-key
3+
DATABASE_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres
4+
STRIPE_PRICE_ID=
5+
STRIPE_SECRET_KEY=
6+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, dev]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: "25"
21+
cache: npm
22+
23+
- name: Install dependencies
24+
run: npm ci
25+
26+
- name: Lint
27+
run: npm run lint
28+
29+
- name: Unit tests
30+
run: npm run test:ci

.gitignore

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# dependencies
2+
/node_modules
3+
/.pnp
4+
.pnp.*
5+
.yarn/*
6+
!.yarn/patches
7+
!.yarn/plugins
8+
!.yarn/releases
9+
!.yarn/versions
10+
11+
# testing
12+
/coverage
13+
14+
# next.js
15+
/.next/
16+
/out/
17+
18+
# production
19+
/build
20+
21+
# misc
22+
.DS_Store
23+
*.pem
24+
Thumbs.db
25+
26+
# debug
27+
npm-debug.log*
28+
yarn-debug.log*
29+
yarn-error.log*
30+
.pnpm-debug.log*
31+
32+
# env files
33+
.env
34+
.env.*
35+
!.env.example
36+
37+
# vercel
38+
.vercel
39+
40+
# typescript
41+
*.tsbuildinfo
42+
next-env.d.ts
43+
44+
# editors
45+
.vscode/*
46+
!.vscode/settings.json
47+
!.vscode/extensions.json
48+
.idea/
49+
*.swp
50+
*.swo
51+
*~
52+
53+
# drizzle
54+
drizzle/meta/
55+
56+
# supabase
57+
.supabase/
58+
59+
60+
.agents/
61+
.playwright-mcp/
62+
.mcp.json
63+
.claude/
64+
.cursor/

.prettierrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"tabWidth": 3
3+
}

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- BEGIN:nextjs-agent-rules -->
2+
# This is NOT the Next.js you know
3+
4+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5+
<!-- END:nextjs-agent-rules -->

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

README.md

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,98 @@
1-
# mcld-project
1+
# mcld-project
2+
3+
## Stack
4+
5+
- **Framework**: Next.js 16 (App Router, Turbopack)
6+
- **Auth**: Supabase Auth (email/password, SSR via `@supabase/ssr`)
7+
- **Database**: PostgreSQL via Supabase + Drizzle ORM
8+
- **UI**: shadcn/ui (Radix) + Tailwind
9+
- **Hosting**: Vercel
10+
11+
## Setup
12+
13+
### 1. Install dependencies
14+
15+
```bash
16+
pnpm install
17+
```
18+
19+
### 2. Configure environment
20+
21+
```bash
22+
cp .env.example .env.local
23+
sudo nano .env
24+
```
25+
26+
Fill in your Supabase credentials from [supabase.com/dashboard](https://supabase.com/dashboard) > Project Settings > API (or Dashboard > Framework > Next.js > Environment Variables):
27+
28+
only on `.env.local`:
29+
30+
- `NEXT_PUBLIC_SUPABASE_URL` — Project URL
31+
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY` — Publishable (anon) key
32+
33+
only on `.env`:
34+
35+
- `DATABASE_URL` — Connection string (Dashboard > Direct (connextion string) > Copy connection string (for the password go to Database > Settings > Database password > Reset password))
36+
37+
### 3. Configure Supabase Auth (if you dont use port 3000)
38+
39+
In your Supabase dashboard under **Authentication > URL Configuration**:
40+
41+
- **Site URL**: `http://localhost:PORT`
42+
- **Redirect URLs**: add `http://localhost:PORT/auth/callback`
43+
44+
Make sure **Email** provider is enabled under **Authentication > Sign In/Providers**.
45+
46+
### 4. Set up the database
47+
48+
```bash
49+
pnpm db:push # push schema to Supabase
50+
pnpm db:generate # generate migration files
51+
pnpm db:migrate # run migrations
52+
```
53+
54+
### 5. Run
55+
56+
```bash
57+
pnpm dev
58+
```
59+
60+
Open [http://localhost:3000](http://localhost:3000). You'll be redirected to `/login` if not authenticated.
61+
62+
## Adding shadcn/ui components
63+
64+
```bash
65+
npx shadcn@latest add <component>
66+
```
67+
68+
Examples:
69+
70+
```bash
71+
npx shadcn@latest add button
72+
npx shadcn@latest add card input label
73+
npx shadcn@latest add dialog dropdown-menu
74+
```
75+
76+
Components are installed to `components/ui/`. Browse available components at [ui.shadcn.com](https://ui.shadcn.com).
77+
78+
## Project structure
79+
80+
```
81+
app/
82+
layout.tsx # Root layout (Helvetica font)
83+
page.tsx # Home (protected, shows user + sign out)
84+
login/
85+
page.tsx # Login / signup form
86+
actions.ts # Server actions (login, signup, signout)
87+
auth/
88+
callback/
89+
route.ts # Email confirmation callback
90+
components/ui/ # shadcn/ui components
91+
utils/supabase/
92+
client.ts # Browser Supabase client
93+
server.ts # Server Supabase client
94+
middleware.ts # Session refresh + auth redirect logic
95+
proxy.ts # Next.js 16 proxy (replaces middleware.ts)
96+
drizzle/ # Drizzle schema + migrations
97+
drizzle.config.ts # Drizzle config
98+
```

__tests__/sanity.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { render, screen } from "@testing-library/react";
2+
import DashboardPage from "@/app/dashboard/page";
3+
4+
describe("jest setup", () => {
5+
it("runs React + Testing Library", async () => {
6+
const jsx = await DashboardPage();
7+
render(jsx);
8+
expect(screen.getByText("You are admin")).toBeInTheDocument();
9+
});
10+
});

app/api/checkout/route.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { stripe, getOrCreateStripeCustomer } from "@/lib/stripe";
3+
import { createClient } from "@/utils/supabase/server";
4+
5+
export async function POST(request: NextRequest) {
6+
const supabase = await createClient();
7+
const {
8+
data: { user },
9+
} = await supabase.auth.getUser();
10+
11+
if (!user) {
12+
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
13+
}
14+
15+
const { priceId, mode = "subscription" } = await request.json();
16+
17+
if (!priceId) {
18+
return NextResponse.json(
19+
{ error: "Price ID is required" },
20+
{ status: 400 },
21+
);
22+
}
23+
24+
const stripeCustomerId = await getOrCreateStripeCustomer(
25+
user.id,
26+
user.email!,
27+
);
28+
29+
const session = await stripe.checkout.sessions.create({
30+
customer: stripeCustomerId,
31+
mode: mode as "subscription" | "payment",
32+
payment_method_types: ["card"],
33+
line_items: [{ price: priceId, quantity: 1 }],
34+
success_url: `${request.nextUrl.origin}/checkout/success`,
35+
cancel_url: `${request.nextUrl.origin}/checkout/cancel`,
36+
});
37+
38+
return NextResponse.json({ url: session.url });
39+
}

app/api/webhooks/stripe/route.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { stripe, syncStripeData } from "@/lib/stripe";
3+
import { db } from "@/lib/db";
4+
import { profiles, purchases } from "@/lib/db/schema";
5+
import { eq } from "drizzle-orm";
6+
import Stripe from "stripe";
7+
8+
const allowedEvents: Stripe.Event.Type[] = [
9+
"checkout.session.completed",
10+
"customer.subscription.created",
11+
"customer.subscription.updated",
12+
"customer.subscription.deleted",
13+
"customer.subscription.paused",
14+
"customer.subscription.resumed",
15+
"invoice.paid",
16+
"invoice.payment_failed",
17+
"invoice.payment_succeeded",
18+
];
19+
20+
export async function POST(request: NextRequest) {
21+
const body = await request.text();
22+
const signature = request.headers.get("stripe-signature");
23+
24+
if (!signature) {
25+
return NextResponse.json({ error: "No signature" }, { status: 400 });
26+
}
27+
28+
let event: Stripe.Event;
29+
try {
30+
event = stripe.webhooks.constructEvent(
31+
body,
32+
signature,
33+
process.env.STRIPE_WEBHOOK_SECRET!,
34+
);
35+
} catch (err) {
36+
console.error("Webhook signature verification failed:", err);
37+
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
38+
}
39+
40+
if (!allowedEvents.includes(event.type)) {
41+
return NextResponse.json({ received: true });
42+
}
43+
44+
if (event.type === "checkout.session.completed") {
45+
const session = event.data.object as Stripe.Checkout.Session;
46+
47+
if (session.mode === "payment" && session.customer) {
48+
const customerId = session.customer as string;
49+
50+
const profile = await db.query.profiles.findFirst({
51+
where: eq(profiles.stripeCustomerId, customerId),
52+
});
53+
54+
if (profile) {
55+
const lineItems = await stripe.checkout.sessions.listLineItems(
56+
session.id,
57+
{ limit: 1 },
58+
);
59+
const item = lineItems.data[0];
60+
if (item) {
61+
await db.insert(purchases).values({
62+
userId: profile.id,
63+
stripePriceId: item.price?.id ?? "",
64+
stripeSessionId: session.id,
65+
productName: item.description ?? "Product",
66+
amount: session.amount_total ?? 0,
67+
currency: session.currency ?? "cad",
68+
});
69+
}
70+
}
71+
72+
return NextResponse.json({ received: true });
73+
}
74+
}
75+
76+
const { customer: customerId } = event.data.object as {
77+
customer: string;
78+
};
79+
80+
if (typeof customerId !== "string") {
81+
console.error(
82+
`[STRIPE WEBHOOK] No customer ID on event type: ${event.type}`,
83+
);
84+
return NextResponse.json({ received: true });
85+
}
86+
87+
await syncStripeData(customerId);
88+
89+
return NextResponse.json({ received: true });
90+
}

0 commit comments

Comments
 (0)