Skip to content

Commit 9e3a094

Browse files
authored
ci: add GitHub Actions workflow for CI/CD pipeline (#31)
* ci: add GitHub Actions workflow for CI/CD pipeline * ci: update GitHub Actions workflow to use latest actions and set environment variables * ci: update biome formatter and linter to specify file extensions * ci: add debug file listing and improve Biome formatter & linter steps * ci: update file search patterns to exclude additional directories in CI workflow * style: add semicolons and improve formatting for consistency across files * style: format code in breadcrumb.tsx and db.ts files * chore: update biome.json to adjust linter rules
1 parent f6a38d3 commit 9e3a094

File tree

9 files changed

+734
-590
lines changed

9 files changed

+734
-590
lines changed

.github/workflows/ci.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
lint-and-build:
10+
name: Lint and Build
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Node.js (Use Required Version)
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 20.18.0 # Set to the required version
21+
22+
- name: Install dependencies
23+
run: npm install
24+
25+
- name: Set up environment variables
26+
run: |
27+
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env
28+
echo "NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}" >> .env
29+
echo "NEXT_PUBLIC_APP_URL=${{ secrets.NEXT_PUBLIC_APP_URL }}" >> .env
30+
echo "RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}" >> .env
31+
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
32+
echo "DATABASE_URL_UNPOOLED=${{ secrets.DATABASE_URL_UNPOOLED }}" >> .env
33+
echo "PGHOST=${{ secrets.PGHOST }}" >> .env
34+
echo "PGHOST_UNPOOLED=${{ secrets.PGHOST_UNPOOLED }}" >> .env
35+
echo "PGUSER=${{ secrets.PGUSER }}" >> .env
36+
echo "PGDATABASE=${{ secrets.PGDATABASE }}" >> .env
37+
echo "PGPASSWORD=${{ secrets.PGPASSWORD }}" >> .env
38+
echo "POSTGRES_URL=${{ secrets.POSTGRES_URL }}" >> .env
39+
echo "POSTGRES_URL_NON_POOLING=${{ secrets.POSTGRES_URL_NON_POOLING }}" >> .env
40+
echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env
41+
echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env
42+
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env
43+
echo "POSTGRES_DATABASE=${{ secrets.POSTGRES_DATABASE }}" >> .env
44+
echo "POSTGRES_URL_NO_SSL=${{ secrets.POSTGRES_URL_NO_SSL }}" >> .env
45+
echo "POSTGRES_PRISMA_URL=${{ secrets.POSTGRES_PRISMA_URL }}" >> .env
46+
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
47+
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
48+
echo "GITHUB_ID=${{ secrets.GH_ID }}" >> .env
49+
echo "GITHUB_SECRET=${{ secrets.GH_SECRET }}" >> .env
50+
51+
- name: Debug File Listing
52+
run: |
53+
echo "Checking for matching files..."
54+
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" -o -name "*.css" -o -name "*.scss" \) \
55+
! -path "./node_modules/*" ! -path "./dist/*" ! -path "./.git/*" ! -path "./coverage/*" \
56+
|| echo "No matching files found."
57+
58+
- name: Run Biome Formatter & Linter
59+
run: |
60+
FILES=$(find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" -o -name "*.css" -o -name "*.scss" \) \
61+
! -path "./node_modules/*" ! -path "./dist/*" ! -path "./.git/*" ! -path "./coverage/*")
62+
63+
if [ -n "$FILES" ]; then
64+
echo "Running Biome on found files..."
65+
echo "$FILES" | xargs npx biome format --write
66+
echo "$FILES" | xargs npx biome lint --write
67+
else
68+
echo "No files found to format or lint. Skipping."
69+
fi
70+
71+
- name: Run Build
72+
run: npm run build

app/admin/page.tsx

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1-
import { authOptions } from "@/lib/auth.config"
2-
import { getServerSession } from "next-auth/next"
3-
import { redirect } from "next/navigation"
1+
import { authOptions } from "@/lib/auth.config";
2+
import { getServerSession } from "next-auth/next";
3+
import { redirect } from "next/navigation";
44

55
export default async function AdminDashboard() {
6-
const session = await getServerSession(authOptions)
6+
const session = await getServerSession(authOptions);
77

8-
if (!session || session.user.role !== "ADMIN") {
9-
redirect("/unauthorized")
10-
}
8+
if (!session || session.user.role !== "ADMIN") {
9+
redirect("/unauthorized");
10+
}
1111

12-
return (
13-
<div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
14-
<div className="relative py-3 sm:max-w-xl sm:mx-auto">
15-
<div className="absolute inset-0 bg-gradient-to-r from-purple-400 to-pink-500 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl"></div>
16-
<div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
17-
<div className="max-w-md mx-auto">
18-
<div>
19-
<h1 className="text-2xl font-semibold">Welcome to the Admin Dashboard!</h1>
20-
</div>
21-
<div className="divide-y divide-gray-200">
22-
<div className="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
23-
<p>You are signed in as an admin: {session.user.email}</p>
24-
<p>Your role is: {session.user.role}</p>
25-
</div>
26-
</div>
27-
</div>
28-
</div>
29-
</div>
30-
</div>
31-
)
12+
return (
13+
<div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
14+
<div className="relative py-3 sm:max-w-xl sm:mx-auto">
15+
<div className="absolute inset-0 bg-gradient-to-r from-purple-400 to-pink-500 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl" />
16+
<div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
17+
<div className="max-w-md mx-auto">
18+
<div>
19+
<h1 className="text-2xl font-semibold">
20+
Welcome to the Admin Dashboard!
21+
</h1>
22+
</div>
23+
<div className="divide-y divide-gray-200">
24+
<div className="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
25+
<p>You are signed in as an admin: {session.user.email}</p>
26+
<p>Your role is: {session.user.role}</p>
27+
</div>
28+
</div>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
);
3234
}
33-

app/auth/forgot-password/page.tsx

Lines changed: 97 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,109 @@
1-
"use client"
1+
"use client";
22

3-
import { useState } from "react"
4-
import { motion } from "framer-motion"
5-
import Image from "next/image"
6-
import { useRouter } from "next/navigation"
3+
import { useState } from "react";
4+
import { motion } from "framer-motion";
5+
import Image from "next/image";
6+
import { useRouter } from "next/navigation";
77

88
export default function ForgotPassword() {
9-
const [email, setEmail] = useState("")
10-
const [error, setError] = useState("")
11-
const [success, setSuccess] = useState("")
12-
const [isLoading, setIsLoading] = useState(false)
13-
const router = useRouter()
9+
const [email, setEmail] = useState("");
10+
const [error, setError] = useState("");
11+
const [success, setSuccess] = useState("");
12+
const [isLoading, setIsLoading] = useState(false);
13+
const router = useRouter();
1414

15-
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
16-
e.preventDefault()
17-
setError("")
18-
setSuccess("")
19-
setIsLoading(true)
15+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
16+
e.preventDefault();
17+
setError("");
18+
setSuccess("");
19+
setIsLoading(true);
2020

21-
try {
22-
const response = await fetch("/api/auth/forgot-password", {
23-
method: "POST",
24-
headers: { "Content-Type": "application/json" },
25-
body: JSON.stringify({ email }),
26-
})
21+
try {
22+
const response = await fetch("/api/auth/forgot-password", {
23+
method: "POST",
24+
headers: { "Content-Type": "application/json" },
25+
body: JSON.stringify({ email }),
26+
});
2727

28-
if (response.ok) {
29-
setSuccess("Password reset link sent to your email.")
30-
} else {
31-
const data = await response.json()
32-
setError(data.message || "Failed to send reset link.")
33-
}
34-
} catch (error) {
35-
setError(`An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`)
36-
} finally {
37-
setIsLoading(false)
38-
}
39-
}
28+
if (response.ok) {
29+
setSuccess("Password reset link sent to your email.");
30+
} else {
31+
const data = await response.json();
32+
setError(data.message || "Failed to send reset link.");
33+
}
34+
} catch (error) {
35+
setError(
36+
`An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`,
37+
);
38+
} finally {
39+
setIsLoading(false);
40+
}
41+
};
4042

41-
return (
42-
<div className="min-h-screen flex items-center justify-center bg-[#dffce8] py-12 px-4 sm:px-6 lg:px-8">
43-
<div className="max-w-md w-full space-y-6 bg-white p-8 rounded-lg shadow-lg">
44-
<div className="flex flex-col items-center">
45-
<Image src="/logo.svg" alt="Boundless" width={180} height={180} className="mb-4" />
46-
<h2 className="mt-2 text-center text-3xl font-extrabold text-[#194247]">Forgot Password</h2>
47-
<p className="mt-2 text-center text-sm text-gray-600">Enter your email to receive a password reset link.</p>
48-
</div>
43+
return (
44+
<div className="min-h-screen flex items-center justify-center bg-[#dffce8] py-12 px-4 sm:px-6 lg:px-8">
45+
<div className="max-w-md w-full space-y-6 bg-white p-8 rounded-lg shadow-lg">
46+
<div className="flex flex-col items-center">
47+
<Image
48+
src="/logo.svg"
49+
alt="Boundless"
50+
width={180}
51+
height={180}
52+
className="mb-4"
53+
/>
54+
<h2 className="mt-2 text-center text-3xl font-extrabold text-[#194247]">
55+
Forgot Password
56+
</h2>
57+
<p className="mt-2 text-center text-sm text-gray-600">
58+
Enter your email to receive a password reset link.
59+
</p>
60+
</div>
4961

50-
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
51-
<div>
52-
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
53-
<input
54-
id="email"
55-
name="email"
56-
type="email"
57-
autoComplete="email"
58-
required
59-
value={email}
60-
onChange={(e) => setEmail(e.target.value)}
61-
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#194247] focus:border-[#194247]"
62-
/>
63-
</div>
62+
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
63+
<div>
64+
<label
65+
htmlFor="email"
66+
className="block text-sm font-medium text-gray-700"
67+
>
68+
Email address
69+
</label>
70+
<input
71+
id="email"
72+
name="email"
73+
type="email"
74+
autoComplete="email"
75+
required
76+
value={email}
77+
onChange={(e) => setEmail(e.target.value)}
78+
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#194247] focus:border-[#194247]"
79+
/>
80+
</div>
6481

65-
{error && <p className="text-red-500 text-sm text-center">{error}</p>}
66-
{success && <p className="text-green-500 text-sm text-center">{success}</p>}
82+
{error && <p className="text-red-500 text-sm text-center">{error}</p>}
83+
{success && (
84+
<p className="text-green-500 text-sm text-center">{success}</p>
85+
)}
6786

68-
<motion.button
69-
whileTap={{ scale: 0.95 }}
70-
type="submit"
71-
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#194247] hover:bg-[#153a3f] focus:outline-none"
72-
disabled={isLoading}
73-
>
74-
{isLoading ? "Sending..." : "Send Reset Link"}
75-
</motion.button>
76-
</form>
87+
<motion.button
88+
whileTap={{ scale: 0.95 }}
89+
type="submit"
90+
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#194247] hover:bg-[#153a3f] focus:outline-none"
91+
disabled={isLoading}
92+
>
93+
{isLoading ? "Sending..." : "Send Reset Link"}
94+
</motion.button>
95+
</form>
7796

78-
<div className="text-center">
79-
<button
80-
onClick={() => router.push("/auth/signin")}
81-
className="text-sm text-[#194247] hover:underline"
82-
>
83-
Back to Sign In
84-
</button>
85-
</div>
86-
</div>
87-
</div>
88-
)
97+
<div className="text-center">
98+
<button
99+
type="button"
100+
onClick={() => router.push("/auth/signin")}
101+
className="text-sm text-[#194247] hover:underline"
102+
>
103+
Back to Sign In
104+
</button>
105+
</div>
106+
</div>
107+
</div>
108+
);
89109
}

0 commit comments

Comments
 (0)