Add proper filtering and display of cases

This commit is contained in:
Nicolas Mowen 2025-12-15 10:47:04 -07:00
parent 46e488653b
commit 7b4f747b6a

View File

@ -15,12 +15,19 @@ import Heading from "@/components/ui/heading";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DeleteClipType, Export, ExportCase } from "@/types/export"; import { DeleteClipType, Export, ExportCase } from "@/types/export";
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import {
MutableRefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -55,35 +62,12 @@ function Exports() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const filteredCases = useMemo(() => {
if (!search || !cases) {
return cases;
}
return cases.filter(
(caseItem) =>
caseItem.name.toLowerCase().includes(search.toLowerCase()) ||
(caseItem.description &&
caseItem.description.toLowerCase().includes(search.toLowerCase())),
);
}, [search, cases]);
const filteredExports = useMemo<Export[]>(() => {
if (!search) {
return exports;
}
return exports.filter((exp) =>
exp.name
.toLowerCase()
.replaceAll("_", " ")
.includes(search.toLowerCase()),
);
}, [exports, search]);
// Viewing // Viewing
const [selected, setSelected] = useState<Export>(); const [selected, setSelected] = useState<Export>();
const [selectedCaseId, setSelectedCaseId] = useOverlayState<
string | undefined
>("caseId", undefined);
const [selectedAspect, setSelectedAspect] = useState(0.0); const [selectedAspect, setSelectedAspect] = useState(0.0);
useSearchEffect("id", (id) => { useSearchEffect("id", (id) => {
@ -95,7 +79,22 @@ function Exports() {
return true; return true;
}); });
// Deleting useSearchEffect("caseId", (caseId: string) => {
if (!cases) {
return false;
}
const exists = cases.some((c) => c.id === caseId);
if (!exists) {
return false;
}
setSelectedCaseId(caseId);
return true;
});
// Modifying
const [deleteClip, setDeleteClip] = useState<DeleteClipType | undefined>(); const [deleteClip, setDeleteClip] = useState<DeleteClipType | undefined>();
@ -112,8 +111,6 @@ function Exports() {
}); });
}, [deleteClip, mutate]); }, [deleteClip, mutate]);
// Renaming
const onHandleRename = useCallback( const onHandleRename = useCallback(
(id: string, update: string) => { (id: string, update: string) => {
axios axios
@ -136,7 +133,7 @@ function Exports() {
}); });
}); });
}, },
[mutate, t], [mutate, setDeleteClip, t],
); );
// Keyboard Listener // Keyboard Listener
@ -144,6 +141,11 @@ function Exports() {
const contentRef = useRef<HTMLDivElement | null>(null); const contentRef = useRef<HTMLDivElement | null>(null);
useKeyboardListener([], undefined, contentRef); useKeyboardListener([], undefined, contentRef);
const selectedCase = useMemo(
() => cases?.find((c) => c.id === selectedCaseId),
[cases, selectedCaseId],
);
return ( return (
<div className="flex size-full flex-col gap-2 overflow-hidden px-1 pt-2 md:p-2"> <div className="flex size-full flex-col gap-2 overflow-hidden px-1 pt-2 md:p-2">
<Toaster closeButton={true} /> <Toaster closeButton={true} />
@ -227,63 +229,207 @@ function Exports() {
</div> </div>
)} )}
<div className="w-full overflow-hidden"> {selectedCase ? (
{filteredCases?.length || filteredExports.length ? ( <CaseView
<div className="flex flex-col gap-4"> contentRef={contentRef}
{cases?.length && ( selectedCase={selectedCase}
<div className="space-y-2"> exports={rawExports}
<Heading as="h4">{t("headings.cases")}</Heading> search={search}
<div setSelected={setSelected}
ref={contentRef} renameClip={onHandleRename}
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" setDeleteClip={setDeleteClip}
> />
{cases.map((item) => ( ) : (
<CaseCard <AllExportsView
key={item.name} contentRef={contentRef}
className={ search={search}
search == "" || filteredCases?.includes(item) cases={cases}
? "" exports={exports}
: "hidden" setSelectedCaseId={setSelectedCaseId}
} setSelected={setSelected}
exportCase={item} renameClip={onHandleRename}
onSelect={() => {}} setDeleteClip={setDeleteClip}
/> />
))} )}
</div> </div>
</div> );
)} }
<div className="space-y-4"> type AllExportsViewProps = {
<Heading as="h4">{t("headings.uncategorizedExports")}</Heading> contentRef: MutableRefObject<HTMLDivElement | null>;
search: string;
cases?: ExportCase[];
exports: Export[];
setSelectedCaseId: (id: string) => void;
setSelected: (e: Export) => void;
renameClip: (id: string, update: string) => void;
setDeleteClip: (d: DeleteClipType | undefined) => void;
};
function AllExportsView({
contentRef,
search,
cases,
exports,
setSelectedCaseId,
setSelected,
renameClip,
setDeleteClip,
}: AllExportsViewProps) {
const { t } = useTranslation(["views/exports"]);
// Filter
const filteredCases = useMemo(() => {
if (!search || !cases) {
return cases;
}
return cases.filter(
(caseItem) =>
caseItem.name.toLowerCase().includes(search.toLowerCase()) ||
(caseItem.description &&
caseItem.description.toLowerCase().includes(search.toLowerCase())),
);
}, [search, cases]);
const filteredExports = useMemo<Export[]>(() => {
if (!search) {
return exports;
}
return exports.filter((exp) =>
exp.name
.toLowerCase()
.replaceAll("_", " ")
.includes(search.toLowerCase()),
);
}, [exports, search]);
return (
<div className="w-full overflow-hidden">
{filteredCases?.length || filteredExports.length ? (
<div className="flex flex-col gap-4">
{cases?.length && (
<div className="space-y-2">
<Heading as="h4">{t("headings.cases")}</Heading>
<div <div
ref={contentRef} ref={contentRef}
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
> >
{exports.map((item) => ( {cases.map((item) => (
<ExportCard <CaseCard
key={item.name} key={item.name}
className={ className={
search == "" || filteredExports.includes(item) search == "" || filteredCases?.includes(item)
? "" ? ""
: "hidden" : "hidden"
} }
exportedRecording={item} exportCase={item}
onSelect={setSelected} onSelect={() => {
onRename={onHandleRename} setSelectedCaseId(item.id);
onDelete={({ file, exportName }) => }}
setDeleteClip({ file, exportName })
}
/> />
))} ))}
</div> </div>
</div> </div>
)}
<div className="space-y-4">
<Heading as="h4">{t("headings.uncategorizedExports")}</Heading>
<div
ref={contentRef}
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
{exports.map((item) => (
<ExportCard
key={item.name}
className={
search == "" || filteredExports.includes(item)
? ""
: "hidden"
}
exportedRecording={item}
onSelect={setSelected}
onRename={renameClip}
onDelete={({ file, exportName }) =>
setDeleteClip({ file, exportName })
}
/>
))}
</div>
</div> </div>
) : ( </div>
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center"> ) : (
<LuFolderX className="size-16" /> <div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
{t("noExports")} <LuFolderX className="size-16" />
</div> {t("noExports")}
)} </div>
)}
</div>
);
}
type CaseViewProps = {
contentRef: MutableRefObject<HTMLDivElement | null>;
selectedCase: ExportCase;
exports?: Export[];
search: string;
setSelected: (e: Export) => void;
renameClip: (id: string, update: string) => void;
setDeleteClip: (d: DeleteClipType | undefined) => void;
};
function CaseView({
contentRef,
selectedCase,
exports,
search,
setSelected,
renameClip,
setDeleteClip,
}: CaseViewProps) {
const filteredExports = useMemo<Export[]>(() => {
const caseExports = (exports || []).filter(
(e) => e.export_case == selectedCase.id,
);
if (!search) {
return caseExports;
}
return caseExports.filter((exp) =>
exp.name
.toLowerCase()
.replaceAll("_", " ")
.includes(search.toLowerCase()),
);
}, [selectedCase, exports, search]);
return (
<div className="flex size-full flex-col gap-8">
<div className="flex flex-col gap-1">
<Heading className="capitalize" as="h2">
{selectedCase.name}
</Heading>
<div className="text-secondary-foreground">
{selectedCase.description}
</div>
</div>
<div
ref={contentRef}
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
{exports.map((item) => (
<ExportCard
key={item.name}
className={filteredExports.includes(item) ? "" : "hidden"}
exportedRecording={item}
onSelect={setSelected}
onRename={renameClip}
onDelete={({ file, exportName }) =>
setDeleteClip({ file, exportName })
}
/>
))}
</div> </div>
</div> </div>
); );