diff --git a/app/components/BoardList.tsx b/app/components/BoardList.tsx index c5e0901..e4b3fe1 100644 --- a/app/components/BoardList.tsx +++ b/app/components/BoardList.tsx @@ -4,7 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createClient } from "../lib/supabase/client"; import Link from "next/link"; import { useState } from "react"; -import { faKey, faRotateRight, faTrashAlt, faChevronRight, faServer } from "@fortawesome/free-solid-svg-icons"; +import { createPortal } from "react-dom"; +import { faKey, faRotateRight, faTrashAlt, faChevronRight, faServer, faRightFromBracket } from "@fortawesome/free-solid-svg-icons"; import { toast } from "react-toastify"; import { Leaderboard } from "./dashboard/LeaderbordList"; import { User } from "@supabase/supabase-js"; @@ -12,9 +13,12 @@ import { User } from "@supabase/supabase-js"; export default function BoardList({ user, board, + allowLeave = false, }: { user: User; board: Leaderboard; + /** When true (joined networks only), show Leave to remove membership */ + allowLeave?: boolean; }) { const supabase = createClient(); const [showCodeModal, setShowCodeModal] = useState(false); @@ -24,6 +28,8 @@ export default function BoardList({ ? `${window.location.origin}/join?id=${selectedCode}` : ""; const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showLeaveModal, setShowLeaveModal] = useState(false); + const [leaving, setLeaving] = useState(false); const handleDelete = async () => { const { error } = await supabase @@ -35,6 +41,25 @@ export default function BoardList({ window.location.reload(); }; + const handleLeave = async () => { + setLeaving(true); + const { error } = await supabase + .from("leaderboard_members") + .delete() + .eq("leaderboard_id", board.id) + .eq("user_id", user.id); + + setLeaving(false); + setShowLeaveModal(false); + + if (error) { + toast.error(error.message || "Could not leave this leaderboard."); + return; + } + toast.success("You left the leaderboard."); + window.location.reload(); + }; + const regenerateJoinCode = (boardId: string) => { const generateJoinCode = new Promise(async (resolve, reject) => { try { @@ -151,6 +176,19 @@ export default function BoardList({ )} + + {allowLeave && user.id !== board.owner_id && ( +
+ +
+ )} {showCodeModal && ( @@ -206,6 +244,39 @@ export default function BoardList({ )} + {showLeaveModal && typeof document !== "undefined" && createPortal( +
+
+
+

Leave leaderboard?

+

+ You'll be removed from{" "} + {board.name}. + You can rejoin later with an invite link or code. +

+
+ + +
+
+
, + document.body + )} + {showDeleteModal && (
diff --git a/app/components/LeaderboardTable.tsx b/app/components/LeaderboardTable.tsx index 6516665..be57a2d 100644 --- a/app/components/LeaderboardTable.tsx +++ b/app/components/LeaderboardTable.tsx @@ -6,9 +6,9 @@ type Member = { role: string; email: string; total_seconds: number; - languages: { name: string }[]; - operating_systems: { name: string }[]; - editors: { name: string }[]; + languages: { name: string }[] | null; + operating_systems: { name: string }[] | null; + editors: { name: string }[] | null; }; function LeaderboardStats({ members }: { members: Member[] }) { @@ -21,13 +21,13 @@ function LeaderboardStats({ members }: { members: Member[] }) { const osCount: Record = {}; members.forEach((m) => { - m.languages.forEach((l) => { + (m.languages || []).forEach((l) => { languageCount[l.name] = (languageCount[l.name] || 0) + 1; }); - m.editors.forEach((e) => { + (m.editors || []).forEach((e) => { editorCount[e.name] = (editorCount[e.name] || 0) + 1; }); - m.operating_systems.forEach((os) => { + (m.operating_systems || []).forEach((os) => { osCount[os.name] = (osCount[os.name] || 0) + 1; }); }); @@ -117,9 +117,9 @@ export default function LeaderboardTable({ email: member.email, hours: Math.round((member.total_seconds || 0) / 3600), role: member.role, - languages: member.languages.slice(0, 3).map((l) => l.name), - os: member.operating_systems[0]?.name || "N/A", - editor: member.editors[0]?.name || "N/A", + languages: (member.languages || []).slice(0, 3).map((l) => l.name), + os: member.operating_systems?.[0]?.name || "N/A", + editor: member.editors?.[0]?.name || "N/A", })); const maxHours = ranked[0]?.hours || 1; diff --git a/app/components/dashboard/LeaderbordList.tsx b/app/components/dashboard/LeaderbordList.tsx index 4e2f56b..4fd52a1 100644 --- a/app/components/dashboard/LeaderbordList.tsx +++ b/app/components/dashboard/LeaderbordList.tsx @@ -1,4 +1,4 @@ -import { createClient } from "../../lib/supabase/server"; +import { createClient } from "../../lib/supabase/server"; import BoardList from "../BoardList"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUsers, faCrown, faGhost } from "@fortawesome/free-solid-svg-icons"; @@ -93,7 +93,7 @@ export default async function LeaderboardsList() { {joinedBoards.map((board) => (
- +
))}
diff --git a/app/components/leaderboard/BackButton.tsx b/app/components/leaderboard/BackButton.tsx index fcb9894..9029387 100644 --- a/app/components/leaderboard/BackButton.tsx +++ b/app/components/leaderboard/BackButton.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client"; import { useRouter } from "next/navigation"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -9,7 +9,7 @@ export default function BackButton() { return (