File size: 2,637 Bytes
fa7b380
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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" };
    }
  });