mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-03-21 17:54:09 -05:00
Improve ArchivesList to include archives with patches
This commit is contained in:
parent
549c59246b
commit
a4c9474f7f
|
|
@ -1,6 +1,6 @@
|
|||
"use server";
|
||||
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { createClient, createServiceClient } from "@/utils/supabase/server";
|
||||
|
||||
export async function getArchives(args: {
|
||||
page?: number;
|
||||
|
|
@ -8,6 +8,7 @@ export async function getArchives(args: {
|
|||
search?: string;
|
||||
sortBy?: "title" | "created_at" | "original_author";
|
||||
sortOrder?: "asc" | "desc";
|
||||
filter?: "all" | "downloadable" | "informational";
|
||||
}) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
|
@ -21,21 +22,35 @@ export async function getArchives(args: {
|
|||
return { ok: false, error: "Forbidden" } as const;
|
||||
}
|
||||
|
||||
// Use service role client to bypass RLS entirely and avoid recursion
|
||||
const serviceClient = await createServiceClient();
|
||||
|
||||
const page = args.page || 1;
|
||||
const limit = args.limit || 50;
|
||||
const offset = (page - 1) * limit;
|
||||
const search = args.search?.trim() || "";
|
||||
const sortBy = args.sortBy || "created_at";
|
||||
const sortOrder = args.sortOrder || "desc";
|
||||
const filter = args.filter || "all";
|
||||
|
||||
let query = supabase
|
||||
let query = serviceClient
|
||||
.from("hacks")
|
||||
.select("slug,title,original_author,base_rom,created_at,created_by,approved", { count: "exact" })
|
||||
.select("slug,title,original_author,base_rom,created_at,created_by,approved,permission_from,current_patch", { count: "exact" })
|
||||
.not("original_author", "is", null)
|
||||
.is("current_patch", null)
|
||||
.or("current_patch.is.null,permission_from.not.is.null")
|
||||
.order(sortBy, { ascending: sortOrder === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
// Apply archive type filter
|
||||
if (filter === "downloadable") {
|
||||
// Downloadable: permission_from is not null AND current_patch is not null
|
||||
query = query.not("permission_from", "is", null).not("current_patch", "is", null);
|
||||
} else if (filter === "informational") {
|
||||
// Informational: current_patch is null
|
||||
query = query.is("current_patch", null);
|
||||
}
|
||||
// "all" doesn't need additional filtering
|
||||
|
||||
if (search) {
|
||||
query = query.or(`title.ilike.%${search}%,original_author.ilike.%${search}%,base_rom.ilike.%${search}%`);
|
||||
}
|
||||
|
|
@ -60,11 +75,13 @@ export async function getArchives(args: {
|
|||
slug: h.slug,
|
||||
title: h.title,
|
||||
original_author: h.original_author,
|
||||
permission_from: h.permission_from,
|
||||
base_rom: h.base_rom,
|
||||
created_at: h.created_at,
|
||||
created_by: h.created_by,
|
||||
creator_username: usernameById.get(h.created_by as string) || null,
|
||||
approved: h.approved,
|
||||
current_patch: h.current_patch,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { FiExternalLink, FiEdit2, FiTrash2, FiChevronLeft, FiChevronRight, FiArrowDown, FiSearch, FiLoader } from "react-icons/fi";
|
||||
import { FiExternalLink, FiEdit2, FiTrash2, FiChevronLeft, FiChevronRight, FiArrowDown, FiSearch, FiLoader, FiDownload, FiInfo, FiBarChart2 } from "react-icons/fi";
|
||||
import { getArchives, deleteArchive } from "@/app/dashboard/archives/actions";
|
||||
import { baseRoms } from "@/data/baseRoms";
|
||||
|
||||
|
|
@ -10,11 +10,13 @@ type Archive = {
|
|||
slug: string;
|
||||
title: string;
|
||||
original_author: string | null;
|
||||
permission_from: string | null;
|
||||
base_rom: string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
creator_username: string | null;
|
||||
approved: boolean;
|
||||
current_patch: number | null;
|
||||
};
|
||||
|
||||
type ArchivesData =
|
||||
|
|
@ -29,6 +31,7 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
const [debouncedSearch, setDebouncedSearch] = React.useState("");
|
||||
const [sortBy, setSortBy] = React.useState<"title" | "created_at" | "original_author">("created_at");
|
||||
const [sortOrder, setSortOrder] = React.useState<"asc" | "desc">("desc");
|
||||
const [filter, setFilter] = React.useState<"all" | "downloadable" | "informational">("all");
|
||||
const [deletingSlug, setDeletingSlug] = React.useState<string | null>(null);
|
||||
|
||||
// Debounce search input
|
||||
|
|
@ -41,17 +44,22 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
return () => clearTimeout(timer);
|
||||
}, [search]);
|
||||
|
||||
// Reset to first page when filter changes
|
||||
React.useEffect(() => {
|
||||
setPage(1);
|
||||
}, [filter]);
|
||||
|
||||
const loadArchives = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await getArchives({ page, limit: 50, search: debouncedSearch, sortBy, sortOrder });
|
||||
const result = await getArchives({ page, limit: 50, search: debouncedSearch, sortBy, sortOrder, filter });
|
||||
setData(result);
|
||||
} catch (err: any) {
|
||||
setData({ ok: false, error: err?.message || "Failed to load archives" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, debouncedSearch, sortBy, sortOrder]);
|
||||
}, [page, debouncedSearch, sortBy, sortOrder, filter]);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadArchives();
|
||||
|
|
@ -110,6 +118,15 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value as "all" | "downloadable" | "informational")}
|
||||
className="w-full md:w-auto rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
||||
>
|
||||
<option value="all">All Archives</option>
|
||||
<option value="downloadable">Downloadable</option>
|
||||
<option value="informational">Informational</option>
|
||||
</select>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
|
|
@ -144,10 +161,12 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
<>
|
||||
{/* Desktop header */}
|
||||
<div className="hidden lg:grid grid-cols-12 gap-4 bg-[var(--surface-2)] px-4 py-2 text-xs text-foreground/60">
|
||||
<div className="col-span-4">Title</div>
|
||||
<div className="col-span-1 text-center">Type</div>
|
||||
<div className="col-span-3">Title</div>
|
||||
<div className="col-span-2">Original Author</div>
|
||||
<div className="col-span-2">Base ROM</div>
|
||||
<div className="col-span-2">Archived by</div>
|
||||
<div className="col-span-2">Permission From</div>
|
||||
<div className="col-span-1">Base ROM</div>
|
||||
<div className="col-span-1 text-xs">Archived by</div>
|
||||
<div className="col-span-2 text-right">Actions</div>
|
||||
</div>
|
||||
<div className="divide-y divide-[var(--border)]">
|
||||
|
|
@ -155,12 +174,28 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
const baseRom = baseRoms.find((r) => r.id === archive.base_rom);
|
||||
const createdDate = new Date(archive.created_at).toLocaleDateString();
|
||||
const creator = archive.creator_username ? `@${archive.creator_username}` : "Unknown";
|
||||
const isDownloadable = archive.permission_from != null && archive.current_patch != null;
|
||||
const isInformational = archive.current_patch == null;
|
||||
|
||||
return (
|
||||
<div key={archive.slug} className="px-4 py-3 text-sm">
|
||||
{/* Desktop row */}
|
||||
<div className="hidden lg:grid grid-cols-12 items-center gap-4">
|
||||
<Link href={`/hack/${archive.slug}`} target="_blank" className="group flex items-center gap-3 col-span-4 min-w-0 hover:text-foreground">
|
||||
<div className="col-span-1 flex items-center justify-center">
|
||||
{isDownloadable && (
|
||||
<FiDownload
|
||||
className="h-4 w-4 text-blue-600 dark:text-blue-400"
|
||||
title="Downloadable Archive"
|
||||
/>
|
||||
)}
|
||||
{isInformational && (
|
||||
<FiInfo
|
||||
className="h-4 w-4 text-gray-600 dark:text-gray-400"
|
||||
title="Informational Archive"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Link href={`/hack/${archive.slug}`} target="_blank" className="group flex items-center gap-3 col-span-3 min-w-0 hover:text-foreground">
|
||||
<div className="flex flex-col items-start min-w-0">
|
||||
<div className="truncate font-medium group-hover:underline">{archive.title}</div>
|
||||
<div className="mt-0.5 text-xs text-foreground/60 group-hover:text-foreground group-hover:underline">/{archive.slug}</div>
|
||||
|
|
@ -168,12 +203,22 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
<FiExternalLink className="h-4 w-4 text-foreground/80 group-hover:text-foreground flex-shrink-0" />
|
||||
</Link>
|
||||
<div className="col-span-2 text-foreground/80">{archive.original_author || "—"}</div>
|
||||
<div className="col-span-2 text-foreground/80">{baseRom?.name || archive.base_rom}</div>
|
||||
<div className="col-span-2 text-foreground/80">
|
||||
<div>{creator}</div>
|
||||
<div className="text-xs text-foreground/60">{createdDate}</div>
|
||||
<div className="col-span-2 text-foreground/80">{archive.permission_from || "—"}</div>
|
||||
<div className="col-span-1 text-foreground/80">{baseRom?.name || archive.base_rom}</div>
|
||||
<div className="col-span-1 text-foreground/80 text-xs">
|
||||
<div className="truncate">{creator}</div>
|
||||
<div className="text-[10px] text-foreground/60">{createdDate}</div>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center justify-end gap-2">
|
||||
{isDownloadable && (
|
||||
<Link
|
||||
href={`/hack/${archive.slug}/stats`}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-black/5 dark:hover:bg-white/10"
|
||||
title="View Stats"
|
||||
>
|
||||
<FiBarChart2 className="h-4 w-4" />
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href={`/hack/${archive.slug}/edit`}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-black/5 dark:hover:bg-white/10"
|
||||
|
|
@ -198,21 +243,46 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
|
|||
{/* Mobile card */}
|
||||
<div className="lg:hidden flex flex-col gap-2">
|
||||
<div className="group flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{isDownloadable && (
|
||||
<FiDownload
|
||||
className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
||||
title="Downloadable Archive"
|
||||
/>
|
||||
)}
|
||||
{isInformational && (
|
||||
<FiInfo
|
||||
className="h-4 w-4 text-gray-600 dark:text-gray-400 flex-shrink-0"
|
||||
title="Informational Archive"
|
||||
/>
|
||||
)}
|
||||
<Link href={`/hack/${archive.slug}`} target="_blank">
|
||||
<div className="text-lg font-bold group-hover:underline">{archive.title}</div>
|
||||
<div className="text-xs text-foreground/60 group-hover:underline">/{archive.slug}</div>
|
||||
</Link>
|
||||
</div>
|
||||
<FiExternalLink className="h-4 w-4 text-foreground/80 group-hover:text-foreground flex-shrink-0" />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-foreground/60">
|
||||
<span className="font-bold">Author: {archive.original_author || "—"}</span>
|
||||
<span>|</span>
|
||||
{archive.permission_from && <span className="font-bold">Permission: {archive.permission_from || "—"}</span>}
|
||||
{archive.permission_from && <span>|</span>}
|
||||
<span className="font-bold">Base: {baseRom?.name || archive.base_rom}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center text-xs italic text-foreground/60">
|
||||
Archived by {creator} on {createdDate}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{isDownloadable && (
|
||||
<Link
|
||||
href={`/hack/${archive.slug}/stats`}
|
||||
className="inline-flex items-center gap-1 rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1 text-xs hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<FiBarChart2 className="h-3 w-3" />
|
||||
Stats
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href={`/hack/${archive.slug}/edit`}
|
||||
className="inline-flex items-center gap-1 rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1 text-xs hover:bg-black/5 dark:hover:bg-white/10"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user