Spaces:
Paused
Paused
| import { createServerFn } from "@tanstack/react-start"; | |
| import { z } from "zod"; | |
| import { normalizePost, getByPath, type NormalizedPost, type SiteConfig } from "./sites"; | |
| const SiteSchema: z.ZodType<SiteConfig> = z.object({ | |
| id: z.string(), | |
| name: z.string(), | |
| baseUrl: z.string().url(), | |
| searchPath: z.string(), | |
| params: z.object({ tags: z.string(), page: z.string(), limit: z.string() }), | |
| postsPath: z.string(), | |
| fields: z.object({ | |
| id: z.string(), | |
| previewUrl: z.string(), | |
| fileUrl: z.string(), | |
| ext: z.string(), | |
| tags: z.string(), | |
| source: z.string().optional(), | |
| rating: z.string().optional(), | |
| score: z.string().optional(), | |
| sampleUrl: z.string().optional(), | |
| fileSize: z.string().optional(), | |
| width: z.string().optional(), | |
| height: z.string().optional(), | |
| md5: z.string().optional(), | |
| uploader: z.string().optional(), | |
| createdAt: z.string().optional(), | |
| }), | |
| headers: z.record(z.string(), z.string()).optional(), | |
| pageStartsAt: z.number().optional(), | |
| }); | |
| const InputSchema = z.object({ | |
| site: SiteSchema, | |
| tags: z.string().default(""), | |
| page: z.number().int().min(1).default(1), | |
| limit: z.number().int().min(1).max(200).default(40), | |
| }); | |
| export type SearchResult = { | |
| posts: NormalizedPost[]; | |
| error?: string; | |
| }; | |
| export const searchPosts = createServerFn({ method: "POST" }) | |
| .inputValidator((input: unknown) => InputSchema.parse(input)) | |
| .handler(async ({ data }): Promise<SearchResult> => { | |
| const { site, tags, page, limit } = data; | |
| const pageParam = (site.pageStartsAt ?? 1) === 0 ? page - 1 : page; | |
| const url = new URL(site.searchPath, site.baseUrl); | |
| url.searchParams.set(site.params.tags, tags); | |
| url.searchParams.set(site.params.page, String(pageParam)); | |
| url.searchParams.set(site.params.limit, String(limit)); | |
| try { | |
| const res = await fetch(url.toString(), { | |
| headers: { | |
| Accept: "application/json", | |
| "User-Agent": "LovableGrabber/1.0", | |
| ...(site.headers ?? {}), | |
| }, | |
| }); | |
| if (!res.ok) { | |
| return { posts: [], error: `${site.name} returned ${res.status}` }; | |
| } | |
| const json = await res.json(); | |
| const arr = getByPath(json, site.postsPath); | |
| if (!Array.isArray(arr)) { | |
| return { posts: [], error: `Response at "${site.postsPath}" is not an array` }; | |
| } | |
| const posts = arr | |
| .map((raw) => normalizePost(raw, site.fields)) | |
| .filter((p): p is NormalizedPost => p !== null); | |
| return { posts }; | |
| } catch (err) { | |
| return { posts: [], error: err instanceof Error ? err.message : "Request failed" }; | |
| } | |
| }); | |