Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion app/components/BoardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ 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";

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);
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -151,6 +176,19 @@ export default function BoardList({
</button>
</div>
)}

{allowLeave && user.id !== board.owner_id && (
<div className="flex items-center gap-1 sm:gap-2 ml-2 pl-4 border-l border-white/10 shrink-0">
<button
type="button"
onClick={() => setShowLeaveModal(true)}
className="w-8 h-8 flex items-center justify-center rounded-md text-gray-500 hover:text-amber-400 hover:bg-amber-500/10 transition-colors"
title="Leave leaderboard"
>
<FontAwesomeIcon icon={faRightFromBracket} className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>

{showCodeModal && (
Expand Down Expand Up @@ -206,6 +244,39 @@ export default function BoardList({
</div>
)}

{showLeaveModal && typeof document !== "undefined" && createPortal(
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-md p-4">
<div className="glass-card p-8 w-full max-w-sm relative shadow-2xl border-amber-500/20">
<div className="absolute -top-10 -right-10 w-32 h-32 bg-amber-500/20 rounded-full blur-3xl" />
<h3 className="text-lg font-bold text-gray-200 mb-2">Leave leaderboard?</h3>
<p className="text-sm text-gray-400 mb-6 leading-relaxed">
You&apos;ll be removed from{" "}
<span className="font-mono text-gray-300 bg-white/5 px-1 rounded">{board.name}</span>.
You can rejoin later with an invite link or code.
</p>
<div className="flex gap-3">
<button
type="button"
onClick={() => setShowLeaveModal(false)}
disabled={leaving}
className="flex-1 py-2.5 px-4 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-sm font-medium text-gray-300 transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
onClick={handleLeave}
disabled={leaving}
className="flex-1 py-2.5 px-4 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 text-amber-400 rounded-lg text-sm font-bold transition-colors disabled:opacity-50"
>
{leaving ? "Leaving…" : "Leave"}
</button>
</div>
</div>
</div>,
document.body
)}

{showDeleteModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md p-4">
<div className="glass-card p-8 w-full max-w-sm relative shadow-2xl border-red-500/20">
Expand Down
18 changes: 9 additions & 9 deletions app/components/LeaderboardTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] }) {
Expand All @@ -21,13 +21,13 @@ function LeaderboardStats({ members }: { members: Member[] }) {
const osCount: Record<string, number> = {};

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;
});
});
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions app/components/dashboard/LeaderbordList.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -93,7 +93,7 @@ export default async function LeaderboardsList() {
{joinedBoards.map((board) => (
<div key={board.id} className="group relative rounded-xl border border-white/5 bg-white/[0.01] hover:bg-white/[0.03] transition-all duration-300 overflow-hidden shadow-md">
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-gradient-to-b from-blue-500 to-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
<BoardList user={user} board={board} />
<BoardList user={user} board={board} allowLeave />
</div>
))}
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/components/leaderboard/BackButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client";
"use client";

import { useRouter } from "next/navigation";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
Expand All @@ -9,7 +9,7 @@ export default function BackButton() {

return (
<button
onClick={() => router.back()}
onClick={() => router.push("/dashboard")}
className="flex items-center gap-2 text-sm font-medium text-gray-400 hover:text-indigo-400 transition-colors mb-6 group w-fit"
>
<div className="w-8 h-8 rounded-full bg-white/5 border border-white/10 flex items-center justify-center group-hover:bg-indigo-500/10 group-hover:border-indigo-500/30 transition-all">
Expand Down
Loading