Skip to content

Commit 4b1a51c

Browse files
Merge pull request #15 from codebuilderinc/chore/cloudflare-pages-workflows
Chore/cloudflare pages workflows
2 parents 150f430 + bde0c37 commit 4b1a51c

6 files changed

Lines changed: 284 additions & 58 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: ☁️ Deploy to Cloudflare Pages (Runtime)
2+
3+
on:
4+
push:
5+
branches: ['main']
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: cloudflare-pages-runtime-${{ github.ref_name }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
deploy-runtime:
17+
name: 🏗 Build and Deploy Runtime
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: 🔍 Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: 🔒 Verify Google services secret exists
25+
run: |
26+
if [ -z "${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}" ]; then
27+
echo "❌ GOOGLE_SERVICES_JSON_BASE64 secret is missing"
28+
exit 1
29+
fi
30+
31+
- name: 📁 Create google-services.json
32+
run: |
33+
echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > google-services.json
34+
if ! jq empty google-services.json; then
35+
echo "❌ google-services.json is invalid"
36+
exit 1
37+
fi
38+
env:
39+
GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}
40+
41+
- name: 📦 Install pnpm
42+
run: npm install -g pnpm@10
43+
44+
- name: ⚙️ Setup Node.js
45+
uses: actions/setup-node@v4
46+
with:
47+
node-version: '24'
48+
cache: 'pnpm'
49+
50+
- name: 📥 Install dependencies
51+
run: pnpm install --frozen-lockfile
52+
53+
- name: 🏗 Build Next.js for runtime
54+
env:
55+
NEXT_OUTPUT_MODE: standalone
56+
run: pnpm build
57+
58+
- name: 🏗 Build Cloudflare Pages output (functions + static)
59+
run: pnpm dlx @cloudflare/next-on-pages@1
60+
61+
- name: 🔎 Verify Cloudflare Pages project exists
62+
env:
63+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
64+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
65+
PROJECT_NAME: ${{ secrets.CLOUDFLARE_PAGES_PROJECT_NAME_RUNTIME }}
66+
run: |
67+
RESPONSE=$(curl -fsSL \
68+
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
69+
-H "Content-Type: application/json" \
70+
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects")
71+
72+
COUNT=$(printf '%s' "$RESPONSE" | jq '.result | length')
73+
MATCH=$(printf '%s' "$RESPONSE" | jq --arg name "$PROJECT_NAME" 'any(.result[]?; .name == $name)')
74+
75+
echo "Pages projects visible in this account: $COUNT"
76+
77+
if [ "$MATCH" != "true" ]; then
78+
echo "Configured Pages project was not found in the provided Cloudflare account."
79+
echo "Verify CLOUDFLARE_ACCOUNT_ID points to the same account that owns the Pages project."
80+
echo "Visible project names:"
81+
printf '%s' "$RESPONSE" | jq -r '.result[]?.name'
82+
exit 1
83+
fi
84+
85+
echo "Configured Pages project exists in the target account."
86+
87+
- name: 🚀 Deploy runtime app to Cloudflare Pages
88+
uses: cloudflare/wrangler-action@v3
89+
with:
90+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
91+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
92+
command: >-
93+
pages deploy .vercel/output/static
94+
--project-name=${{ secrets.CLOUDFLARE_PAGES_PROJECT_NAME_RUNTIME }}
95+
--branch=${{ github.ref_name }}
96+
--commit-hash=${{ github.sha }}
97+
--functions=.vercel/output/functions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: ☁️ Deploy to Cloudflare Pages
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: cloudflare-pages-${{ github.ref_name }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
deploy:
17+
name: 🏗 Build and Deploy
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: 🔍 Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: 📦 Install pnpm
25+
run: npm install -g pnpm@10
26+
27+
- name: ⚙️ Setup Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: '24'
31+
cache: 'pnpm'
32+
33+
- name: 🚫 Remove server-only paths for export build
34+
run: |
35+
echo "Pruning dynamic/server paths not supported by static export..."
36+
rm -rf src/app/api src/server src/proxy.ts src/app/jobs/[id] src/app/[...not-found] prisma.config.ts
37+
38+
- name: 📥 Install dependencies
39+
run: pnpm install --frozen-lockfile
40+
41+
- name: 🏗 Build static export
42+
env:
43+
NEXT_OUTPUT_MODE: export
44+
GITHUB_PAGES: 1
45+
NEXT_BASE_PATH: ''
46+
run: |
47+
pnpm build
48+
49+
- name: 🔎 Verify Cloudflare Pages project exists
50+
env:
51+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
52+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
53+
PROJECT_NAME: ${{ secrets.CLOUDFLARE_PAGES_PROJECT_NAME }}
54+
run: |
55+
RESPONSE=$(curl -fsSL \
56+
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
57+
-H "Content-Type: application/json" \
58+
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects")
59+
60+
COUNT=$(printf '%s' "$RESPONSE" | jq '.result | length')
61+
MATCH=$(printf '%s' "$RESPONSE" | jq --arg name "$PROJECT_NAME" 'any(.result[]?; .name == $name)')
62+
63+
echo "Pages projects visible in this account: $COUNT"
64+
65+
if [ "$MATCH" != "true" ]; then
66+
echo "Configured Pages project was not found in the provided Cloudflare account."
67+
echo "Verify CLOUDFLARE_ACCOUNT_ID points to the same account that owns the Pages project."
68+
echo "Visible project names:"
69+
printf '%s' "$RESPONSE" | jq -r '.result[]?.name'
70+
exit 1
71+
fi
72+
73+
echo "Configured Pages project exists in the target account."
74+
75+
- name: 🚀 Deploy to Cloudflare Pages
76+
uses: cloudflare/wrangler-action@v3
77+
with:
78+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
79+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
80+
command: >-
81+
pages deploy out
82+
--project-name=${{ secrets.CLOUDFLARE_PAGES_PROJECT_NAME }}
83+
--branch=${{ github.ref_name }}

.github/workflows/deploy-docker.yml

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/app/jobs/details/page.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client"
2+
3+
import React, { useEffect, useMemo, useState } from 'react'
4+
import Link from 'next/link'
5+
import { useSearchParams } from 'next/navigation'
6+
import JobDetails from '@/components/jobs/JobDetails'
7+
import type { ApiResponse, JobWithRelations } from '@/lib/jobs/types'
8+
9+
type LoadState = 'idle' | 'loading' | 'error' | 'success'
10+
11+
export default function JobDetailsStaticPage() {
12+
const searchParams = useSearchParams()
13+
const [job, setJob] = useState<JobWithRelations | null>(null)
14+
const [state, setState] = useState<LoadState>('idle')
15+
const [errorMessage, setErrorMessage] = useState<string>('')
16+
17+
const jobId = useMemo(() => {
18+
const raw = searchParams.get('id')
19+
if (!raw) return null
20+
21+
const parsed = parseInt(raw, 10)
22+
return Number.isNaN(parsed) ? null : parsed
23+
}, [searchParams])
24+
25+
useEffect(() => {
26+
if (jobId === null) {
27+
setState('error')
28+
setErrorMessage('Missing or invalid job id in the URL.')
29+
setJob(null)
30+
return
31+
}
32+
33+
const controller = new AbortController()
34+
35+
const loadJob = async () => {
36+
try {
37+
setState('loading')
38+
setErrorMessage('')
39+
40+
const res = await fetch(`https://api.codebuilder.org/jobs/${jobId}`, {
41+
signal: controller.signal,
42+
})
43+
44+
if (!res.ok) {
45+
throw new Error('Job not found.')
46+
}
47+
48+
const json: ApiResponse<JobWithRelations> | JobWithRelations = await res.json()
49+
const data = (json as any)?.success === true ? (json as any).data : json
50+
51+
if (!data) {
52+
throw new Error('Job payload is empty.')
53+
}
54+
55+
setJob(data)
56+
setState('success')
57+
} catch (error: any) {
58+
if (controller.signal.aborted) {
59+
return
60+
}
61+
62+
setJob(null)
63+
setState('error')
64+
setErrorMessage(error?.message || 'Unable to load job details.')
65+
}
66+
}
67+
68+
loadJob()
69+
70+
return () => controller.abort()
71+
}, [jobId])
72+
73+
return (
74+
<div className="flex flex-col inset-0 z-50 bg-primary transition-transform">
75+
<section className="bg-gray-100 py-4 md:py-6">
76+
<div className="container mx-auto py-16 px-8 md:px-20 lg:px-32">
77+
<nav className="mb-6">
78+
<Link
79+
href="/jobs"
80+
className="text-blue-600 hover:text-blue-800 transition-colors duration-200"
81+
>
82+
← Back to Jobs
83+
</Link>
84+
</nav>
85+
86+
{state === 'loading' && (
87+
<div className="rounded-lg bg-white p-6 shadow-md text-gray-700">Loading job details...</div>
88+
)}
89+
90+
{state === 'error' && (
91+
<div className="rounded-lg bg-white p-6 shadow-md">
92+
<h1 className="mb-2 text-2xl font-semibold text-gray-800">Job Not Found</h1>
93+
<p className="text-gray-600">{errorMessage || 'This job does not exist or is unavailable.'}</p>
94+
</div>
95+
)}
96+
97+
{state === 'success' && job && <JobDetails job={job} />}
98+
</div>
99+
</section>
100+
</div>
101+
)
102+
}

src/components/index/contact-banner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const ContactSection: React.FC = () => {
4545
}
4646

4747
return (
48-
<section ref={ref} className="">
48+
<section ref={ref} className="border-b-1 border-[#FFFFFF]">
4949
<motion.div
5050
className="h-24 text-white flex items-center justify-center relative overflow-hidden"
5151
initial={{

src/components/jobs/JobsTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const JobsTable: React.FC<Props> = ({
4545
}
4646

4747
const handleRowClick = (jobId: number) => {
48-
router.push(`/jobs/${jobId}`)
48+
router.push(`/jobs/details?id=${jobId}`)
4949
}
5050

5151
const colSpan = 8

0 commit comments

Comments
 (0)