| 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), |
| login: z.string().optional(), |
| apiKey: z.string().optional(), |
| }); |
|
|
| 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, login, apiKey } = 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)); |
|
|
| if (login) { |
| url.searchParams.set("login", login); |
| } |
| if (apiKey) { |
| url.searchParams.set("api_key", apiKey); |
| } |
|
|
| 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" }; |
| } |
| }); |
|
|