mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-09-07 16:36:26 -05:00
Require completion status for hacks
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
MdChevronLeft,
|
||||
MdChevronRight,
|
||||
} from "react-icons/md";
|
||||
import { TbProgressCheck } from "react-icons/tb";
|
||||
import { IoEllipsisHorizontal } from "react-icons/io5";
|
||||
import { BsSdCardFill } from "react-icons/bs";
|
||||
import { CATEGORY_ICONS } from "@/components/Icons/tagCategories";
|
||||
@@ -56,6 +57,7 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [selectedTags, setSelectedTags] = React.useState<string[]>([]);
|
||||
const [selectedBaseRoms, setSelectedBaseRoms] = React.useState<string[]>([]);
|
||||
const [selectedCompletionStatuses, setSelectedCompletionStatuses] = React.useState<string[]>([]);
|
||||
const [sort, setSort] = React.useState<DiscoverSortOption>(initialSort ?? "trending");
|
||||
const [hacks, setHacks] = React.useState<HackCardAttributes[]>([]);
|
||||
const [tagGroups, setTagGroups] = React.useState<Record<string, string[]>>({});
|
||||
@@ -83,7 +85,7 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
React.useEffect(() => {
|
||||
// Reset to first page when filters or sort change
|
||||
setCurrentPage(1);
|
||||
}, [query, selectedTags, selectedBaseRoms, onlyReady, sort]);
|
||||
}, [query, selectedTags, selectedBaseRoms, selectedCompletionStatuses, onlyReady, sort]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const run = async () => {
|
||||
@@ -125,12 +127,23 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
if (selectedBaseRoms.length > 0) {
|
||||
out = out.filter((h) => h.baseRomId && selectedBaseRoms.includes(h.baseRomId));
|
||||
}
|
||||
// OR filter across completion statuses: hack's completion_status must be in selectedCompletionStatuses
|
||||
// If "Complete" is selected, also include hacks with null completion_status
|
||||
if (selectedCompletionStatuses.length > 0) {
|
||||
out = out.filter((h) => {
|
||||
if (!h.completion_status) {
|
||||
// Include null completion_status if "Complete" is selected
|
||||
return selectedCompletionStatuses.includes("Complete");
|
||||
}
|
||||
return selectedCompletionStatuses.includes(h.completion_status);
|
||||
});
|
||||
}
|
||||
// Filter to hacks whose base ROM is ready (linked with permission or cached)
|
||||
if (onlyReady) {
|
||||
out = out.filter((h) => !h.is_archive && h.baseRomId && readyBaseRomIds.has(h.baseRomId));
|
||||
}
|
||||
return out;
|
||||
}, [hacks, query, selectedTags, selectedBaseRoms, onlyReady, readyBaseRomIds]);
|
||||
}, [hacks, query, selectedTags, selectedBaseRoms, selectedCompletionStatuses, onlyReady, readyBaseRomIds]);
|
||||
|
||||
const totalPages = React.useMemo(
|
||||
() => Math.max(1, Math.ceil(filtered.length / HACKS_PER_PAGE)),
|
||||
@@ -308,6 +321,13 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
if (vals.length > 0) setOnlyReady(false);
|
||||
}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
icon={TbProgressCheck}
|
||||
label="Completion"
|
||||
options={['Complete','Demo','Alpha','Beta'].map((s) => ({ id: s, name: s }))}
|
||||
values={selectedCompletionStatuses}
|
||||
onChange={setSelectedCompletionStatuses}
|
||||
/>
|
||||
{loadingTags ? (
|
||||
<>
|
||||
{[
|
||||
@@ -353,11 +373,12 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(selectedTags.length > 0 || selectedBaseRoms.length > 0 || onlyReady) && (
|
||||
{(selectedTags.length > 0 || selectedBaseRoms.length > 0 || selectedCompletionStatuses.length > 0 || onlyReady) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
clearTags();
|
||||
clearBaseRoms();
|
||||
setSelectedCompletionStatuses([]);
|
||||
setOnlyReady(false);
|
||||
}}
|
||||
className="ml-2 rounded-full px-3 py-1 text-sm ring-1 ring-inset transition-colors bg-[var(--surface-2)] text-foreground/80 ring-[var(--border)] hover:bg-black/5 dark:hover:bg-white/10"
|
||||
@@ -384,7 +405,7 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
) : (
|
||||
<>No results</>
|
||||
)}
|
||||
{(selectedTags.length > 0 || selectedBaseRoms.length > 0) && (
|
||||
{(selectedTags.length > 0 || selectedBaseRoms.length > 0 || selectedCompletionStatuses.length > 0) && (
|
||||
<>
|
||||
{" "}with the selected filters
|
||||
</>
|
||||
@@ -399,11 +420,12 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr
|
||||
Clear search
|
||||
</button>
|
||||
)}
|
||||
{(selectedTags.length > 0 || selectedBaseRoms.length > 0 || onlyReady) && (
|
||||
{(selectedTags.length > 0 || selectedBaseRoms.length > 0 || selectedCompletionStatuses.length > 0 || onlyReady) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
clearTags();
|
||||
clearBaseRoms();
|
||||
setSelectedCompletionStatuses([]);
|
||||
setOnlyReady(false);
|
||||
}}
|
||||
className="rounded-full px-3 py-1 text-sm ring-1 ring-inset transition-colors bg-[var(--surface-2)] text-foreground/80 ring-[var(--border)] hover:bg-black/5 dark:hover:bg-white/10"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createClient } from "@/utils/supabase/client";
|
||||
import { updateHack, saveHackCovers, presignCoverUpload } from "@/app/hack/actions";
|
||||
import SortableCovers from "@/components/Hack/SortableCovers";
|
||||
import Select from "@/components/Primitives/Select";
|
||||
import type { Database } from "@/types/db";
|
||||
|
||||
interface HackEditFormProps {
|
||||
slug: string;
|
||||
@@ -20,6 +21,7 @@ interface HackEditFormProps {
|
||||
description: string;
|
||||
base_rom: string;
|
||||
language: string;
|
||||
completion_status: Database["public"]["Enums"]["Completion Status"] | null;
|
||||
version: string;
|
||||
box_art: string | null;
|
||||
social_links: {
|
||||
@@ -43,6 +45,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
const [showMdPreview, setShowMdPreview] = React.useState(false);
|
||||
const [baseRom, setBaseRom] = React.useState(initial.base_rom);
|
||||
const [language, setLanguage] = React.useState(initial.language);
|
||||
const [completionStatus, setCompletionStatus] = React.useState<Database["public"]["Enums"]["Completion Status"] | null>(initial.completion_status);
|
||||
const [version, setVersion] = React.useState(initial.version);
|
||||
const [boxArt, setBoxArt] = React.useState(initial.box_art || "");
|
||||
const [discord, setDiscord] = React.useState(initial.social_links?.discord || "");
|
||||
@@ -57,6 +60,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
summary: initial.summary,
|
||||
description: initial.description,
|
||||
language: initial.language,
|
||||
completionStatus: initial.completion_status,
|
||||
boxArt: initial.box_art || "",
|
||||
tags: initial.tags || [],
|
||||
discord: initial.social_links?.discord || "",
|
||||
@@ -116,12 +120,13 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
const summaryChanged = summary !== baseline.summary;
|
||||
const descriptionChanged = description !== baseline.description;
|
||||
const languageChanged = language !== baseline.language;
|
||||
const completionStatusChanged = completionStatus !== baseline.completionStatus;
|
||||
const boxArtChanged = boxArt !== baseline.boxArt;
|
||||
const discordChanged = discord !== baseline.discord;
|
||||
const twitterChanged = twitter !== baseline.twitter;
|
||||
const pokeChanged = pokecommunity !== baseline.pokecommunity;
|
||||
const githubChanged = github !== baseline.github;
|
||||
const contentChanged = titleChanged || summaryChanged || descriptionChanged || languageChanged || boxArtChanged || tagsChanged || discordChanged || twitterChanged || pokeChanged || githubChanged;
|
||||
const contentChanged = titleChanged || summaryChanged || descriptionChanged || languageChanged || completionStatusChanged || boxArtChanged || tagsChanged || discordChanged || twitterChanged || pokeChanged || githubChanged;
|
||||
|
||||
const newItemsCount = coverItems.filter((i) => i.type === "new").length;
|
||||
const currentExistingKeys = coverItems.filter((i): i is { type: "existing"; key: string; url: string } => i.type === "existing").map((i) => i.key);
|
||||
@@ -169,6 +174,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
if (summaryChanged) updateArgs.summary = summary.trim();
|
||||
if (descriptionChanged) updateArgs.description = description.trim();
|
||||
if (languageChanged) updateArgs.language = language;
|
||||
if (completionStatusChanged) updateArgs.completion_status = completionStatus;
|
||||
if (boxArtChanged) updateArgs.box_art = boxArt ? boxArt.trim() : null;
|
||||
if (tagsChanged) updateArgs.tags = tags.slice();
|
||||
if (discordChanged || twitterChanged || pokeChanged || githubChanged) updateArgs.social_links = social; // may be null to clear
|
||||
@@ -181,6 +187,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
summary,
|
||||
description,
|
||||
language,
|
||||
completionStatus,
|
||||
boxArt,
|
||||
tags: tags.slice(),
|
||||
discord,
|
||||
@@ -229,7 +236,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
|
||||
const summaryLimit = 120;
|
||||
const summaryTooLong = summary.length > summaryLimit;
|
||||
const contentHasErrors = summaryTooLong || (!!boxArt && !urlLike(boxArt));
|
||||
const contentHasErrors = summaryTooLong || (!!boxArt && !urlLike(boxArt)) || !completionStatus;
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-6 lg:grid lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
@@ -246,6 +253,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
setSummary(baseline.summary);
|
||||
setDescription(baseline.description);
|
||||
setLanguage(baseline.language);
|
||||
setCompletionStatus(baseline.completionStatus);
|
||||
setBoxArt(baseline.boxArt);
|
||||
setTags(baseline.tags.slice());
|
||||
setDiscord(baseline.discord);
|
||||
@@ -263,6 +271,21 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!completionStatus && (
|
||||
<div className="mt-4 rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-sm text-amber-900 dark:text-amber-100">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex items-center justify-center w-5 h-5 shrink-0 mt-0.5">
|
||||
<div className="inline-block h-2 w-2 rounded-full bg-amber-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-semibold">Missing Completion Status</p>
|
||||
<p className="text-xs text-amber-800 dark:text-amber-200">
|
||||
This is a new required field. Please select a completion status in the Details section to save your changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -424,6 +447,35 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className={`text-sm ${!completionStatus ? 'text-red-500 font-semibold' : 'text-foreground/80'}`}>
|
||||
Completion Status <span className="text-red-500">*</span>
|
||||
{!completionStatus && <span className="ml-2 text-xs font-normal text-red-500/80">(Required)</span>}
|
||||
</label>
|
||||
{completionStatusChanged && (
|
||||
<div className="ml-auto flex items-center gap-2 text-[11px] text-foreground/70">
|
||||
<span>Modified</span>
|
||||
<button type="button" onClick={() => setCompletionStatus(baseline.completionStatus)} className="inline-flex items-center underline underline-offset-2 text-[11px] cursor-pointer">Revert</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
value={completionStatus || ""}
|
||||
onChange={(value) => setCompletionStatus(value as Database["public"]["Enums"]["Completion Status"] | null)}
|
||||
placeholder="Select completion status"
|
||||
className={!completionStatus ? 'ring-2 ring-red-500/60 bg-red-500/10 dark:ring-red-400/60 dark:bg-red-950/20' : completionStatusChanged ? 'ring-[var(--ring)]' : ''}
|
||||
options={['Complete','Demo','Alpha','Beta'].map(s => ({
|
||||
value: s,
|
||||
label: s,
|
||||
}))}
|
||||
/>
|
||||
{!completionStatus && (
|
||||
<p className="text-xs text-red-500/80 dark:text-red-400/80">
|
||||
This is a new required field. Please select a completion status to save your changes.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Current version</label>
|
||||
<p className="flex items-center h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)] text-foreground/60 select-none cursor-not-allowed">
|
||||
|
||||
@@ -102,6 +102,7 @@ export default function HackSubmitForm({
|
||||
const [platform, setPlatform] = React.useState<"GB" | "GBC" | "GBA" | "NDS" | "">(() => (initialDraftRef.current?.platform as any) || "");
|
||||
const [version, setVersion] = React.useState(() => initialDraftRef.current?.version || "");
|
||||
const [language, setLanguage] = React.useState(() => initialDraftRef.current?.language || "");
|
||||
const [completionStatus, setCompletionStatus] = React.useState<"Complete" | "Demo" | "Alpha" | "Beta" | "">(() => (initialDraftRef.current?.completionStatus as any) || "");
|
||||
const [boxArt, setBoxArt] = React.useState(() => initialDraftRef.current?.boxArt || "");
|
||||
const [discord, setDiscord] = React.useState(() => initialDraftRef.current?.discord || "");
|
||||
const [twitter, setTwitter] = React.useState(() => initialDraftRef.current?.twitter || "");
|
||||
@@ -285,7 +286,7 @@ export default function HackSubmitForm({
|
||||
const data = JSON.parse(raw);
|
||||
if (data && typeof data === "object") {
|
||||
const isEmpty =
|
||||
!title && !summary && !description && !baseRom && !platform && !version && !language && !boxArt && !discord && !twitter && !pokecommunity && !github && (!tags || tags.length === 0) && !originalAuthor;
|
||||
!title && !summary && !description && !baseRom && !platform && !version && !language && !completionStatus && !boxArt && !discord && !twitter && !pokecommunity && !github && (!tags || tags.length === 0) && !originalAuthor;
|
||||
if (isEmpty) {
|
||||
let applied = false;
|
||||
if (typeof data.title === "string") setTitle(data.title);
|
||||
@@ -302,6 +303,8 @@ export default function HackSubmitForm({
|
||||
if (typeof data.version === "string") applied = applied || !!data.version;
|
||||
if (typeof data.language === "string") setLanguage(data.language);
|
||||
if (typeof data.language === "string") applied = applied || !!data.language;
|
||||
if (["Complete","Demo","Alpha","Beta",""].includes(data.completionStatus)) setCompletionStatus(data.completionStatus);
|
||||
if (["Complete","Demo","Alpha","Beta",""].includes(data.completionStatus)) applied = applied || !!data.completionStatus;
|
||||
if (typeof data.boxArt === "string") setBoxArt(data.boxArt);
|
||||
if (typeof data.boxArt === "string") applied = applied || !!data.boxArt;
|
||||
if (typeof data.discord === "string") setDiscord(data.discord);
|
||||
@@ -342,7 +345,7 @@ export default function HackSubmitForm({
|
||||
if (!d || typeof d !== "object") return;
|
||||
// Don't count originalAuthor if customCreator is provided
|
||||
const hasAny = Boolean(
|
||||
d.title || d.summary || d.description || d.baseRom || d.platform || d.version || d.language || d.boxArt || d.discord || d.twitter || d.pokecommunity || d.github || (Array.isArray(d.tags) && d.tags.length > 0) || (!customCreator && d.originalAuthor)
|
||||
d.title || d.summary || d.description || d.baseRom || d.platform || d.version || d.language || d.completionStatus || d.boxArt || d.discord || d.twitter || d.pokecommunity || d.github || (Array.isArray(d.tags) && d.tags.length > 0) || (!customCreator && d.originalAuthor)
|
||||
);
|
||||
if (hasAny) { hydratedFromDraftRef.current = true; setRestoredDraft(true); }
|
||||
}, [dummy, draftKey, customCreator]);
|
||||
@@ -359,6 +362,7 @@ export default function HackSubmitForm({
|
||||
platform,
|
||||
version,
|
||||
language,
|
||||
completionStatus,
|
||||
boxArt,
|
||||
discord,
|
||||
twitter,
|
||||
@@ -391,6 +395,7 @@ export default function HackSubmitForm({
|
||||
platform,
|
||||
version,
|
||||
language,
|
||||
completionStatus,
|
||||
boxArt,
|
||||
discord,
|
||||
twitter,
|
||||
@@ -413,7 +418,7 @@ export default function HackSubmitForm({
|
||||
|
||||
const allSocialValid = [discord, twitter, pokecommunity, github].every((s) => !s || urlLike(s));
|
||||
|
||||
const step1Valid = !!title.trim() && !!platform && !!baseRom.trim() && !!language.trim() && (isArchive ? !!originalAuthor.trim() : true);
|
||||
const step1Valid = !!title.trim() && !!platform && !!baseRom.trim() && !!language.trim() && !!completionStatus.trim() && (isArchive ? !!originalAuthor.trim() : true);
|
||||
const step2Valid = (isArchive ? true : !!version.trim()) && !!summary.trim() && !summaryTooLong && !!description.trim() && tags.length > 0;
|
||||
const step3Valid = (newCoverFiles.length > 0) && !overLimit && coverErrors.length === 0 && (!boxArt.trim() || urlLike(boxArt)) && allSocialValid;
|
||||
const isValid = step1Valid && step2Valid && step3Valid && (isArchive ? true : !!patchFile);
|
||||
@@ -428,6 +433,7 @@ export default function HackSubmitForm({
|
||||
fd.set('description', description);
|
||||
fd.set('base_rom', baseRom);
|
||||
fd.set('language', language);
|
||||
fd.set('completion_status', completionStatus);
|
||||
fd.set('version', version);
|
||||
if (boxArt) fd.set('box_art', boxArt);
|
||||
if (discord) fd.set('discord', discord);
|
||||
@@ -692,6 +698,7 @@ export default function HackSubmitForm({
|
||||
setPlatform("");
|
||||
setVersion("");
|
||||
setLanguage("");
|
||||
setCompletionStatus("");
|
||||
setBoxArt("");
|
||||
setDiscord("");
|
||||
setTwitter("");
|
||||
@@ -797,6 +804,23 @@ export default function HackSubmitForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Completion Status <span className="text-red-500">*</span></label>
|
||||
{!isDummy ? (
|
||||
<Select
|
||||
value={completionStatus}
|
||||
onChange={(value) => setCompletionStatus(value as any)}
|
||||
placeholder="Select completion status"
|
||||
options={['Complete','Demo','Alpha','Beta'].map(s => ({
|
||||
value: s,
|
||||
label: s,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<div role="textbox" aria-disabled className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">{completionStatus || "Select completion status"}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isArchive && (
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Original Author <span className="text-red-500">*</span></label>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { usePathname } from "next/navigation";
|
||||
import { FaRegImages } from "react-icons/fa6";
|
||||
import { ImDownload } from "react-icons/im";
|
||||
import { RiArchiveStackFill } from "react-icons/ri";
|
||||
import type { Database } from "@/types/db";
|
||||
|
||||
export interface HackCardAttributes {
|
||||
slug: string;
|
||||
@@ -25,6 +26,7 @@ export interface HackCardAttributes {
|
||||
summary?: string;
|
||||
description?: string;
|
||||
is_archive: boolean;
|
||||
completion_status?: Database["public"]["Enums"]["Completion Status"] | null;
|
||||
};
|
||||
|
||||
export default function HackCard({ hack, clickable = true, className = "" }: { hack: HackCardAttributes; clickable?: boolean; className?: string }) {
|
||||
@@ -63,7 +65,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
|
||||
setIsClicked(false);
|
||||
}, []);
|
||||
|
||||
const cardClass = `rounded-[12px] overflow-hidden h-full ${
|
||||
const cardClass = `rounded-[12px] overflow-hidden h-full flex flex-col ${
|
||||
clickable ? `transition-transform duration-300 hover:-translate-y-0.5 hover:shadow-xl ${isClicked ? "anim-float" : ""}` : ""
|
||||
} ring-1 ${ready ? "ring-emerald-400/50 bg-emerald-500/10" : "card ring-[var(--border)]"}`;
|
||||
const gradientBgClass = `bg-gradient-to-b ${ready ? 'from-emerald-300/5 to-emerald-400/30 dark:from-emerald-950/10 dark:to-emerald-600/40' : 'from-black/30 to-black/10 dark:from-black/80 dark:to-black/40'}`;
|
||||
@@ -181,7 +183,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="p-4 flex flex-col flex-1 min-h-0">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 w-full">
|
||||
<div className={`flex items-center gap-2 ${isArchive ? "justify-between" : "justify-start"}`}>
|
||||
@@ -207,7 +209,10 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
|
||||
return text.length > 120 ? text.slice(0, 120).trimEnd() + "…" : text;
|
||||
})()}
|
||||
</p>
|
||||
<div className="mt-3 text-xs text-foreground/60">Base: {baseName ?? "Unknown"}</div>
|
||||
<div className="flex justify-between items-end mt-auto pt-3 text-xs text-foreground/60">
|
||||
<p>Base: {baseName ?? "Unknown"}</p>
|
||||
{hack.completion_status && hack.completion_status !== "Complete" && <p className="font-bold text-sm">{hack.completion_status}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user