feat: allow filtering with null (no) rating

This commit is contained in:
max_richter 2023-08-20 20:13:47 +02:00
parent aeb067aadb
commit 93f359e684
12 changed files with 67 additions and 36 deletions

View File

@ -1,15 +1,25 @@
import { isLocalImage, isYoutubeLink } from "@lib/string.ts"; import { isLocalImage, isYoutubeLink } from "@lib/string.ts";
import { IconBrandYoutube } from "@components/icons.tsx"; import { IconBrandYoutube } from "@components/icons.tsx";
import { GenericResource } from "@lib/types.ts"; import { GenericResource } from "@lib/types.ts";
import { Rating, SmallRating } from "@components/Rating.tsx";
export function Card( export function Card(
{ link, title, image, thumbnail, backgroundColor, backgroundSize = 100 }: { {
link,
rating,
title,
image,
thumbnail,
backgroundColor,
backgroundSize = 100,
}: {
backgroundSize?: number; backgroundSize?: number;
backgroundColor?: string; backgroundColor?: string;
thumbnail?: string; thumbnail?: string;
link?: string; link?: string;
title?: string; title?: string;
image?: string; image?: string;
rating?: number;
}, },
) { ) {
const backgroundStyle = { const backgroundStyle = {
@ -55,13 +65,21 @@ export function Card(
boxShadow: "0px -60px 40px black inset, 0px 10px 20px #fff1 inset", boxShadow: "0px -60px 40px black inset, 0px 10px 20px #fff1 inset",
}} }}
> >
<div> <div class="flex-1">
{/* Recipe Card content */} {/* Recipe Card content */}
</div> </div>
<div class="mt-2 flex items-center gap-2"> <div
class="mt-2 flex items-center gap-2"
style={{ textShadow: "0px 0px 10px black" }}
>
{isYoutubeLink(link || "") && <IconBrandYoutube />} {isYoutubeLink(link || "") && <IconBrandYoutube />}
{title} {title}
</div> </div>
{rating !== undefined && (
<div class="my-2">
<SmallRating rating={rating} />
</div>
)}
</div> </div>
<div class="absolute inset-x-0 bottom-0 h-3/4" /> <div class="absolute inset-x-0 bottom-0 h-3/4" />
</a> </a>
@ -81,6 +99,7 @@ export function ResourceCard(
<Card <Card
title={res.name} title={res.name}
backgroundColor={res.meta?.average} backgroundColor={res.meta?.average}
rating={res.meta?.rating}
thumbnail={res.meta?.thumbnail} thumbnail={res.meta?.thumbnail}
image={imageUrl} image={imageUrl}
link={`/${sublink}/${res.id}`} link={`/${sublink}/${res.id}`}

View File

@ -2,6 +2,27 @@ import { IconStar, IconStarFilled } from "@components/icons.tsx";
import { useSignal } from "@preact/signals"; import { useSignal } from "@preact/signals";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
export const SmallRating = (
{ max = 5, rating }: { max?: number; rating: number },
) => {
return (
<div
class="flex gap-1 rounded"
style={{ filter: "drop-shadow(0px 3px 3px black)" }}
>
{Array.from({ length: max }).map((_, i) => {
return (
<span>
{(i + 1) <= rating
? <IconStarFilled class="w-3 h-3" />
: <IconStar class="w-3 h-3" />}
</span>
);
})}
</div>
);
};
export const Rating = ( export const Rating = (
props: { max?: number; rating: number }, props: { max?: number; rating: number },
) => { ) => {

View File

@ -18,11 +18,7 @@ export async function fetchQueryResource(url: URL, type = "") {
try { try {
url.pathname = "/api/resources"; url.pathname = "/api/resources";
if (query) { url.searchParams.set("q", encodeURIComponent(query || "*"));
url.searchParams.set("q", encodeURIComponent(query));
} else {
return;
}
if (status) { if (status) {
url.searchParams.set("status", "not-seen"); url.searchParams.set("status", "not-seen");
} }
@ -45,7 +41,7 @@ export const RedirectSearchHandler = () => {
e.key === "?" && e.key === "?" &&
window.location.search === "" window.location.search === ""
) { ) {
window.location.href += "?q="; window.location.href += "?q=*";
} }
}, IS_BROWSER ? document?.body : undefined); }, IS_BROWSER ? document?.body : undefined);
} }
@ -127,9 +123,9 @@ const Search = (
u.searchParams.set("q", searchQuery.value); u.searchParams.set("q", searchQuery.value);
} }
if (showSeenStatus.value) { if (showSeenStatus.value) {
u.searchParams.set("status", "not-seen"); u.searchParams.set("rating", "0");
} else { } else {
u.searchParams.delete("status"); u.searchParams.delete("rating");
} }
window.history.replaceState({}, "", u); window.history.replaceState({}, "", u);
@ -191,7 +187,7 @@ const Search = (
onInput={handleInputChange} onInput={handleInputChange}
/> />
</div> </div>
<Checkbox label="seen" checked={showSeenStatus} /> <Checkbox label="not-seen" checked={showSeenStatus} />
<Rating rating={4} /> <Rating rating={4} />
</header> </header>
{data?.value?.hits?.length && !isLoading.value {data?.value?.hits?.length && !isLoading.value

View File

@ -23,5 +23,3 @@ export const TYPESENSE_API_KEY = Deno.env.get("TYPESENSE_API_KEY");
export const LOG_LEVEL: string = Deno.env.get("LOG_LEVEL") || export const LOG_LEVEL: string = Deno.env.get("LOG_LEVEL") ||
"warn"; "warn";
console.log({ LOG_LEVEL });

View File

@ -23,9 +23,8 @@ const logFuncs = {
} as const; } as const;
let longestScope = 0; let longestScope = 0;
let logLevel = (_LOG_LEVEL && _LOG_LEVEL in logMap && logMap[_LOG_LEVEL]) || let logLevel = (_LOG_LEVEL && _LOG_LEVEL in logMap && logMap[_LOG_LEVEL]) ??
LOG_LEVEL.WARN; LOG_LEVEL.WARN;
console.log({ logLevel, logMap });
const ee = new EventEmitter<{ const ee = new EventEmitter<{
log: { level: LOG_LEVEL; scope: string; args: unknown[] }; log: { level: LOG_LEVEL; scope: string; args: unknown[] };

View File

@ -13,7 +13,7 @@ export type Article = {
name: string; name: string;
tags: string[]; tags: string[];
meta: { meta: {
status: "finished" | "not-finished"; done?: boolean;
date: Date; date: Date;
link: string; link: string;
thumbnail?: string; thumbnail?: string;

View File

@ -17,7 +17,6 @@ export type Movie = {
average?: string; average?: string;
author: string; author: string;
rating: number; rating: number;
status: "not-seen" | "watch-again" | "finished";
}; };
}; };

View File

@ -18,7 +18,7 @@ export type Series = {
rating: number; rating: number;
average?: string; average?: string;
thumbnail?: string; thumbnail?: string;
status: "not-seen" | "watch-again" | "finished"; done?: boolean;
}; };
}; };

View File

@ -1,6 +1,6 @@
import { BadRequestError } from "@lib/errors.ts"; import { BadRequestError } from "@lib/errors.ts";
import { resources } from "@lib/resources.ts"; import { resources } from "@lib/resources.ts";
import { ResourceStatus, SearchResult } from "@lib/types.ts"; import { SearchResult } from "@lib/types.ts";
import { getTypeSenseClient } from "@lib/typesense.ts"; import { getTypeSenseClient } from "@lib/typesense.ts";
import { extractHashTags } from "@lib/string.ts"; import { extractHashTags } from "@lib/string.ts";
@ -10,7 +10,7 @@ type SearchParams = {
q: string; q: string;
type?: ResourceType; type?: ResourceType;
tags?: string[]; tags?: string[];
status?: ResourceStatus; rating?: string;
author?: string; author?: string;
query_by?: string; query_by?: string;
}; };
@ -18,7 +18,7 @@ type SearchParams = {
export function parseResourceUrl(_url: string): SearchParams | undefined { export function parseResourceUrl(_url: string): SearchParams | undefined {
try { try {
const url = new URL(_url); const url = new URL(_url);
let query = url.searchParams.get("q"); let query = url.searchParams.get("q") || "*";
if (!query) { if (!query) {
return undefined; return undefined;
} }
@ -34,7 +34,7 @@ export function parseResourceUrl(_url: string): SearchParams | undefined {
q: query, q: query,
type: url.searchParams.get("type") as ResourceType || undefined, type: url.searchParams.get("type") as ResourceType || undefined,
tags: hashTags, tags: hashTags,
status: url.searchParams.get("status") as ResourceStatus || undefined, rating: url.searchParams.get("rating") || undefined,
query_by: url.searchParams.get("query_by") || undefined, query_by: url.searchParams.get("query_by") || undefined,
}; };
} catch (_err) { } catch (_err) {
@ -43,7 +43,7 @@ export function parseResourceUrl(_url: string): SearchParams | undefined {
} }
export async function searchResource( export async function searchResource(
{ q, query_by = "name,description,author,tags", tags = [], type, status }: { q, query_by = "name,description,author,tags", tags = [], type, rating }:
SearchParams, SearchParams,
): Promise<SearchResult> { ): Promise<SearchResult> {
const typesenseClient = await getTypeSenseClient(); const typesenseClient = await getTypeSenseClient();
@ -57,10 +57,6 @@ export async function searchResource(
filter_by.push(`type:=${type}`); filter_by.push(`type:=${type}`);
} }
if (status) {
filter_by.push(`status:=${status}`);
}
if (tags?.length) { if (tags?.length) {
filter_by.push(`tags:[${tags.map((t) => `\`${t}\``).join(",")}]`); filter_by.push(`tags:[${tags.map((t) => `\`${t}\``).join(",")}]`);
for (const tag of tags) { for (const tag of tags) {
@ -71,6 +67,14 @@ export async function searchResource(
} }
} }
if (typeof rating !== "undefined") {
if (rating === "null") {
filter_by.push(`rating: null`);
} else {
filter_by.push(`rating: ${rating}`);
}
}
return await typesenseClient.collections("resources") return await typesenseClient.collections("resources")
.documents().search({ .documents().search({
q, q,

View File

@ -41,6 +41,7 @@ export type GenericResource = {
meta?: { meta?: {
image?: string; image?: string;
author?: string; author?: string;
rating?: number;
average?: string; average?: string;
thumbnail?: string; thumbnail?: string;
}; };
@ -66,9 +67,4 @@ export type TypesenseDocument = {
image?: string; image?: string;
}; };
export enum ResourceStatus {
COMPLETED = "completed",
NOT_COMPLETED = "not_completed",
}
export type SearchResult = SearchResponse<TypesenseDocument>; export type SearchResult = SearchResponse<TypesenseDocument>;

View File

@ -77,7 +77,6 @@ async function initializeTypesense() {
{ name: "type", type: "string", facet: true }, { name: "type", type: "string", facet: true },
{ name: "date", type: "string", optional: true }, { name: "date", type: "string", optional: true },
{ name: "author", type: "string", facet: true, optional: true }, { name: "author", type: "string", facet: true, optional: true },
{ name: "status", type: "string", facet: true, optional: true },
{ name: "rating", type: "int32", facet: true }, { name: "rating", type: "int32", facet: true },
{ name: "tags", type: "string[]", facet: true }, { name: "tags", type: "string[]", facet: true },
{ name: "description", type: "string", optional: true }, { name: "description", type: "string", optional: true },

View File

@ -130,7 +130,7 @@ async function processCreateArticle(
const meta: Article["meta"] = { const meta: Article["meta"] = {
author: (author || "").replace("@", "twitter:"), author: (author || "").replace("@", "twitter:"),
link: fetchUrl, link: fetchUrl,
status: "not-finished", done: false,
date: new Date(), date: new Date(),
}; };
@ -194,7 +194,7 @@ async function processCreateYoutubeVideo(
content: video.snippet.description, content: video.snippet.description,
tags: video.snippet.tags.slice(0, 5), tags: video.snippet.tags.slice(0, 5),
meta: { meta: {
status: "not-finished", done: false,
link: fetchUrl, link: fetchUrl,
author: video.snippet.channelTitle, author: video.snippet.channelTitle,
date: new Date(video.snippet.publishedAt), date: new Date(video.snippet.publishedAt),