diff --git a/components/Image.tsx b/components/Image.tsx index d14fa77..7b5d435 100644 --- a/components/Image.tsx +++ b/components/Image.tsx @@ -65,7 +65,9 @@ const Image = ( style={props.style} srcset={responsiveAttributes.srcset} sizes={responsiveAttributes.sizes} - src={`/api/images?image=${asset(props.src)}`} + src={`/api/images?image=${asset(props.src)}${ + props.width ? `&width=${props.width}` : "" + }${props.height ? `&height=${props.height}` : ""}`} width={props.width} height={props.height} class={props.class} diff --git a/components/layouts/main.tsx b/components/layouts/main.tsx index a20741e..2ec639f 100644 --- a/components/layouts/main.tsx +++ b/components/layouts/main.tsx @@ -1,6 +1,6 @@ import { ComponentChildren } from "preact"; import Search from "@islands/Search.tsx"; -import { SearchResult } from "@lib/types.ts"; +import { GenericResource, SearchResult } from "@lib/types.ts"; export type Props = { children: ComponentChildren; @@ -9,7 +9,7 @@ export type Props = { url: URL; description?: string; context?: { type: string }; - searchResults?: SearchResult; + searchResults?: GenericResource[]; }; export const MainLayout = ( diff --git a/deno.json b/deno.json index 56634aa..1421f2a 100644 --- a/deno.json +++ b/deno.json @@ -33,6 +33,7 @@ "@std/yaml": "jsr:@std/yaml@^1.0.5", "drizzle-kit": "npm:drizzle-kit@^0.30.1", "drizzle-orm": "npm:drizzle-orm@^0.38.3", + "fuzzysort": "npm:fuzzysort@^3.1.0", "preact": "https://esm.sh/preact@10.22.0", "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2", "preact/": "https://esm.sh/preact@10.22.0/", @@ -42,7 +43,6 @@ "tailwindcss/plugin": "npm:/tailwindcss@^3.4.17/plugin.js", "camelcase-css": "npm:camelcase-css", "tsx": "npm:tsx@^4.19.2", - "typesense": "https://raw.githubusercontent.com/bradenmacdonald/typesense-deno/main/mod.ts", "yaml": "https://deno.land/std@0.197.0/yaml/mod.ts", "zod": "https://deno.land/x/zod@v3.21.4/mod.ts", "fs": "https://deno.land/std/fs/mod.ts", diff --git a/docker-compose.yml b/docker-compose.yml index 3882edc..8fc20b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,16 +8,6 @@ services: volumes: - ./data/redis-data:/data - typesense: - image: typesense/typesense:0.24.1 - restart: on-failure - ports: - - "8108:8108" - volumes: - - ./data/typesense-data:/data - env_file: .env - command: '--data-dir /data' - volumes: redis-data: typesense-data: diff --git a/fresh.gen.ts b/fresh.gen.ts index a12c0b6..8161e8d 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -21,14 +21,12 @@ import * as $api_movies_name_ from "./routes/api/movies/[name].ts"; import * as $api_movies_enhance_name_ from "./routes/api/movies/enhance/[name].ts"; import * as $api_movies_index from "./routes/api/movies/index.ts"; import * as $api_query_index from "./routes/api/query/index.ts"; -import * as $api_query_sync from "./routes/api/query/sync.ts"; import * as $api_recipes_name_ from "./routes/api/recipes/[name].ts"; import * as $api_recipes_index from "./routes/api/recipes/index.ts"; import * as $api_recommendation_all from "./routes/api/recommendation/all.ts"; import * as $api_recommendation_data from "./routes/api/recommendation/data.ts"; import * as $api_recommendation_index from "./routes/api/recommendation/index.ts"; import * as $api_recommendation_movie_id_ from "./routes/api/recommendation/movie/[id].ts"; -import * as $api_resources from "./routes/api/resources.ts"; import * as $api_series_name_ from "./routes/api/series/[name].ts"; import * as $api_series_enhance_name_ from "./routes/api/series/enhance/[name].ts"; import * as $api_series_index from "./routes/api/series/index.ts"; @@ -80,14 +78,12 @@ const manifest = { "./routes/api/movies/enhance/[name].ts": $api_movies_enhance_name_, "./routes/api/movies/index.ts": $api_movies_index, "./routes/api/query/index.ts": $api_query_index, - "./routes/api/query/sync.ts": $api_query_sync, "./routes/api/recipes/[name].ts": $api_recipes_name_, "./routes/api/recipes/index.ts": $api_recipes_index, "./routes/api/recommendation/all.ts": $api_recommendation_all, "./routes/api/recommendation/data.ts": $api_recommendation_data, "./routes/api/recommendation/index.ts": $api_recommendation_index, "./routes/api/recommendation/movie/[id].ts": $api_recommendation_movie_id_, - "./routes/api/resources.ts": $api_resources, "./routes/api/series/[name].ts": $api_series_name_, "./routes/api/series/enhance/[name].ts": $api_series_enhance_name_, "./routes/api/series/index.ts": $api_series_index, diff --git a/islands/KMenu.tsx b/islands/KMenu.tsx index 48a88bb..5b8f12d 100644 --- a/islands/KMenu.tsx +++ b/islands/KMenu.tsx @@ -1,5 +1,5 @@ import { Signal, useSignal } from "@preact/signals"; -import { useEffect, useRef } from "preact/hooks"; +import { useRef } from "preact/hooks"; import { useEventListener } from "@lib/hooks/useEventListener.ts"; import { menus } from "@islands/KMenu/commands.ts"; import { MenuEntry } from "@islands/KMenu/types.ts"; diff --git a/islands/KMenu/commands.ts b/islands/KMenu/commands.ts index caf293f..7b50c59 100644 --- a/islands/KMenu/commands.ts +++ b/islands/KMenu/commands.ts @@ -31,13 +31,13 @@ export const menus: Record = { title: "Login", icon: "IconLogin", cb: () => { - const url = new URL(window.location.href); + const url = new URL(globalThis.location.href); url.pathname = "/api/auth/login"; url.searchParams.set( "redirect", - encodeURIComponent(window.location.pathname), + encodeURIComponent(globalThis.location.pathname), ); - window.location.href = url.href; + globalThis.location.href = url.href; }, visible: () => { return !getCookie("session_cookie"); @@ -47,35 +47,24 @@ export const menus: Record = { title: "Search", icon: "IconSearch", cb: () => { - window.location.href += "?q="; + globalThis.location.href += "?q="; }, visible: () => { - return !!getCookie("session_cookie") && window.location.search === ""; + return !!getCookie("session_cookie") && + globalThis.location.search === ""; }, }, { title: "Logout", icon: "IconLogout", cb: () => { - const url = new URL(window.location.href); + const url = new URL(globalThis.location.href); url.pathname = "/api/auth/logout"; url.searchParams.set( "redirect", - encodeURIComponent(window.location.pathname), + encodeURIComponent(globalThis.location.pathname), ); - window.location.href = url.href; - }, - visible: () => { - return !!getCookie("session_cookie"); - }, - }, - { - title: "Sync Typesense", - icon: "IconStarFilled", - cb: () => { - fetch("/api/query/sync", { - method: "POST", - }); + globalThis.location.href = url.href; }, visible: () => { return !!getCookie("session_cookie"); diff --git a/islands/Search.tsx b/islands/Search.tsx index 659de72..b1aa4fe 100644 --- a/islands/Search.tsx +++ b/islands/Search.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "preact/hooks"; import useDebouncedCallback from "@lib/hooks/useDebouncedCallback.ts"; import { IconLoader2, IconSearch } from "@components/icons.tsx"; import { useEventListener } from "@lib/hooks/useEventListener.ts"; -import { SearchResult } from "@lib/types.ts"; +import { GenericResource } from "@lib/types.ts"; import { resources } from "@lib/resources.ts"; import { getCookie } from "@lib/string.ts"; import { IS_BROWSER } from "$fresh/runtime.ts"; @@ -17,13 +17,13 @@ export async function fetchQueryResource(url: URL, type = "") { const status = url.searchParams.get("status"); try { - url.pathname = "/api/resources"; - url.searchParams.set("q", encodeURIComponent(query || "*")); + url.pathname = "/api/query"; + url.searchParams.set("q", encodeURIComponent(query || "")); if (status) { url.searchParams.set("status", "not-seen"); } if (type) { - url.searchParams.set("type", type); + url.searchParams.set("types", type); } const response = await fetch(url); const jsonData = await response.json(); @@ -39,9 +39,9 @@ export const RedirectSearchHandler = () => { if (e?.target?.nodeName == "INPUT") return; if ( e.key === "?" && - window.location.search === "" + globalThis.location.search === "" ) { - window.location.href += "?q=*"; + globalThis.location.href += "?q="; } }, IS_BROWSER ? document?.body : undefined); } @@ -50,12 +50,12 @@ export const RedirectSearchHandler = () => { }; const SearchResultImage = ({ src }: { src: string }) => { - const imageSrc = `/api/images?image=${src}&width=50&height=50`; - return ( preview image ); @@ -63,13 +63,12 @@ const SearchResultImage = ({ src }: { src: string }) => { export const SearchResultItem = ( { item, showEmoji = false }: { - item: NonNullable[number]; + item: GenericResource; showEmoji?: boolean; }, ) => { - const doc = item.document; - const resourceType = resources[doc.type]; - const href = resourceType ? `${resourceType.link}/${doc.id}` : ""; + const resourceType = resources[item.type]; + const href = resourceType ? `${resourceType.link}/${item.id}` : ""; return ( : ""} - {doc?.image && } - {doc?.name} + {item.meta?.image && } + {item?.name} ); }; export const SearchResultList = ( - { result, showEmoji }: { result: SearchResult; showEmoji?: boolean }, + { result, showEmoji }: { result: GenericResource[]; showEmoji?: boolean }, ) => { return (
- {result?.hits + {result?.length ? (
- {result.hits.map((hit) => ( + {result.map((hit) => ( ))}
@@ -106,17 +105,17 @@ const Search = ( { q = "*", type, results }: { q: string; type?: string; - results?: SearchResult; + results?: GenericResource[]; }, ) => { const searchQuery = useSignal(q); - const data = useSignal(results); + const data = useSignal(results); const isLoading = useSignal(false); const showSeenStatus = useSignal(false); const inputRef = useRef(null); if ("history" in globalThis) { - const u = new URL(window.location.href); + const u = new URL(globalThis.location.href); if (u.searchParams.get("q") !== searchQuery.value) { u.searchParams.set("q", searchQuery.value); } @@ -126,14 +125,14 @@ const Search = ( u.searchParams.delete("rating"); } - window.history.replaceState({}, "", u); + globalThis.history.replaceState({}, "", u); } const fetchData = async () => { try { isLoading.value = true; const jsonData = await fetchQueryResource( - new URL(window?.location.href), + new URL(globalThis?.location.href), type, ); data.value = jsonData; @@ -188,7 +187,7 @@ const Search = ( - {data?.value?.hits?.length && !isLoading.value + {data.value?.length && !isLoading.value ? : isLoading.value ?
diff --git a/lib/crud.ts b/lib/crud.ts index d4d99da..eb03465 100644 --- a/lib/crud.ts +++ b/lib/crud.ts @@ -82,7 +82,7 @@ export function createCrud( return addThumbnailToResource(res); } - return res; + return { ...res, content }; } function create(id: string, content: string | ArrayBuffer | T) { const path = pathFromId(id); diff --git a/lib/documents.ts b/lib/documents.ts index c308b15..e77a69b 100644 --- a/lib/documents.ts +++ b/lib/documents.ts @@ -11,7 +11,6 @@ import remarkFrontmatter, { import { SILVERBULLET_SERVER } from "@lib/env.ts"; import { fixRenderedMarkdown } from "@lib/helpers.ts"; import { createLogger } from "@lib/log.ts"; -import * as typesense from "@lib/typesense.ts"; import { db } from "@lib/sqlite/sqlite.ts"; import { documentTable } from "@lib/sqlite/schema.ts"; import { eq } from "drizzle-orm/sql"; @@ -59,8 +58,6 @@ export function createDocument( log.info("creating document", { name }); - typesense.synchronize(); - return fetch(SILVERBULLET_SERVER + "/" + name, { body: content, method: "PUT", diff --git a/lib/env.ts b/lib/env.ts index 843bf35..57517a4 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -22,13 +22,9 @@ export const SESSION_DURATION = duration ? +duration : (60 * 60 * 24); export const JWT_SECRET = Deno.env.get("JWT_SECRET"); -export const TYPESENSE_URL = Deno.env.get("TYPESENSE_URL") || - "http://localhost:8108"; -export const TYPESENSE_API_KEY = Deno.env.get("TYPESENSE_API_KEY"); - export const DATA_DIR = Deno.env.has("DATA_DIR") ? path.resolve(Deno.env.get("DATA_DIR")!) : path.resolve(Deno.cwd(), "data"); export const LOG_LEVEL: string = Deno.env.get("LOG_LEVEL") || - "debug"; + "warn"; diff --git a/lib/search.ts b/lib/search.ts index 1a2cabc..c2d877e 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -1,17 +1,20 @@ import { resources } from "@lib/resources.ts"; -import { SearchResult } from "@lib/types.ts"; -import { getTypeSenseClient } from "@lib/typesense.ts"; +import fuzzysort from "npm:fuzzysort"; +import { GenericResource } from "@lib/types.ts"; import { extractHashTags } from "@lib/string.ts"; +import { getAllMovies, Movie } from "@lib/resource/movies.ts"; +import { Article, getAllArticles } from "@lib/resource/articles.ts"; +import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; +import { getAllSeries, Series } from "@lib/resource/series.ts"; type ResourceType = keyof typeof resources; type SearchParams = { q: string; - type?: ResourceType; + types?: string[]; tags?: string[]; - rating?: string; - author?: string; - query_by?: string; + rating?: number; + authors?: string[]; }; export function parseResourceUrl(_url: string | URL): SearchParams | undefined { @@ -31,56 +34,60 @@ export function parseResourceUrl(_url: string | URL): SearchParams | undefined { return { q: query, - type: url.searchParams.get("type") as ResourceType || undefined, + types: url.searchParams.get("type")?.split(",") as ResourceType[] || + undefined, tags: hashTags, - rating: url.searchParams.get("rating") || undefined, - query_by: url.searchParams.get("query_by") || undefined, + rating: url.searchParams.has("rating") + ? parseInt(url.searchParams.get("rating")!) + : undefined, }; } catch (_err) { return undefined; } } +const isResource = ( + item: Movie | Series | Article | Recipe | boolean, +): item is Movie | Series | Article | Recipe => { + return !!item; +}; + export async function searchResource( - { q, query_by = "name,description,author,tags", tags = [], type, rating }: - SearchParams, -): Promise { - const typesenseClient = await getTypeSenseClient(); - if (!typesenseClient) { - throw new Error("Query not available"); - } + { q, tags = [], types, authors, rating }: SearchParams, +): Promise { + console.log("searchResource", { q, tags, types, authors, rating }); - const filter_by: string[] = []; - - if (type) { - filter_by.push(`type:=${type}`); - } + let resources = (await Promise.all([ + (!types || types.includes("movie")) && getAllMovies(), + (!types || types.includes("series")) && getAllSeries(), + (!types || types.includes("article")) && getAllArticles(), + (!types || types.includes("recipe")) && getAllRecipes(), + ])).flat().filter(isResource); if (tags?.length) { - filter_by.push(`tags:[${tags.map((t) => `\`${t}\``).join(",")}]`); - for (const tag of tags) { - q = q.replaceAll(`#${tag}`, ""); - } - if (!q.trim().length) { - q = "*"; - } - } - - if (typeof rating !== "undefined") { - if (rating === "null") { - filter_by.push(`rating: null`); - } else { - filter_by.push(`rating: ${rating}`); - } - } - - return await typesenseClient.collections("resources") - .documents().search({ - q, - query_by, - facet_by: "rating,author,tags", - max_facet_values: 10, - filter_by: filter_by.join(" && "), - per_page: 50, + resources = resources.filter((r) => { + return tags?.every((t) => r.tags.includes(t)); }); + } + + if (authors?.length) { + resources = resources.filter((r) => { + return r?.meta?.author && authors.includes(r?.meta?.author); + }); + } + if (rating) { + resources = resources.filter((r) => { + return r?.meta?.rating && r.meta.rating >= rating; + }); + } + + if (q.length && q !== "*") { + const results = fuzzysort.go(q, resources, { + keys: ["content", "name", "description"], + threshold: 0.3, + }); + resources = results.map((r) => r.obj); + } + + return resources; } diff --git a/lib/types.ts b/lib/types.ts index f5024c1..97ac3dd 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,3 @@ -import { SearchResponse } from "https://raw.githubusercontent.com/bradenmacdonald/typesense-deno/main/src/Typesense/Documents.ts"; import { resources } from "@lib/resources.ts"; export interface TMDBMovie { @@ -39,6 +38,7 @@ export type GenericResource = { id: string; tags?: string[]; type: keyof typeof resources; + content?: string; meta?: { image?: string; author?: string; @@ -58,7 +58,7 @@ export interface GiteaOauthUser { groups: any; } -export type TypesenseDocument = { +export type SearchResult = { id: string; name: string; type: keyof typeof resources; @@ -68,5 +68,3 @@ export type TypesenseDocument = { description?: string; image?: string; }; - -export type SearchResult = SearchResponse; diff --git a/lib/typesense.ts b/lib/typesense.ts deleted file mode 100644 index 8c6582f..0000000 --- a/lib/typesense.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Client } from "typesense"; -import { TYPESENSE_API_KEY, TYPESENSE_URL } from "@lib/env.ts"; -import { getAllMovies } from "@lib/resource/movies.ts"; -import { getAllRecipes } from "@lib/resource/recipes.ts"; -import { getAllArticles } from "@lib/resource/articles.ts"; -import { createLogger } from "@lib/log.ts"; -import { debounce } from "https://deno.land/std@0.193.0/async/mod.ts"; -import { getAllSeries } from "@lib/resource/series.ts"; -import { TypesenseDocument } from "@lib/types.ts"; - -const log = createLogger("typesense"); - -function sanitizeStringForTypesense(input: string) { - // Remove backslashes - const withoutBackslashes = input.replace(/\\/g, ""); - - // Remove control characters other than carriage return and line feed - const withoutControlCharacters = withoutBackslashes.replace( - /[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/g, - "", - ); - - // Remove Unicode characters above U+FFFF - const withoutUnicodeAboveFFFF = withoutControlCharacters.replace( - /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - "", - ); - - return withoutUnicodeAboveFFFF; -} - -// Use a promise to initialize the client as needed, rather than at import time. -let clientPromise: Promise | undefined; - -export function getTypeSenseClient(): Promise { - if (clientPromise === undefined) { - let typesenseUrl: URL; - try { - typesenseUrl = new URL(TYPESENSE_URL); - } catch (_err) { - return Promise.resolve(null); - } - - clientPromise = new Promise((resolve) => { - if (!TYPESENSE_API_KEY) { - return resolve(null); - } - const client = new Client({ - nodes: [{ - host: typesenseUrl.hostname, - port: +typesenseUrl.port || 8108, - protocol: typesenseUrl.protocol.slice(0, -1), - }], - apiKey: TYPESENSE_API_KEY, - connectionTimeoutSeconds: 2, - }); - resolve(client); - }); - } - return clientPromise; -} - -async function initializeTypesense() { - try { - const client = await getTypeSenseClient(); - if (!client) return; - // Create the "resources" collection if it doesn't exist - const collections = await client.collections().retrieve(); - const resourcesCollection = collections.find((collection) => - collection.name === "resources" - ); - if (!resourcesCollection) { - await client.collections().create({ - name: "resources", - fields: [ - { name: "name", type: "string" }, - { name: "type", type: "string", facet: true }, - { name: "date", type: "string", optional: true }, - { name: "author", type: "string", facet: true, optional: true }, - { name: "rating", type: "int32", facet: true }, - { name: "tags", type: "string[]", facet: true }, - { name: "description", type: "string", optional: true }, - { name: "image", type: "string", optional: true }, - ], - default_sorting_field: "rating", // Default field for sorting - }); - log.info('created "resources" collection'); - } else { - log.info('collection "resources" already exists.'); - } - } catch (error) { - log.error("error initializing", error); - } -} - -const init = initializeTypesense(); - -export async function createTypesenseDocument(doc: TypesenseDocument) { - const client = await getTypeSenseClient(); - if (!client) return; - - // await client.collections("resources").documents().create( - // doc, - // { action: "upsert" }, - // ); -} - -async function synchronizeWithTypesense() { - return; - await init; - try { - const allResources = (await Promise.all([ - getAllMovies(), - getAllArticles(), - getAllRecipes(), - getAllSeries(), - ])).flat(); // Replace with your function to get all resources from the database - - const client = await getTypeSenseClient(); - if (!client) return; - - // Convert the list of documents to Typesense compatible format (array of objects) - const typesenseDocuments = allResources.map((resource) => { - return { - id: resource.id, // Convert the document ID to a string, as Typesense only supports string IDs - name: sanitizeStringForTypesense(resource.name), - description: sanitizeStringForTypesense( - resource?.description || resource?.content || "", - ), - author: resource.meta?.author, - image: resource.meta?.image, - tags: resource?.tags || [], - rating: resource.meta?.rating || 0, - date: resource.meta?.date?.toString() || "", - type: resource.type, - }; - }); - - return; - - await client.collections("resources").documents().import( - typesenseDocuments, - { action: "upsert" }, - ); - - // Get all the IDs of documents currently indexed in Typesense - const allTypesenseDocuments = await client.collections("resources") - .documents().search({ - q: "*", - query_by: "name,type,date,description", - per_page: 250, - limit_hits: 9999, - }); - - const deletedDocumentIds = allTypesenseDocuments.hits - ?.map((doc) => doc?.document?.id) - ?.filter((id) => - // Find deleted document IDs by comparing the Typesense document IDs with the current list of resources - !allResources.some((resource) => resource.id.toString() === id) - ).map((id) => client.collections("resources").documents(id).delete()); - - // Delete the documents with IDs found in deletedDocumentIds - if (deletedDocumentIds) { - await Promise.all( - deletedDocumentIds, - ); - } - - log.info("data synchronized"); - } catch (error) { - log.error("error synchronizing", error); - } -} - -// Call the synchronizeWithTypesense function to trigger the synchronization -synchronizeWithTypesense(); - -export const synchronize = debounce(synchronizeWithTypesense, 1000 * 60 * 5); diff --git a/routes/api/query/index.ts b/routes/api/query/index.ts index 1c4ed38..d3ac300 100644 --- a/routes/api/query/index.ts +++ b/routes/api/query/index.ts @@ -1,15 +1,7 @@ import { Handlers } from "$fresh/server.ts"; import { json } from "@lib/helpers.ts"; -import { getAllMovies, Movie } from "@lib/resource/movies.ts"; -import { Article, getAllArticles } from "@lib/resource/articles.ts"; -import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; import { AccessDeniedError } from "@lib/errors.ts"; - -const isResource = ( - item: Movie | Article | Recipe | boolean, -): item is Movie | Article | Recipe => { - return !!item; -}; +import { searchResource } from "@lib/search.ts"; export const handler: Handlers = { async GET(req, ctx) { @@ -20,27 +12,16 @@ export const handler: Handlers = { const url = new URL(req.url); - const types = url.searchParams.get("type")?.split(", "); - - let resources = (await Promise.all([ - (!types || types.includes("movie")) && getAllMovies(), - (!types || types.includes("article")) && getAllArticles(), - (!types || types.includes("recipe")) && getAllRecipes(), - ])).flat().filter(isResource); - + const types = url.searchParams.get("types")?.split(","); const tags = url.searchParams?.get("tags")?.split(","); - if (tags?.length) { - resources = resources.filter((r) => { - return tags?.every((t) => r.tags.includes(t)); - }); - } + const authors = url.searchParams?.get("authors")?.split(","); - const authors = url.searchParams?.get("author")?.split(","); - if (authors?.length) { - resources = resources.filter((r) => { - return r?.meta?.author && authors.includes(r?.meta?.author); - }); - } + const resources = await searchResource({ + q: url.searchParams.get("q") || "", + types, + tags, + authors, + }); return json(resources); }, diff --git a/routes/api/query/sync.ts b/routes/api/query/sync.ts deleted file mode 100644 index 2f96d8b..0000000 --- a/routes/api/query/sync.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AccessDeniedError } from "@lib/errors.ts"; -import { Handlers } from "$fresh/server.ts"; -import { synchronize } from "@lib/typesense.ts"; - -export const handler: Handlers = { - POST(_, ctx) { - const session = ctx.state.session; - if (!session) { - throw new AccessDeniedError(); - } - - synchronize(); - - return new Response("OK"); - }, -}; diff --git a/routes/api/resources.ts b/routes/api/resources.ts deleted file mode 100644 index 4d3cf1c..0000000 --- a/routes/api/resources.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; -import { AccessDeniedError, BadRequestError } from "@lib/errors.ts"; -import { getTypeSenseClient } from "@lib/typesense.ts"; -import { json } from "@lib/helpers.ts"; -import { parseResourceUrl, searchResource } from "@lib/search.ts"; - -export const handler: Handlers = { - async GET(req, ctx) { - const session = ctx.state.session; - if (!session) { - throw new AccessDeniedError(); - } - - const searchParams = parseResourceUrl(req.url); - - // Perform the Typesense search - const searchResults = await searchResource(searchParams); - - return json(searchResults); - }, -}; diff --git a/routes/articles/index.tsx b/routes/articles/index.tsx index 7331fb1..c4489d1 100644 --- a/routes/articles/index.tsx +++ b/routes/articles/index.tsx @@ -1,29 +1,30 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; import { Article, getAllArticles } from "@lib/resource/articles.ts"; -import { Card } from "@components/Card.tsx"; import { KMenu } from "@islands/KMenu.tsx"; import { Grid } from "@components/Grid.tsx"; import { IconArrowLeft } from "@components/icons.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; -import { SearchResult } from "@lib/types.ts"; +import { GenericResource } from "@lib/types.ts"; import { ResourceCard } from "@components/Card.tsx"; export const handler: Handlers< - { articles: Article[] | null; searchResults?: SearchResult } + { articles: Article[] | null; searchResults?: GenericResource[] } > = { async GET(req, ctx) { const articles = await getAllArticles(); const searchParams = parseResourceUrl(req.url); const searchResults = searchParams && - await searchResource({ ...searchParams, type: "article" }); + await searchResource({ ...searchParams, types: ["article"] }); return ctx.render({ articles, searchResults }); }, }; export default function Greet( - props: PageProps<{ articles: Article[] | null; searchResults: SearchResult }>, + props: PageProps< + { articles: Article[] | null; searchResults: GenericResource[] } + >, ) { const { articles, searchResults } = props.data; return ( diff --git a/routes/movies/index.tsx b/routes/movies/index.tsx index 28038fc..eea8112 100644 --- a/routes/movies/index.tsx +++ b/routes/movies/index.tsx @@ -6,16 +6,18 @@ import { IconArrowLeft } from "@components/icons.tsx"; import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; -import { SearchResult } from "@lib/types.ts"; +import { GenericResource } from "@lib/types.ts"; import { PageProps } from "$fresh/server.ts"; export default async function Greet( - props: PageProps<{ movies: Movie[] | null; searchResults: SearchResult }>, + props: PageProps< + { movies: Movie[] | null; searchResults: GenericResource[] } + >, ) { const allMovies = await getAllMovies(); const searchParams = parseResourceUrl(props.url); const searchResults = searchParams && - await searchResource({ ...searchParams, type: "movie" }); + await searchResource({ ...searchParams, types: ["movie"] }); const movies = allMovies.sort((a, b) => a?.meta?.rating > b?.meta?.rating ? -1 : 1 ); diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index 756598b..dae1384 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -6,23 +6,25 @@ import { IconArrowLeft } from "@components/icons.tsx"; import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; -import { SearchResult } from "@lib/types.ts"; +import { GenericResource } from "@lib/types.ts"; import { ResourceCard } from "@components/Card.tsx"; export const handler: Handlers< - { recipes: Recipe[] | null; searchResults?: SearchResult } + { recipes: Recipe[] | null; searchResults?: GenericResource[] } > = { async GET(req, ctx) { const recipes = await getAllRecipes(); const searchParams = parseResourceUrl(req.url); const searchResults = searchParams && - await searchResource({ ...searchParams, type: "recipe" }); + await searchResource({ ...searchParams, types: ["recipe"] }); return ctx.render({ recipes, searchResults }); }, }; export default function Greet( - props: PageProps<{ recipes: Recipe[] | null; searchResults: SearchResult }>, + props: PageProps< + { recipes: Recipe[] | null; searchResults: GenericResource[] } + >, ) { const { recipes, searchResults } = props.data; return ( diff --git a/routes/series/index.tsx b/routes/series/index.tsx index 7fda3c9..1c93348 100644 --- a/routes/series/index.tsx +++ b/routes/series/index.tsx @@ -7,22 +7,24 @@ import { RedirectSearchHandler } from "@islands/Search.tsx"; import { KMenu } from "@islands/KMenu.tsx"; import { ResourceCard } from "@components/Card.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; -import { SearchResult } from "@lib/types.ts"; +import { GenericResource } from "@lib/types.ts"; export const handler: Handlers< - { series: Series[] | null; searchResults?: SearchResult } + { series: Series[] | null; searchResults?: GenericResource[] } > = { async GET(req, ctx) { const series = await getAllSeries(); const searchParams = parseResourceUrl(req.url); const searchResults = searchParams && - await searchResource({ ...searchParams, type: "series" }); + await searchResource({ ...searchParams, types: ["series"] }); return ctx.render({ series, searchResults }); }, }; export default function Greet( - props: PageProps<{ series: Series[] | null; searchResults: SearchResult }>, + props: PageProps< + { series: Series[] | null; searchResults: GenericResource[] } + >, ) { const { series, searchResults } = props.data;