diff --git a/src/app/dashboard/archives/actions.ts b/src/app/dashboard/archives/actions.ts index dfddc34..40f30ae 100644 --- a/src/app/dashboard/archives/actions.ts +++ b/src/app/dashboard/archives/actions.ts @@ -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 { diff --git a/src/components/Dashboard/ArchivesList.tsx b/src/components/Dashboard/ArchivesList.tsx index 574ec60..199961d 100644 --- a/src/components/Dashboard/ArchivesList.tsx +++ b/src/components/Dashboard/ArchivesList.tsx @@ -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(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 )}
+