Skip to content

Commit 39d5708

Browse files
authored
Merge pull request hallofcodes#11 from mrepol742/master
feat: address several known issues
2 parents 6817d68 + 2716958 commit 39d5708

5 files changed

Lines changed: 187 additions & 31 deletions

File tree

.github/workflows/build.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Build Next.js App
2+
3+
on:
4+
push:
5+
branches: ['master']
6+
pull_request:
7+
branches: ['master']
8+
workflow_dispatch:
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 22
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Cache Supabase Docker images
26+
uses: actions/cache@v4
27+
with:
28+
path: /var/lib/docker
29+
key: ${{ runner.os }}-supabase-docker-${{ hashFiles('supabase/config.toml') }}
30+
restore-keys: |
31+
${{ runner.os }}-supabase-docker-
32+
33+
- name: Start Supabase locally in background
34+
run: |
35+
npx --yes supabase start
36+
SUPABASE_URL=$(npx --yes supabase status --output env | awk -F= '/API_URL/ {gsub(/"/,"",$2); print $2}')
37+
SUPABASE_ANON_KEY=$(npx --yes supabase status --output env | awk -F= '/PUBLISHABLE_KEY/ {gsub(/"/,"",$2); print $2}')
38+
SUPABASE_SERVICE_ROLE_KEY=$(npx --yes supabase status --output env | awk -F= '/SERVICE_ROLE_KEY/ {gsub(/"/,"",$2); print $2}')
39+
40+
echo "NEXT_PUBLIC_SUPABASE_URL=$SUPABASE_URL" >> $GITHUB_ENV
41+
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=$SUPABASE_ANON_KEY" >> $GITHUB_ENV
42+
echo "SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY" >> $GITHUB_ENV
43+
44+
sleep 15 # wait
45+
46+
- name: Build project
47+
run: npm run build

app/components/BoardList.tsx

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
44
import { createClient } from "../lib/supabase/client";
55
import Link from "next/link";
66
import { useState } from "react";
7-
import { faKey, faTrash } from "@fortawesome/free-solid-svg-icons";
7+
import { faKey, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
88
import { toast } from "react-toastify";
9+
import { Leaderboard } from "./dashboard/LeaderbordList";
10+
import { User } from "@supabase/supabase-js";
911

1012
export default function BoardList({
13+
user,
1114
board,
1215
}: {
13-
board: { id: string; name: string; slug: string; join_code: string };
16+
user: User;
17+
board: Leaderboard;
1418
}) {
19+
const supabase = createClient();
1520
const [showCodeModal, setShowCodeModal] = useState(false);
1621
const [selectedCode, setSelectedCode] = useState<string | null>(null);
1722
const [showDeleteModal, setShowDeleteModal] = useState(false);
1823

1924
const handleDelete = async () => {
20-
const supabase = await createClient();
2125
const { error } = await supabase
2226
.from("leaderboards")
2327
.delete()
@@ -27,6 +31,74 @@ export default function BoardList({
2731
window.location.reload();
2832
};
2933

34+
const regenerateJoinCode = (boardId: string) => {
35+
const generateJoinCode = new Promise(async (resolve, reject) => {
36+
try {
37+
const joinCode = crypto.randomUUID().slice(0, 8);
38+
const { data, error } = await supabase
39+
.from("leaderboards")
40+
.update({ join_code: joinCode })
41+
.eq("id", boardId)
42+
.select()
43+
.single();
44+
45+
if (error) return reject(error);
46+
47+
resolve(data);
48+
} catch (error) {
49+
reject(error);
50+
}
51+
});
52+
53+
toast.promise(generateJoinCode, {
54+
pending: "Generating new join code...",
55+
success: "Successfully generated new join code.",
56+
error: {
57+
render({ data }) {
58+
const err = data as Error;
59+
return (
60+
err?.message ||
61+
"Failed to generate new join code. Please try again."
62+
);
63+
},
64+
},
65+
});
66+
};
67+
68+
const getJoinCode = (boardId: string) => {
69+
const joinCode: Promise<{ join_code: string }[]> = new Promise(
70+
async (resolve, reject) => {
71+
try {
72+
const { data, error } = await supabase
73+
.from("leaderboards")
74+
.select("join_code")
75+
.eq("id", boardId)
76+
77+
if (error) return reject(error);
78+
79+
resolve(data);
80+
} catch (error) {
81+
reject(error);
82+
}
83+
},
84+
);
85+
86+
toast.promise(joinCode, {
87+
pending: "Getting join code...",
88+
error: {
89+
render({ data }) {
90+
const err = data as Error;
91+
return err?.message || "Failed to get join code. Please try again.";
92+
},
93+
},
94+
});
95+
96+
joinCode.then((data) => {
97+
setSelectedCode(data[0].join_code);
98+
setShowCodeModal(true);
99+
});
100+
};
101+
30102
return (
31103
<>
32104
<div
@@ -39,23 +111,31 @@ export default function BoardList({
39111
</p>
40112
</Link>
41113

42-
<div className="flex items-center gap-2">
43-
<button
44-
onClick={() => setShowDeleteModal(true)}
45-
className="p-2 rounded-lg text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition"
46-
>
47-
<FontAwesomeIcon icon={faTrash} className="text-xs" />
48-
</button>
49-
<button
50-
onClick={() => {
51-
setSelectedCode(board.join_code);
52-
setShowCodeModal(true);
53-
}}
54-
className="p-2 rounded-lg text-gray-600 hover:text-indigo-400 hover:bg-indigo-500/10 transition"
55-
>
56-
<FontAwesomeIcon icon={faKey} className="text-xs" />
57-
</button>
58-
</div>
114+
{user.id === board.owner_id && (
115+
<div className="flex items-center gap-2">
116+
<button
117+
onClick={() => setShowDeleteModal(true)}
118+
className="p-2 rounded-lg text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition"
119+
title="Delete Leaderboard"
120+
>
121+
<FontAwesomeIcon icon={faTrash} className="text-xs" />
122+
</button>
123+
<button
124+
onClick={() => regenerateJoinCode(board.id)}
125+
className="p-2 rounded-lg text-gray-600 hover:text-indigo-400 hover:bg-indigo-500/10 transition"
126+
title="Regenerate Join Code"
127+
>
128+
<FontAwesomeIcon icon={faRefresh} className="text-xs" />
129+
</button>
130+
<button
131+
onClick={() => getJoinCode(board.id)}
132+
className="p-2 rounded-lg text-gray-600 hover:text-indigo-400 hover:bg-indigo-500/10 transition"
133+
title="View Join Code"
134+
>
135+
<FontAwesomeIcon icon={faKey} className="text-xs" />
136+
</button>
137+
</div>
138+
)}
59139
</div>
60140

61141
{showCodeModal && (

app/components/dashboard/LeaderbordList.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import { createClient } from "../../lib/supabase/server";
22
import BoardList from "../BoardList";
33

4+
export interface Leaderboard {
5+
id: string;
6+
name: string;
7+
slug: string;
8+
join_code?: string;
9+
owner_id?: string;
10+
};
11+
12+
export interface LeaderboardMember {
13+
leaderboards: Leaderboard[];
14+
};
15+
416
export default async function LeaderboardsList() {
517
const supabase = await createClient();
618

@@ -12,23 +24,23 @@ export default async function LeaderboardsList() {
1224

1325
const { data: owned } = await supabase
1426
.from("leaderboards")
15-
.select("id, name, slug, join_code")
27+
.select("id, name, slug, owner_id")
1628
.eq("owner_id", user.id);
1729

1830
const { data: joined } = await supabase
1931
.from("leaderboard_members")
20-
.select("leaderboards(id, name, slug, join_code)")
32+
.select("leaderboards(id, name, slug)")
2133
.eq("user_id", user.id)
2234
.neq("role", "owner");
2335

24-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25-
const joinedBoards = joined?.map((j: any) => j.leaderboards) || [];
36+
const joinedBoards =
37+
joined?.flatMap((j: LeaderboardMember) => j.leaderboards) || [];
2638

2739
const ownedCount = owned?.length || 0;
2840
const joinedCount = joinedBoards.length;
2941

3042
return (
31-
<div
43+
<div
3244
className="glass-card p-6 h-full"
3345
data-aos="fade-up"
3446
data-aos-delay="200"
@@ -50,7 +62,7 @@ export default async function LeaderboardsList() {
5062
Owned ({ownedCount})
5163
</p>
5264
{owned.map((board) => (
53-
<BoardList key={board.id} board={board} />
65+
<BoardList key={board.id} user={user} board={board} />
5466
))}
5567
</>
5668
)}
@@ -61,8 +73,8 @@ export default async function LeaderboardsList() {
6173
<p className="text-[10px] uppercase tracking-widest text-gray-700 font-semibold mt-4 mb-2">
6274
Joined ({joinedCount})
6375
</p>
64-
{joinedBoards.map((board: { id: string; name: string; slug: string; join_code: string }) => (
65-
<BoardList key={board.id} board={board} />
76+
{joinedBoards.map((board) => (
77+
<BoardList key={board.id} user={user} board={board} />
6678
))}
6779
</>
6880
)}

app/lib/proxy/auth.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ export default async function Auth(req: NextRequest) {
3333
const { pathname } = req.nextUrl;
3434

3535
const protectedRoutes = ["/dashboard", "/logout"];
36-
const isProtectedRoute = protectedRoutes.some((route) =>
37-
pathname.startsWith(route),
38-
);
36+
const isProtectedRoute = protectedRoutes.some((route) => {
37+
const regex = new RegExp(`^${route}(/.*)?$`);
38+
return regex.test(pathname);
39+
});
3940

4041
if (isProtectedRoute && !user) {
4142
console.log("User is not authenticated, redirecting to login.");

sql/public.leaderboards.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,19 @@ create policy "Users can join leaderboard"
7373
on public.leaderboard_members
7474
for insert
7575
with check (auth.uid() = user_id);
76+
77+
create policy "Members can leave leaderboard"
78+
on public.leaderboard_members
79+
for delete
80+
using (user_id = auth.uid());
81+
82+
create policy "Owner can manage members"
83+
on public.leaderboard_members
84+
for delete
85+
using (
86+
leaderboard_id IN (
87+
select leaderboard_id
88+
from public.leaderboards
89+
where owner_id = auth.uid()
90+
)
91+
);

0 commit comments

Comments
 (0)