feat: allow filtering with null (no) rating
This commit is contained in:
parent
aeb067aadb
commit
93f359e684
@ -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}`}
|
||||||
|
@ -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 },
|
||||||
) => {
|
) => {
|
||||||
|
@ -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
|
||||||
|
@ -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 });
|
|
||||||
|
@ -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[] };
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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>;
|
||||||
|
@ -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 },
|
||||||
|
@ -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),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user