summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/iconify-api.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-08-15 02:12:16 +0200
committerMaze Winther <mazewinther@gmail.com>2025-08-15 02:12:16 +0200
commit7f91c9ca00cd51bea1cd1e97eeb3c58b7e7a1bae (patch)
tree25c6cddf67e3227d99c2c28f71496851962f1f45 /apps/web/src/lib/iconify-api.ts
parent5e8a489a147c148b6495a6a25fb4dd9725e6fca6 (diff)
parentc3f3345d7b1ac820b411a5adbca3172378642953 (diff)
downloadopencut-7f91c9ca00cd51bea1cd1e97eeb3c58b7e7a1bae.tar.gz
Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging
Diffstat (limited to 'apps/web/src/lib/iconify-api.ts')
-rw-r--r--apps/web/src/lib/iconify-api.ts242
1 files changed, 242 insertions, 0 deletions
diff --git a/apps/web/src/lib/iconify-api.ts b/apps/web/src/lib/iconify-api.ts
new file mode 100644
index 00000000..9d1bcb97
--- /dev/null
+++ b/apps/web/src/lib/iconify-api.ts
@@ -0,0 +1,242 @@
+
+export const ICONIFY_HOSTS = [
+ "https://api.iconify.design",
+ "https://api.simplesvg.com",
+ "https://api.unisvg.com",
+];
+
+let currentHost = ICONIFY_HOSTS[0];
+
+async function fetchWithFallback(path: string): Promise<Response> {
+ for (const host of ICONIFY_HOSTS) {
+ try {
+ const response = await fetch(`${host}${path}`, {
+ signal: AbortSignal.timeout(2000),
+ });
+ if (response.ok) {
+ currentHost = host;
+ return response;
+ }
+ } catch (error) {
+ console.warn(`Failed to fetch from ${host}:`, error);
+ }
+ }
+ throw new Error("All API hosts failed");
+}
+
+export interface IconSet {
+ prefix: string;
+ name: string;
+ total: number;
+ author?: {
+ name: string;
+ url?: string;
+ };
+ license?: {
+ title: string;
+ spdx?: string;
+ url?: string;
+ };
+ samples?: string[];
+ category?: string;
+ palette?: boolean;
+}
+
+export interface IconSearchResult {
+ icons: string[];
+ total: number;
+ limit: number;
+ start: number;
+ collections: Record<string, IconSet>;
+}
+
+export interface CollectionInfo {
+ prefix: string;
+ total: number;
+ title?: string;
+ uncategorized?: string[];
+ categories?: Record<string, string[]>;
+ hidden?: string[];
+ aliases?: Record<string, string>;
+}
+
+export async function getCollections(
+ category?: string
+): Promise<Record<string, IconSet>> {
+ try {
+ const response = await fetchWithFallback("/collections?pretty=1");
+ const data = (await response.json()) as Record<string, IconSet>;
+
+ if (category) {
+ const filtered = Object.fromEntries(
+ Object.entries(data).filter(
+ ([_key, info]) => info.category === category
+ )
+ ) as Record<string, IconSet>;
+ return filtered;
+ }
+
+ return data;
+ } catch (error) {
+ console.error("Failed to fetch collections:", error);
+ return {};
+ }
+}
+
+export async function getCollection(
+ prefix: string
+): Promise<CollectionInfo | null> {
+ try {
+ const response = await fetchWithFallback(
+ `/collection?prefix=${prefix}&pretty=1`
+ );
+ return await response.json();
+ } catch (error) {
+ console.error(`Failed to fetch collection ${prefix}:`, error);
+ return null;
+ }
+}
+
+export async function searchIcons(
+ query: string,
+ limit: number = 64,
+ prefixes?: string[],
+ category?: string
+): Promise<IconSearchResult> {
+ const params = new URLSearchParams({
+ query,
+ limit: limit.toString(),
+ pretty: "1",
+ });
+
+ if (prefixes?.length) {
+ params.append("prefixes", prefixes.join(","));
+ }
+
+ if (category) {
+ params.append("category", category);
+ }
+
+ try {
+ const response = await fetchWithFallback(`/search?${params}`);
+ return await response.json();
+ } catch (error) {
+ console.error("Failed to search icons:", error);
+ return {
+ icons: [],
+ total: 0,
+ limit,
+ start: 0,
+ collections: {},
+ };
+ }
+}
+
+export function buildIconSvgUrl(
+ host: string,
+ iconName: string,
+ params?: {
+ color?: string;
+ width?: number;
+ height?: number;
+ flip?: "horizontal" | "vertical" | "horizontal,vertical";
+ rotate?: number | string;
+ }
+): string {
+ const [prefix, name] = iconName.includes(":")
+ ? iconName.split(":")
+ : ["", iconName];
+
+ if (!prefix || !name) {
+ throw new Error('Invalid icon name format. Expected "prefix:name"');
+ }
+
+ const urlParams = new URLSearchParams();
+
+ if (params?.color) {
+ urlParams.append("color", params.color.replace("#", "%23"));
+ }
+
+ if (params?.width) {
+ urlParams.append("width", params.width.toString());
+ }
+
+ if (params?.height) {
+ urlParams.append("height", params.height.toString());
+ }
+
+ if (params?.flip) {
+ urlParams.append("flip", params.flip);
+ }
+
+ if (params?.rotate) {
+ urlParams.append("rotate", params.rotate.toString());
+ }
+
+ const queryString = urlParams.toString();
+ return `${host}/${prefix}/${name}.svg${queryString ? "?" + queryString : ""}`;
+}
+
+export function getIconSvgUrl(
+ iconName: string,
+ params?: Parameters<typeof buildIconSvgUrl>[2]
+): string {
+ return buildIconSvgUrl(currentHost, iconName, params);
+}
+
+export async function downloadSvgAsText(
+ iconName: string,
+ params?: Parameters<typeof getIconSvgUrl>[1]
+): Promise<string> {
+ const url = getIconSvgUrl(iconName, params);
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to download SVG: ${response.statusText}`);
+ }
+ return await response.text();
+}
+
+export function svgToFile(svgText: string, fileName: string): File {
+ const blob = new Blob([svgText], { type: "image/svg+xml" });
+ return new File([blob], fileName, { type: "image/svg+xml" });
+}
+
+export const POPULAR_COLLECTIONS = {
+ general: [
+ { prefix: "mdi", name: "Material Design Icons" },
+ { prefix: "ic", name: "Google Material Icons" },
+ { prefix: "ph", name: "Phosphor" },
+ { prefix: "heroicons", name: "Heroicons" },
+ { prefix: "lucide", name: "Lucide" },
+ { prefix: "tabler", name: "Tabler Icons" },
+ { prefix: "fe", name: "Feather Icons" },
+ { prefix: "bi", name: "Bootstrap Icons" },
+ ],
+ brands: [
+ { prefix: "simple-icons", name: "Simple Icons" },
+ { prefix: "logos", name: "SVG Logos" },
+ { prefix: "skill-icons", name: "Skill Icons" },
+ { prefix: "devicon", name: "Devicon" },
+ { prefix: "fa-brands", name: "Font Awesome Brands" },
+ ],
+ emoji: [
+ { prefix: "noto", name: "Noto Emoji" },
+ { prefix: "twemoji", name: "Twemoji" },
+ { prefix: "fluent-emoji", name: "Fluent Emoji" },
+ { prefix: "fluent-emoji-flat", name: "Fluent Emoji Flat" },
+ { prefix: "emojione", name: "EmojiOne" },
+ { prefix: "openmoji", name: "OpenMoji" },
+ ],
+};
+
+export function getCategoriesFromCollections(
+ collections: Record<string, IconSet>
+): string[] {
+ const categories = new Set<string>();
+ Object.values(collections).forEach((collection) => {
+ if (collection.category) {
+ categories.add(collection.category);
+ }
+ });
+ return Array.from(categories).sort();
+}