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 (