added fuzzy search for table

This commit is contained in:
Polaris
2024-08-21 13:34:23 -04:00
parent 7fe79979b5
commit 007b23783a
5 changed files with 226 additions and 96 deletions

View File

@ -1,21 +1,19 @@
"use server"; "use server";
import { artemis, daphnis } from "@/lib/prisma"; import { artemis, daphnis } from "@/lib/prisma";
import type * as Prisma from "@prisma/client"; import type * as Prisma from "@prisma/client";
type ChuniScorePlaylog = Prisma.PrismaClient; type ChuniScorePlaylog = Prisma.PrismaClient;
type ChuniStaticMusic = Prisma.PrismaClient; type ChuniStaticMusic = Prisma.PrismaClient;
type LinkSharingToken = { type LinkSharingToken = {
playlogId: number; playlogId: number;
}; };
export async function getSongsWithTitles(userId: number) { export async function getSongsWithTitles(userId: number) {
try { try {
const songs: ChuniScorePlaylog[] = await artemis.chuni_score_playlog.findMany({ const songs: ChuniScorePlaylog[] =
await artemis.chuni_score_playlog.findMany({
where: { where: {
user: userId, user: userId,
}, },
@ -64,11 +62,12 @@ export async function getSongsWithTitles(userId: number) {
}, },
}); });
const chuniScorePlaylogMusicId = songs const chuniScorePlaylogMusicId = songs
.map((song) => song.musicId) .map((song) => song.musicId)
.filter((id): id is number => id !== null); .filter((id): id is number => id !== null);
const staticMusicInfo: ChuniStaticMusic[] = await artemis.chuni_static_music.findMany({ const staticMusicInfo: ChuniStaticMusic[] =
await artemis.chuni_static_music.findMany({
where: { where: {
songId: { songId: {
in: chuniScorePlaylogMusicId, in: chuniScorePlaylogMusicId,
@ -86,70 +85,72 @@ export async function getSongsWithTitles(userId: number) {
}, },
}); });
const playCounts = await artemis.chuni_score_playlog.groupBy({ const playCounts = await artemis.chuni_score_playlog.groupBy({
by: ['musicId'], by: ["musicId"],
_count: { _count: {
musicId: true, musicId: true,
},
where: {
user: userId,
musicId: {
in: chuniScorePlaylogMusicId,
}, },
where: { },
user: userId, });
musicId: {
in: chuniScorePlaylogMusicId,
},
},
});
const playCountMap = playCounts.reduce((map, item) => { const playCountMap = playCounts.reduce(
(map, item) => {
if (item.musicId !== null) { if (item.musicId !== null) {
map[item.musicId] = item._count.musicId; map[item.musicId] = item._count.musicId;
} }
return map; return map;
}, {} as Record<number, number>); },
{} as Record<number, number>,
);
const songsWithTitles = songs.map((song) => { const songsWithTitles = songs.map((song) => {
const staticInfo = staticMusicInfo.find( const staticInfo = staticMusicInfo.find(
(chuniStaticMusic) => (chuniStaticMusic) =>
chuniStaticMusic.songId === song.musicId && chuniStaticMusic.songId === song.musicId &&
chuniStaticMusic.chartId === song.level chuniStaticMusic.chartId === song.level,
); );
return { return {
...song, ...song,
title: staticInfo?.title || "Unknown Title", title: staticInfo?.title || "Unknown Title",
artist: staticInfo?.artist || "Unknown Artist", artist: staticInfo?.artist || "Unknown Artist",
genre: staticInfo?.genre || "Unknown Genre", genre: staticInfo?.genre || "Unknown Genre",
chartId: staticInfo?.chartId || "Unknown chartId", chartId: staticInfo?.chartId || "Unknown chartId",
level: staticInfo?.level || "Unknown Level", level: staticInfo?.level || "Unknown Level",
chartlevel: song.level || "Unknown Level", chartlevel: song.level || "Unknown Level",
playCount: song.musicId !== null ? playCountMap[song.musicId] || 0 : 0, playCount: song.musicId !== null ? playCountMap[song.musicId] || 0 : 0,
jacketPath: staticInfo?.jacketPath || "", jacketPath: staticInfo?.jacketPath || "",
}; };
}); });
return songsWithTitles; return songsWithTitles;
} catch (error) { } catch (error) {
console.error("Error fetching songs with titles:", error); console.error("Error fetching songs with titles:", error);
throw error; throw error;
}
} }
}
export async function generatePlaylogId(playlogid: number) { export async function generatePlaylogId(playlogid: number) {
try { try {
const tokens = (await daphnis.linkSharingToken.findMany({ const tokens = (await daphnis.linkSharingToken.findMany({
where: { where: {
playlogId: playlogid, playlogId: playlogid,
}, },
select: { select: {
playlogId: true, playlogId: true,
}, },
})) as LinkSharingToken[]; })) as LinkSharingToken[];
const playlogIds: number[] = tokens.map((token) => token.playlogId); const playlogIds: number[] = tokens.map((token) => token.playlogId);
return playlogIds; return playlogIds;
} catch (error) { } catch (error) {
console.error("Error fetching playlogIds:", error); console.error("Error fetching playlogIds:", error);
throw error; throw error;
}
} }
}

View File

@ -41,16 +41,7 @@ const HeaderNavigation = async () => {
</SheetContent> </SheetContent>
</Sheet> </Sheet>
<div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4"> <div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
<form className="ml-auto flex-1 sm:flex-initial"> <div className="ml-auto flex-1 sm:flex-initial"></div>
{/* <div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search"
className="pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px]"
/>
</div> */}
</form>
<DarkToggle /> <DarkToggle />
<DropdownMenu> <DropdownMenu>

View File

@ -0,0 +1,37 @@
import React, { useEffect, useState } from "react";
import { Input } from "../ui/input";
export default function DebouncedInput({
value: initialValue,
onChange,
debounce = 500,
...props
}: {
value: string | number;
onChange: (value: string | number) => void;
debounce?: number;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange">) {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
useEffect(() => {
const timeout = setTimeout(() => {
onChange(value);
}, debounce);
return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
<Input
className="m-4 w-[400px]"
{...props}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}

View File

@ -0,0 +1,87 @@
"use server";
import { artemis } from "@/lib/prisma";
import type * as Prisma from "@prisma/client";
type ChuniScorePlaylog = Prisma.PrismaClient;
type ChuniStaticMusic = Prisma.PrismaClient;
export async function searchSongWithTitle(
userId: number,
searchQuery: string = "",
) {
try {
const songs: ChuniScorePlaylog[] =
await artemis.chuni_score_playlog.findMany({
where: {
user: userId,
},
orderBy: {
userPlayDate: "desc",
},
select: {
id: true,
},
});
const chuniScorePlaylogMusicId = songs
.map((song) => song.musicId)
.filter((id): id is number => id !== null);
const staticMusicInfo: ChuniStaticMusic[] =
await artemis.chuni_static_music.findMany({
where: {
songId: {
in: chuniScorePlaylogMusicId,
},
title: {
contains: searchQuery,
},
},
select: {
title: true,
},
});
const playCounts = await artemis.chuni_score_playlog.groupBy({
by: ["musicId"],
_count: {
musicId: true,
},
where: {
user: userId,
musicId: {
in: chuniScorePlaylogMusicId,
},
},
});
const playCountMap = playCounts.reduce(
(map, item) => {
if (item.musicId !== null) {
map[item.musicId] = item._count.musicId;
}
return map;
},
{} as Record<number, number>,
);
const songsWithTitles = songs.map((song) => {
const staticInfo = staticMusicInfo.find(
(chuniStaticMusic) =>
chuniStaticMusic.songId === song.musicId &&
chuniStaticMusic.chartId === song.level,
);
return {
...song,
title: staticInfo?.title || "Unknown Title",
};
});
return songsWithTitles;
} catch (error) {
console.error("Error fetching songs with titles:", error);
throw error;
}
}

View File

@ -1,10 +1,14 @@
"use client"; "use client";
// https://github.com/dracor-org/einakter/blob/466ca1663098a16cc1141129a6ba22628135b04c/src/components/Table.tsx#L26
// used the above for reference on how to fuzzy search
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
ColumnDef, ColumnDef,
SortingState, SortingState,
getSortedRowModel, getSortedRowModel,
flexRender, flexRender,
getFilteredRowModel,
getCoreRowModel, getCoreRowModel,
getPaginationRowModel, getPaginationRowModel,
useReactTable, useReactTable,
@ -18,8 +22,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { useState } from "react"; import DebouncedInput from "./DebouncedInput";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
@ -30,21 +33,32 @@ export function DataTable<TData, TValue>({
data, data,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState("");
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: { state: {
sorting, sorting,
globalFilter,
}, },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
}); });
return ( return (
<div className="rounded-md border "> <div className="rounded-md border">
<div className="mb-2">
<DebouncedInput
value={globalFilter ?? ""}
onChange={(value) => setGlobalFilter(String(value))}
placeholder={`Search`}
/>
</div>
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@ -55,7 +69,7 @@ export function DataTable<TData, TValue>({
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext(),
)} )}
</TableHead> </TableHead>
))} ))}
@ -85,7 +99,7 @@ export function DataTable<TData, TValue>({
)} )}
</TableBody> </TableBody>
</Table> </Table>
<div className="flex items-center justify-end space-x-2 py- p-4"> <div className="py- flex items-center justify-end space-x-2 p-4">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"