diff --git a/src/components/PluginCard.astro b/src/components/PluginCard.astro index efb9c89ff..f08a86d29 100644 --- a/src/components/PluginCard.astro +++ b/src/components/PluginCard.astro @@ -1,17 +1,8 @@ --- -import PluginModal from "./PluginModal.astro"; import { Markdown } from "@astropub/md"; +import PluginModal from "./PluginModal.astro"; import DeprecatedIcon from "./DeprecatedIcon.astro"; - -export interface Plugin { - description: string; - hash: string; - name: string; - author: string[]; - link: string; - example: string; - version: string; -} +import type { Plugin } from "../utils/types"; type Props = Plugin; diff --git a/src/components/PluginModal.astro b/src/components/PluginModal.astro index c91063d39..f093656a0 100644 --- a/src/components/PluginModal.astro +++ b/src/components/PluginModal.astro @@ -1,7 +1,7 @@ --- -import type { Plugin } from "./PluginCard.astro"; import { Code } from "@astrojs/starlight/components"; import { Markdown } from "@astropub/md"; +import type { Plugin } from "../utils/types"; import Modal from "./Modal.astro"; type Props = Plugin; diff --git a/src/components/SponsorCard.astro b/src/components/SponsorCard.astro index c4df3db66..ca7830f69 100644 --- a/src/components/SponsorCard.astro +++ b/src/components/SponsorCard.astro @@ -1,31 +1,7 @@ --- -export interface Sponsor { - id: string; - name: string; - roles: string[]; - isAdmin: boolean; - isCore: boolean; - isBacker: boolean; - since: string; - image: string; - description: string | null; - collectiveSlug: string; - totalAmountDonated: number; - type: string; - publicMessage: string | null; - isIncognito: boolean; - __typename: string; -} +import type { Contributor } from "../utils/types"; -type Props = Pick< - Sponsor, - | "name" - | "image" - | "totalAmountDonated" - | "isAdmin" - | "publicMessage" - | "collectiveSlug" ->; +type Props = Contributor; const { name, diff --git a/src/pages/plugins.astro b/src/pages/plugins.astro index 5967afcfe..11a7642de 100644 --- a/src/pages/plugins.astro +++ b/src/pages/plugins.astro @@ -1,13 +1,22 @@ --- import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; import { CardGrid } from "@astrojs/starlight/components"; -import PluginCard, { type Plugin } from "../components/PluginCard.astro"; +import { z } from "astro/zod"; +import PluginCard from "../components/PluginCard.astro"; +import { PluginSchema } from "../utils/types"; +import { zodFetch } from "../utils/fetch"; -const plugins = (await ( - await fetch( - "https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json", - ) -).json()) as Plugin[]; +const pluginResponse = await zodFetch( + z.array(PluginSchema), + "Failed to fetch plugins", + "https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json", +); + +if (!pluginResponse.ok) { + return console.error(pluginResponse.error); +} + +const plugins = pluginResponse.value; --- diff --git a/src/pages/sponsors.astro b/src/pages/sponsors.astro index 997c57dbe..a8fc967a2 100644 --- a/src/pages/sponsors.astro +++ b/src/pages/sponsors.astro @@ -1,36 +1,41 @@ --- import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; -import type { Sponsor } from "../components/SponsorCard.astro"; +import { z } from "astro/zod"; import SponsorCard from "../components/SponsorCard.astro"; +import { zodFetch } from "../utils/fetch"; +import { OpenCollectiveAccountSchema } from "../utils/types"; -interface SponsorResponse { - data: { - account: { - contributors: { - nodes: Sponsor[]; - }; - }; - }; +const sponsorResponse = await zodFetch( + z.object({ + data: z.object({ + account: OpenCollectiveAccountSchema, + }), + }), + "Failed to fetch sponsors", + "https://opencollective.com/api/graphql/v2", + { + body: JSON.stringify({ + operationName: "BannerTopContributors", + variables: { + collectiveSlug: "sern", + }, + query: + "query BannerTopContributors($collectiveSlug: String!) {\n account(slug: $collectiveSlug, throwIfMissing: false) {\n id\n currency\n slug\n ... on AccountWithContributions {\n contributors(limit: 150) {\n totalCount\n nodes {\n id\n name\n roles\n isAdmin\n isCore\n isBacker\n since\n image\n description\n collectiveSlug\n totalAmountDonated\n type\n publicMessage\n isIncognito\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}", + }), + method: "POST", + headers: { + "content-type": "application/json", + }, + }, +); + +if (!sponsorResponse.ok) { + return console.error(sponsorResponse.error); } -const sponsors = ( - (await ( - await fetch("https://opencollective.com/api/graphql/v2", { - body: JSON.stringify({ - operationName: "BannerTopContributors", - variables: { - collectiveSlug: "sern", - }, - query: - "query BannerTopContributors($collectiveSlug: String!) {\n account(slug: $collectiveSlug, throwIfMissing: false) {\n id\n currency\n slug\n ... on AccountWithContributions {\n contributors(limit: 150) {\n totalCount\n nodes {\n id\n name\n roles\n isAdmin\n isCore\n isBacker\n since\n image\n description\n collectiveSlug\n totalAmountDonated\n type\n publicMessage\n isIncognito\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}", - }), - method: "POST", - headers: { - "content-type": "application/json", - }, - }) - ).json()) as SponsorResponse -).data.account.contributors.nodes.filter((s) => s.totalAmountDonated > 0); +const sponsors = sponsorResponse.value.data.account.contributors.nodes.filter( + (s) => s.totalAmountDonated > 0, +); --- diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 000000000..4b1a25b30 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,40 @@ +import type { z } from "astro/zod"; +import { Ok, Err } from "./types"; + +/** + * @param schema The Zod schema to validate the response against + * @param error An error message to return if the response is invalid (e.g. "Failed to fetch data") + * @param args The arguments to pass to fetch (e.g. URL, headers, etc.) + * @returns A Result type containing either the parsed JSON or an error message (specified by the error parameter) + * + * @example + * const plugins = await zodFetch(PluginSchema, "Failed to fetch plugins", "/api/plugins"); + * if (!plugins.ok) { + * console.error(plugins.error); // "Failed to fetch plugins" + * } + * console.log(plugins.value); // { description: "A plugin", hash: "123", name: "My Plugin", author: ["Me"], link: "https://example.com", example: "example" } + */ +export const zodFetch = async ( + schema: TSchema, + error: string, + ...args: Parameters +) => { + const res = await fetch(...args); + + if (!res.ok) { + console.error(await res.text()); + return Err(error); + } + + const json = (await res.json()) as unknown; + const parsed = schema.safeParse(json); + + if (!parsed.success) { + console.error( + parsed.error.issues.map((i) => `${i.code} | ${i.message}`).join("\n"), + ); + return Err(error); + } + + return Ok(json as z.infer); +}; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 000000000..12d986ea9 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,53 @@ +import { z } from "astro/zod"; + +export type Result = + | { ok: true; value: Ok } + | { ok: false; error: Err }; +export const Ok = (value: Ok) => ({ ok: true, value } as const); +export const Err = (error: Err) => ({ ok: false, error } as const); + +export type Contributor = z.infer; +export const ContributorSchema = z.object({ + id: z.string(), + name: z.string(), + roles: z.array(z.string()), + isAdmin: z.boolean(), + isCore: z.boolean(), + isBacker: z.boolean(), + since: z.string(), + image: z.string(), + description: z.string().nullable(), + collectiveSlug: z.string(), + totalAmountDonated: z.number(), + type: z.string(), + publicMessage: z.string().nullable(), + isIncognito: z.boolean(), + __typename: z.string(), +}); + +export type Contributors = z.infer; +export const ContributorsSchema = z.object({ + totalCount: z.number(), + nodes: z.array(ContributorSchema), + __typename: z.string(), +}); + +export type OpenCollectiveAccount = z.infer; +export const OpenCollectiveAccountSchema = z.object({ + id: z.string(), + currency: z.string(), + slug: z.string(), + contributors: ContributorsSchema, + __typename: z.string(), +}); + +export type Plugin = z.infer; +export const PluginSchema = z.object({ + description: z.string(), + hash: z.string(), + name: z.string(), + author: z.array(z.string()), + link: z.string().url(), + example: z.string(), + version: z.string(), +});