export interface ZenTheme { name: string; description: string; image: string; downloadUrl: string; id: string; homepage?: string; readme: string; preferences?: string; isColorTheme: boolean; author: string; version: string; tags: string[]; createdAt: Date; updatedAt: Date; } const THEME_API = "https://zen-browser.github.io/theme-store/themes.json"; const CACHE_OPTIONS: RequestInit = { next: { revalidate: 60, // Revalidate every 60 seconds }, }; /** * Type Guard to validate Date objects. * @param date - The date to validate. * @returns True if valid Date, else false. */ function isValidDate(date: any): date is Date { return date instanceof Date && !isNaN(date.getTime()); } /** * Parses a date string into a Date object. * Assigns a future date if `assignFutureDate` is true and date is invalid/missing. * @param dateString - The date string to parse. * @param assignFutureDate - Whether to assign a future date if parsing fails. * @returns A valid Date object. */ function parseDate( dateString: string | undefined, assignFutureDate: boolean = false, ): Date { const date = new Date(dateString || ""); if (isValidDate(date)) { return date; } else { return assignFutureDate ? new Date(8640000000000000) : new Date(0); // Future date or Unix epoch } } /** * Fetches all mods from the API and transforms them into an array of ZenTheme objects. * Assigns a future date to `createdAt` if it's missing to ensure proper sorting. * @returns A promise that resolves to an array of ZenTheme objects. */ export async function getAllThemes(): Promise { try { const response = await fetch(THEME_API, CACHE_OPTIONS); if (!response.ok) { throw new Error(`Failed to fetch themes: ${response.statusText}`); } const themes = await response.json(); const themesArray: ZenTheme[] = []; for (const key in themes) { if (themes.hasOwnProperty(key)) { const theme = themes[key]; // Remove duplicate tags const uniqueTags: string[] = Array.from(new Set(theme.tags || [])); // Parse dates const createdAt = parseDate(theme.createdAt, true); // Assign future date if missing const updatedAt = parseDate(theme.updatedAt); const zenTheme: ZenTheme = { name: theme.name, description: theme.description, image: theme.image, downloadUrl: theme.style, // Assuming 'style' is the download URL id: theme.id, homepage: theme.homepage, readme: theme.readme, preferences: theme.preferences, isColorTheme: typeof theme.isColorTheme === "boolean" ? theme.isColorTheme : false, author: theme.author, version: theme.version, tags: uniqueTags, createdAt, updatedAt, }; // Validate dates if (!isValidDate(zenTheme.createdAt)) { zenTheme.createdAt = new Date(8640000000000000); // Assign future date } if (!isValidDate(zenTheme.updatedAt)) { zenTheme.updatedAt = new Date(0); // Assign Unix epoch } themesArray.push(zenTheme); } } return themesArray; } catch (error) { console.error("Error fetching or parsing mods:", error); return []; // Return an empty array in case of error } } /** * Searches and sorts mods based on query, tags, and sort criteria. * @param themes - Array of ZenTheme objects. * @param query - Search query string. * @param tags - Array of tags to filter by. * @param sortBy - Criterion to sort by ('name', 'createdAt', 'updatedAt'). * @param createdBefore - Optional Date to filter mods created before this date. * @returns An array of filtered and sorted ZenTheme objects. */ export function getThemesFromSearch( themes: ZenTheme[], query: string, tags: string[], sortBy: string, createdBefore?: Date, ): ZenTheme[] { const normalizedQuery = query.toLowerCase(); return themes .filter((theme) => { const matchesQuery = theme.name.toLowerCase().includes(normalizedQuery); const matchesTag = tags.length === 0 || (theme.tags && tags.some((tag) => theme.tags.includes(tag))); const matchesDate = !createdBefore || theme.createdAt < createdBefore; return matchesQuery && matchesTag && matchesDate; }) .sort((a, b) => { // Sort by number of matching tags first const aMatchCount = tags.filter((tag) => a.tags.includes(tag)).length; const bMatchCount = tags.filter((tag) => b.tags.includes(tag)).length; if (aMatchCount !== bMatchCount) { return bMatchCount - aMatchCount; } // Sort by selected sort method if (sortBy === "name") { return a.name.localeCompare(b.name); } else if (sortBy === "createdAt") { return a.createdAt.getTime() - b.createdAt.getTime(); // Oldest first } else if (sortBy === "updatedAt") { return b.updatedAt.getTime() - a.updatedAt.getTime(); // Newest first } return 0; // Default to no sorting if sortBy is unrecognized }); } /** * Retrieves a theme by its ID. * @param id - The ID of the theme to retrieve. * @returns A promise that resolves to the ZenTheme object or undefined if not found. */ export async function getThemeFromId( id: string, ): Promise { const allThemes = await getAllThemes(); return allThemes.find((theme) => theme.id === id); } /** * Fetches the markdown content of a theme's readme. * @param theme - The ZenTheme object. * @returns A promise that resolves to the readme markdown string. */ export async function getThemeMarkdown(theme: ZenTheme): Promise { try { const response = await fetch(theme.readme, CACHE_OPTIONS); if (!response.ok) { throw new Error(`Failed to fetch README: ${response.statusText}`); } return await response.text(); } catch (error) { console.error("Error fetching README:", error); return ""; // Return an empty string in case of error } } /** * Generates the GitHub link for a theme's author. * @param theme - The ZenTheme object. * @returns A string URL to the author's GitHub profile. */ export function getThemeAuthorLink(theme: ZenTheme): string { return `https://github.com/${theme.author}`; }