Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
07898e7
feat(app): initial architecture
martin0024 Mar 22, 2026
77a06fa
fix(drizzle): moved to lib/db
martin0024 Mar 23, 2026
a78bc14
Merge pull request #34 from hack4impact/project-structure
martin0024 Mar 24, 2026
df87ca6
docs
Berny-ft Mar 28, 2026
097b709
schema
Berny-ft Mar 28, 2026
ed1a032
added missing column in docs
Berny-ft Mar 28, 2026
da1742a
pnpm setup
Berny-ft Mar 29, 2026
24f9702
fixed services
Berny-ft Mar 29, 2026
07e6de7
fixed webinars
Berny-ft Mar 29, 2026
73ff387
removed seats from webinars
Berny-ft Mar 29, 2026
4d93dbd
updated docs
Berny-ft Mar 29, 2026
34bfc5f
feat(dashboard): add admin-only access to dashboard page and user rol…
Jxl-s Apr 1, 2026
1ac5d67
feat(login): enhance signup form to include first and last name fields
Jxl-s Apr 1, 2026
c2ebadf
feat(login): implement login and signup schemas with validation
Jxl-s Apr 1, 2026
3c3b438
feat(dashboard): simplify admin dashboard by removing user authentica…
Jxl-s Apr 1, 2026
7ba83a7
feat: updated schema to have subscriptions and purchases
Berny-ft Apr 1, 2026
ba9a262
removed unused files
Berny-ft Apr 1, 2026
f57b223
fixed schedule table, timeslots, youtube_url and update times
Berny-ft Apr 1, 2026
9f607ff
removed the schedules table , added a scheduled at blob to services t…
Berny-ft Apr 1, 2026
b606166
Merge pull request #38 from hack4impact/feature/4-db-schema
Berny-ft Apr 2, 2026
971fb30
feat(roles): add roles management with constants and types; update mi…
Jxl-s Apr 2, 2026
d7f06b3
Merge remote-tracking branch 'origin/dev' into feature/7-roles
martin0024 Apr 4, 2026
6d579ff
Merge pull request #37 from hack4impact/feature/7-roles
martin0024 Apr 4, 2026
187876b
logic payment
martin0024 Mar 29, 2026
91b81b9
fix(stripe): ts type
martin0024 Apr 6, 2026
44cf1f3
config(gitignore): add .cursor/ for stripe mcp
martin0024 Apr 6, 2026
4766988
fix(app): removed product price id hardcoded
martin0024 Apr 13, 2026
1746bd2
Merge pull request #40 from hack4impact/feature/10-payment
RenaudBernier Apr 13, 2026
d3110ab
feat(tests): add Jest
RenaudBernier Apr 15, 2026
514836e
feat(ci): add CI workflow
RenaudBernier Apr 15, 2026
c3e3999
chore(ci): update Node.js version from 20 to 22
RenaudBernier Apr 15, 2026
116a30c
chore(ci): restrict CI workflow branches to main and dev
RenaudBernier Apr 15, 2026
e27bded
refactor(services): remove ServicesSectionHeader component
RenaudBernier Apr 17, 2026
020e20b
chore(ci): update Node.js version from 22 to 25
RenaudBernier Apr 17, 2026
fd32eda
test(sanity): refactor test to use a dedicated react component
RenaudBernier Apr 17, 2026
94e0e76
test(sanity): update test to render existing DashboardPage component
RenaudBernier Apr 17, 2026
43805ed
Merge pull request #50 from hack4impact/feature/48-jest-ci
martin0024 Apr 17, 2026
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_your-key
DATABASE_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres
STRIPE_PRICE_ID=
STRIPE_SECRET_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main, dev]
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "25"
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Unit tests
run: npm run test:ci
64 changes: 64 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem
Thumbs.db

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files
.env
.env.*
!.env.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# editors
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea/
*.swp
*.swo
*~

# drizzle
drizzle/meta/

# supabase
.supabase/


.agents/
.playwright-mcp/
.mcp.json
.claude/
.cursor/
3 changes: 3 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tabWidth": 3
}
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

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.
<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
99 changes: 98 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,98 @@
# mcld-project
# mcld-project

## Stack

- **Framework**: Next.js 16 (App Router, Turbopack)
- **Auth**: Supabase Auth (email/password, SSR via `@supabase/ssr`)
- **Database**: PostgreSQL via Supabase + Drizzle ORM
- **UI**: shadcn/ui (Radix) + Tailwind
- **Hosting**: Vercel

## Setup

### 1. Install dependencies

```bash
pnpm install
```

### 2. Configure environment

```bash
cp .env.example .env.local
sudo nano .env
```

Fill in your Supabase credentials from [supabase.com/dashboard](https://supabase.com/dashboard) > Project Settings > API (or Dashboard > Framework > Next.js > Environment Variables):

only on `.env.local`:

- `NEXT_PUBLIC_SUPABASE_URL` — Project URL
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY` — Publishable (anon) key

only on `.env`:

- `DATABASE_URL` — Connection string (Dashboard > Direct (connextion string) > Copy connection string (for the password go to Database > Settings > Database password > Reset password))

### 3. Configure Supabase Auth (if you dont use port 3000)

In your Supabase dashboard under **Authentication > URL Configuration**:

- **Site URL**: `http://localhost:PORT`
- **Redirect URLs**: add `http://localhost:PORT/auth/callback`

Make sure **Email** provider is enabled under **Authentication > Sign In/Providers**.

### 4. Set up the database

```bash
pnpm db:push # push schema to Supabase
pnpm db:generate # generate migration files
pnpm db:migrate # run migrations
```

### 5. Run

```bash
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000). You'll be redirected to `/login` if not authenticated.

## Adding shadcn/ui components

```bash
npx shadcn@latest add <component>
```

Examples:

```bash
npx shadcn@latest add button
npx shadcn@latest add card input label
npx shadcn@latest add dialog dropdown-menu
```

Components are installed to `components/ui/`. Browse available components at [ui.shadcn.com](https://ui.shadcn.com).

## Project structure

```
app/
layout.tsx # Root layout (Helvetica font)
page.tsx # Home (protected, shows user + sign out)
login/
page.tsx # Login / signup form
actions.ts # Server actions (login, signup, signout)
auth/
callback/
route.ts # Email confirmation callback
components/ui/ # shadcn/ui components
utils/supabase/
client.ts # Browser Supabase client
server.ts # Server Supabase client
middleware.ts # Session refresh + auth redirect logic
proxy.ts # Next.js 16 proxy (replaces middleware.ts)
drizzle/ # Drizzle schema + migrations
drizzle.config.ts # Drizzle config
```
10 changes: 10 additions & 0 deletions __tests__/sanity.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { render, screen } from "@testing-library/react";
import DashboardPage from "@/app/dashboard/page";

describe("jest setup", () => {
it("runs React + Testing Library", async () => {
const jsx = await DashboardPage();
render(jsx);
expect(screen.getByText("You are admin")).toBeInTheDocument();
});
});
39 changes: 39 additions & 0 deletions app/api/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from "next/server";
import { stripe, getOrCreateStripeCustomer } from "@/lib/stripe";
import { createClient } from "@/utils/supabase/server";

export async function POST(request: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

const { priceId, mode = "subscription" } = await request.json();

if (!priceId) {
return NextResponse.json(
{ error: "Price ID is required" },
{ status: 400 },
);
}

const stripeCustomerId = await getOrCreateStripeCustomer(
user.id,
user.email!,
);

const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: mode as "subscription" | "payment",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${request.nextUrl.origin}/checkout/success`,
cancel_url: `${request.nextUrl.origin}/checkout/cancel`,
});

return NextResponse.json({ url: session.url });
}
90 changes: 90 additions & 0 deletions app/api/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextRequest, NextResponse } from "next/server";
import { stripe, syncStripeData } from "@/lib/stripe";
import { db } from "@/lib/db";
import { profiles, purchases } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import Stripe from "stripe";

const allowedEvents: Stripe.Event.Type[] = [
"checkout.session.completed",
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted",
"customer.subscription.paused",
"customer.subscription.resumed",
"invoice.paid",
"invoice.payment_failed",
"invoice.payment_succeeded",
];

export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");

if (!signature) {
return NextResponse.json({ error: "No signature" }, { status: 400 });
}

let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err) {
console.error("Webhook signature verification failed:", err);
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}

if (!allowedEvents.includes(event.type)) {
return NextResponse.json({ received: true });
}

if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;

if (session.mode === "payment" && session.customer) {
const customerId = session.customer as string;

const profile = await db.query.profiles.findFirst({
where: eq(profiles.stripeCustomerId, customerId),
});

if (profile) {
const lineItems = await stripe.checkout.sessions.listLineItems(
session.id,
{ limit: 1 },
);
const item = lineItems.data[0];
if (item) {
await db.insert(purchases).values({
userId: profile.id,
stripePriceId: item.price?.id ?? "",
stripeSessionId: session.id,
productName: item.description ?? "Product",
amount: session.amount_total ?? 0,
currency: session.currency ?? "cad",
});
}
}

return NextResponse.json({ received: true });
}
}

const { customer: customerId } = event.data.object as {
customer: string;
};

if (typeof customerId !== "string") {
console.error(
`[STRIPE WEBHOOK] No customer ID on event type: ${event.type}`,
);
return NextResponse.json({ received: true });
}

await syncStripeData(customerId);

return NextResponse.json({ received: true });
}
18 changes: 18 additions & 0 deletions app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";

export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
const next = searchParams.get("next") ?? "/";

if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}

return NextResponse.redirect(`${origin}/login?error=Could+not+authenticate`);
}
Loading
Loading