Skip to content

Commit 35f3524

Browse files
Miriadpm
andcommitted
chore: update lockfile after dependency install
Co-authored-by: pm <pm@miriad.systems>
1 parent 6935912 commit 35f3524

14 files changed

Lines changed: 1478 additions & 613 deletions

File tree

.env.example

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# =============================================================================
2+
# codingcat.dev — Environment Variables
3+
# =============================================================================
4+
# Copy this file to .env.local and fill in the values.
5+
# Variables prefixed with NEXT_PUBLIC_ are exposed to the browser.
6+
# All other variables are server-only.
7+
# =============================================================================
8+
9+
# -----------------------------------------------------------------------------
10+
# Public (client-side) variables — NEXT_PUBLIC_*
11+
# -----------------------------------------------------------------------------
12+
13+
# Sanity CMS — project configuration
14+
NEXT_PUBLIC_SANITY_PROJECT_ID= # Sanity project ID (from sanity.io/manage)
15+
NEXT_PUBLIC_SANITY_DATASET= # Sanity dataset name (e.g. "prod")
16+
NEXT_PUBLIC_SANITY_API_VERSION= # Sanity API version date (e.g. "2024-01-01")
17+
18+
# Site URLs
19+
NEXT_PUBLIC_BASE_URL= # Canonical base URL (e.g. "https://codingcat.dev")
20+
NEXT_PUBLIC_VERCEL_URL= # Vercel preview URL (auto-set by Vercel)
21+
22+
# Algolia search — public keys
23+
NEXT_PUBLIC_ALGOLIA_APP_ID= # Algolia application ID
24+
NEXT_PUBLIC_ALGOLIA_INDEX= # Algolia index name
25+
26+
# Analytics
27+
NEXT_PUBLIC_FB_PIXEL_ID= # Facebook Pixel tracking ID
28+
29+
# Preview mode
30+
NEXT_PUBLIC_PREVIEW_TOKEN_SECRET= # Secret token for Sanity preview mode
31+
32+
# -----------------------------------------------------------------------------
33+
# Server-only variables
34+
# -----------------------------------------------------------------------------
35+
36+
# Sanity CMS — API tokens
37+
SANITY_API_READ_TOKEN= # Sanity read token (viewer role)
38+
SANITY_API_WRITE_TOKEN= # Sanity write token (editor role)
39+
40+
# Algolia search — admin keys
41+
PRIVATE_ALGOLIA_ADMIN_API_KEY= # Algolia admin API key (server-side indexing)
42+
PRIVATE_ALGOLIA_WEBOOK_SECRET= # Shared secret for Algolia webhook verification
43+
44+
# Syndication
45+
PRIVATE_SYNDICATE_WEBOOK_SECRET= # Shared secret for syndication webhook verification
46+
PRIVATE_DEVTO= # Dev.to API key (cross-posting)
47+
PRIVATE_HASHNODE= # Hashnode API key (cross-posting)
48+
49+
# Cloudflare Turnstile (bot protection)
50+
CLOUDFLARE_TURNSTILE_SECRET_KEY= # Turnstile server-side secret key
51+
52+
# Cron / scheduled jobs
53+
CRON_SECRET= # Secret for authenticating cron job requests
54+
55+
# YouTube integration
56+
YOUTUBE_API_KEY= # YouTube Data API v3 key
57+
YOUTUBE_CHANNEL_ID= # YouTube channel ID to fetch videos from
58+
59+
# Vercel
60+
VERCEL_PROJECT_PRODUCTION_URL= # Production URL (auto-set by Vercel)
61+
62+
# -----------------------------------------------------------------------------
63+
# Supabase
64+
# -----------------------------------------------------------------------------
65+
NEXT_PUBLIC_SUPABASE_URL= # Supabase project URL (e.g. "https://xxx.supabase.co")
66+
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Supabase anonymous/public key
67+
SUPABASE_SERVICE_ROLE_KEY= # Supabase service role key (server-only, bypasses RLS)

.github/workflows/ci.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [dev]
6+
pull_request:
7+
branches: [dev]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
env:
14+
NODE_VERSION: "20"
15+
16+
jobs:
17+
ci:
18+
name: Lint, Build & Test
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 15
21+
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: Setup pnpm
27+
uses: pnpm/action-setup@v4
28+
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: ${{ env.NODE_VERSION }}
33+
cache: pnpm
34+
35+
- name: Install dependencies
36+
run: pnpm install --frozen-lockfile
37+
38+
- name: Type-check
39+
run: pnpm exec tsc --noEmit
40+
41+
- name: Lint (Biome)
42+
run: pnpm exec biome check .
43+
44+
- name: Build
45+
run: pnpm build
46+
env:
47+
# Provide dummy values for build-time env vars so Next.js build succeeds
48+
NEXT_PUBLIC_SANITY_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_SANITY_PROJECT_ID }}
49+
NEXT_PUBLIC_SANITY_DATASET: ${{ secrets.NEXT_PUBLIC_SANITY_DATASET }}
50+
NEXT_PUBLIC_SANITY_API_VERSION: ${{ secrets.NEXT_PUBLIC_SANITY_API_VERSION }}
51+
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
52+
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
53+
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
54+
55+
- name: Cache Playwright browsers
56+
uses: actions/cache@v4
57+
id: playwright-cache
58+
with:
59+
path: ~/.cache/ms-playwright
60+
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
61+
62+
- name: Install Playwright browsers
63+
if: steps.playwright-cache.outputs.cache-hit != 'true'
64+
run: pnpm exec playwright install --with-deps chromium
65+
66+
- name: Install Playwright system deps
67+
if: steps.playwright-cache.outputs.cache-hit == 'true'
68+
run: pnpm exec playwright install-deps chromium
69+
70+
- name: Run Playwright tests
71+
run: pnpm exec playwright test
72+
env:
73+
CI: true
74+
PLAYWRIGHT_BASE_URL: http://localhost:3000
75+
NEXT_PUBLIC_SANITY_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_SANITY_PROJECT_ID }}
76+
NEXT_PUBLIC_SANITY_DATASET: ${{ secrets.NEXT_PUBLIC_SANITY_DATASET }}
77+
NEXT_PUBLIC_SANITY_API_VERSION: ${{ secrets.NEXT_PUBLIC_SANITY_API_VERSION }}
78+
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
79+
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
80+
81+
- name: Upload Playwright report
82+
uses: actions/upload-artifact@v4
83+
if: ${{ !cancelled() }}
84+
with:
85+
name: playwright-report
86+
path: playwright-report/
87+
retention-days: 7

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
# testing
1111
/coverage
12+
/test-results/
13+
/playwright-report/
14+
/blob-report/
15+
/playwright/.cache/
1216

1317
# next.js
1418
/.next/

biome.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@
3636
"enabled": true
3737
},
3838
"files": {
39-
"ignore": ["node_modules/**", "sanity/types.ts"]
39+
"ignore": ["node_modules/**", "sanity/types.ts", "supabase/**", "playwright-report/**", "test-results/**"]
4040
},
4141
"javascript": {
42-
"jsxRuntime": "reactClassic",
42+
"jsxRuntime": "automatic",
4343
"formatter": {
4444
"trailingCommas": "all",
4545
"semicolons": "always"

components.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"rsc": true,
55
"tsx": true,
66
"tailwind": {
7-
"config": "tailwind.config.ts",
87
"css": "app/globals.css",
98
"baseColor": "neutral",
109
"cssVariables": true,

e2e/smoke.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("Smoke tests", () => {
4+
test("homepage renders", async ({ page }) => {
5+
await page.goto("/");
6+
await expect(page).toHaveTitle(/codingcat/i);
7+
// Ensure the page has meaningful content (not a blank error page)
8+
await expect(page.locator("body")).not.toBeEmpty();
9+
});
10+
11+
test("/blog page renders", async ({ page }) => {
12+
await page.goto("/blog");
13+
// The blog page should have rendered content
14+
await expect(page.locator("body")).not.toBeEmpty();
15+
// Check for a heading or main content area
16+
await expect(
17+
page.locator("main, [role='main'], article, h1").first(),
18+
).toBeVisible();
19+
});
20+
21+
test("Sanity studio route exists", async ({ page }) => {
22+
const response = await page.goto("/studio");
23+
// The studio route should return a valid response (not 404)
24+
expect(response).not.toBeNull();
25+
expect(response!.status()).toBeLessThan(400);
26+
});
27+
});

lib/supabase/client.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createBrowserClient } from "@supabase/ssr";
2+
3+
/**
4+
* Creates a Supabase client for use in Client Components (browser).
5+
*
6+
* Usage:
7+
* import { createClient } from "@/lib/supabase/client";
8+
* const supabase = createClient();
9+
*/
10+
export function createClient() {
11+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
12+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
13+
14+
if (!supabaseUrl || !supabaseAnonKey) {
15+
throw new Error(
16+
"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables",
17+
);
18+
}
19+
20+
return createBrowserClient(supabaseUrl, supabaseAnonKey);
21+
}

lib/supabase/server.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createServerClient } from "@supabase/ssr";
2+
import { cookies } from "next/headers";
3+
4+
/**
5+
* Creates a Supabase client for use in Server Components, Route Handlers,
6+
* and Server Actions.
7+
*
8+
* Usage:
9+
* import { createClient } from "@/lib/supabase/server";
10+
* const supabase = await createClient();
11+
*/
12+
export async function createClient() {
13+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
14+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
15+
16+
if (!supabaseUrl || !supabaseAnonKey) {
17+
throw new Error(
18+
"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables",
19+
);
20+
}
21+
22+
const cookieStore = await cookies();
23+
24+
return createServerClient(supabaseUrl, supabaseAnonKey, {
25+
cookies: {
26+
getAll() {
27+
return cookieStore.getAll();
28+
},
29+
setAll(cookiesToSet) {
30+
try {
31+
for (const { name, value, options } of cookiesToSet) {
32+
cookieStore.set(name, value, options);
33+
}
34+
} catch {
35+
// The `setAll` method was called from a Server Component.
36+
// This can be ignored if you have middleware refreshing
37+
// user sessions.
38+
}
39+
},
40+
},
41+
});
42+
}
43+
44+
/**
45+
* Creates a Supabase admin client using the service role key.
46+
* WARNING: This bypasses Row Level Security — use only in trusted server contexts.
47+
*
48+
* Usage:
49+
* import { createAdminClient } from "@/lib/supabase/server";
50+
* const supabase = createAdminClient();
51+
*/
52+
export function createAdminClient() {
53+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
54+
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
55+
56+
if (!supabaseUrl || !supabaseServiceRoleKey) {
57+
throw new Error(
58+
"Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY environment variables",
59+
);
60+
}
61+
62+
// Import directly to avoid cookie overhead for admin operations
63+
const { createClient } = require("@supabase/supabase-js");
64+
return createClient(supabaseUrl, supabaseServiceRoleKey, {
65+
auth: {
66+
autoRefreshToken: false,
67+
persistSession: false,
68+
},
69+
});
70+
}

0 commit comments

Comments
 (0)