diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/lib/stickers | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/lib/stickers')
| -rw-r--r-- | apps/web/src/lib/stickers/__tests__/sticker-id.test.ts | 14 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/index.ts | 230 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/countries-data.ts | 1819 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/emoji.ts | 119 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/flags.ts | 97 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/icons.ts | 122 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/index.ts | 14 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/logos.ts | 31 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/providers/shapes.ts | 160 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/registry.ts | 33 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/resolver.ts | 8 | ||||
| -rw-r--r-- | apps/web/src/lib/stickers/types.ts | 79 |
12 files changed, 2319 insertions, 407 deletions
diff --git a/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts b/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts index 573dc251..3236e213 100644 --- a/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts +++ b/apps/web/src/lib/stickers/__tests__/sticker-id.test.ts @@ -3,13 +3,13 @@ import { buildStickerId, parseStickerId } from "../sticker-id"; describe("sticker-id strict mode", () => { test("parses provider-prefixed IDs", () => { - expect(parseStickerId({ stickerId: "icons:mdi:home" })).toEqual({ - providerId: "icons", - providerValue: "mdi:home", + expect(parseStickerId({ stickerId: "flags:US" })).toEqual({ + providerId: "flags", + providerValue: "US", }); - expect(parseStickerId({ stickerId: "emoji:noto:grinning-face" })).toEqual({ - providerId: "emoji", - providerValue: "noto:grinning-face", + expect(parseStickerId({ stickerId: "shapes:circle" })).toEqual({ + providerId: "shapes", + providerValue: "circle", }); }); @@ -19,7 +19,7 @@ describe("sticker-id strict mode", () => { test("throws for malformed IDs", () => { expect(() => parseStickerId({ stickerId: "" })).toThrow(); - expect(() => parseStickerId({ stickerId: "icons:" })).toThrow(); + expect(() => parseStickerId({ stickerId: "flags:" })).toThrow(); expect(() => parseStickerId({ stickerId: ":mdi:home" })).toThrow(); }); diff --git a/apps/web/src/lib/stickers/index.ts b/apps/web/src/lib/stickers/index.ts index 1f924c6d..47be18fe 100644 --- a/apps/web/src/lib/stickers/index.ts +++ b/apps/web/src/lib/stickers/index.ts @@ -1,10 +1,20 @@ -import { STICKER_CATEGORIES } from "@/constants/sticker-constants"; -import type { StickerCategory } from "@/types/stickers"; -import { getAllProviders, getProvider } from "./registry"; +import { + STICKER_CATEGORIES, + STICKER_INTRINSIC_SIZE_FALLBACK, +} from "@/constants/sticker-constants"; +import type { StickerCategory } from "@/lib/stickers/types"; +import { stickersRegistry } from "./registry"; import { resolveStickerId } from "./resolver"; import { registerDefaultStickerProviders } from "./providers"; -import type { StickerProvider, StickerSearchResult } from "./types"; +import { parseStickerId } from "./sticker-id"; +import type { + StickerBrowseResult, + StickerItem, + StickerProvider, + StickerSearchResult, +} from "./types"; +const DEFAULT_BROWSE_LIMIT = 12; const DEFAULT_SEARCH_LIMIT = 100; function mergeSearchResults({ @@ -46,7 +56,46 @@ function getProviderByCategory({ } try { - return getProvider({ providerId: category }); + return stickersRegistry.get(category); + } catch { + return null; + } +} + +function getEmptyBrowseResult(): StickerBrowseResult { + return { + sections: [], + }; +} + +function getStickerNameFromId({ stickerId }: { stickerId: string }): string { + const stickerIdParts = stickerId.split(":"); + if (stickerIdParts.length <= 1) { + return stickerId; + } + return ( + stickerIdParts.slice(1).join(":").split(":").pop()?.replaceAll("-", " ") ?? + stickerId + ); +} + +function toRecentStickerItem({ + stickerId, +}: { + stickerId: string; +}): StickerItem | null { + try { + const { providerId } = parseStickerId({ stickerId }); + return { + id: stickerId, + provider: providerId, + name: getStickerNameFromId({ stickerId }), + previewUrl: resolveStickerId({ + stickerId, + options: { width: 64, height: 64 }, + }), + metadata: {}, + }; } catch { return null; } @@ -79,7 +128,7 @@ export async function searchStickers({ }); } - const providers = getAllProviders(); + const providers = stickersRegistry.getAll(); if (providers.length === 0) { return { items: [], @@ -110,64 +159,157 @@ export async function searchStickers({ }); } -export async function browseStickers({ - category, - page = 1, +export async function searchAll({ + query, limit = DEFAULT_SEARCH_LIMIT, }: { - category: StickerCategory; - page?: number; + query: string; limit?: number; -}): Promise<StickerSearchResult> { +}): Promise<StickerBrowseResult> { registerDefaultStickerProviders({}); - const effectiveCategory = category in STICKER_CATEGORIES ? category : "all"; - if (effectiveCategory !== "all") { - const provider = getProviderByCategory({ category: effectiveCategory }); - if (!provider) { - return { - items: [], - total: 0, - hasMore: false, - }; + const providers = stickersRegistry.getAll(); + if (providers.length === 0) { + return { sections: [] }; + } + + const perProviderLimit = Math.max(1, Math.ceil(limit / providers.length)); + const settledResults = await Promise.allSettled( + providers.map(async (provider) => { + const result = await provider.search({ + query, + options: { limit: perProviderLimit }, + }); + return { provider, result }; + }), + ); + + const sections: StickerBrowseResult["sections"] = []; + for (const settled of settledResults) { + if (settled.status !== "fulfilled") { + continue; } - return provider.browse({ - options: { page, limit }, + const { provider, result } = settled.value; + if (result.items.length === 0) { + continue; + } + const category = provider.id as StickerCategory; + sections.push({ + id: category, + title: STICKER_CATEGORIES[category] ?? provider.id, + items: result.items, + hasMore: result.hasMore, + layout: "grid", }); } - const providers = getAllProviders(); - if (providers.length === 0) { - return { - items: [], - total: 0, - hasMore: false, - }; + return { sections }; +} + +export async function browseCategory({ + category, +}: { + category: StickerCategory; +}): Promise<StickerBrowseResult> { + registerDefaultStickerProviders({}); + + const effectiveCategory = category in STICKER_CATEGORIES ? category : "all"; + if (effectiveCategory === "all") { + return getEmptyBrowseResult(); + } + + const provider = getProviderByCategory({ category: effectiveCategory }); + if (!provider) { + return getEmptyBrowseResult(); + } + + return provider.browse({ options: {} }); +} + +export async function browseAll({ + recentStickers, + limit = DEFAULT_BROWSE_LIMIT, +}: { + recentStickers: string[]; + limit?: number; +}): Promise<StickerBrowseResult> { + registerDefaultStickerProviders({}); + + const sections: StickerBrowseResult["sections"] = []; + const recentItems = recentStickers + .map((stickerId) => toRecentStickerItem({ stickerId })) + .filter((item): item is StickerItem => item !== null); + + if (recentItems.length > 0) { + sections.push({ + id: "recent", + title: "Recently used", + items: recentItems.slice(0, limit), + hasMore: recentItems.length > limit, + layout: "row", + }); } - const perProviderLimit = Math.max(1, Math.ceil(limit / providers.length)); const settledResults = await Promise.allSettled( - providers.map((provider) => - provider.browse({ - options: { page, limit: perProviderLimit }, - }), - ), + stickersRegistry.getAll().map(async (provider) => { + const browseResult = await provider.browse({ + options: { limit }, + }); + const firstSection = browseResult.sections[0]; + + if (!firstSection || firstSection.items.length === 0) { + return null; + } + + const category = provider.id as StickerCategory; + return { + ...firstSection, + id: category, + title: STICKER_CATEGORIES[category] ?? firstSection.title, + layout: "row" as const, + action: { + type: "see-all" as const, + category, + sectionId: firstSection.id, + }, + }; + }), ); - const fulfilledResults = settledResults - .filter( - (result): result is PromiseFulfilledResult<StickerSearchResult> => - result.status === "fulfilled", - ) - .map((result) => result.value); + for (const result of settledResults) { + if (result.status === "fulfilled" && result.value) { + sections.push(result.value); + } + } - return mergeSearchResults({ - results: fulfilledResults, + return { sections }; +} + +export async function resolveStickerIntrinsicSize({ + stickerId, +}: { + stickerId: string; +}): Promise<{ width: number; height: number }> { + const url = resolveStickerId({ stickerId }); + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => + resolve({ width: img.naturalWidth, height: img.naturalHeight }); + img.onerror = () => + resolve({ + width: STICKER_INTRINSIC_SIZE_FALLBACK, + height: STICKER_INTRINSIC_SIZE_FALLBACK, + }); + img.src = url; }); } export { resolveStickerId }; +export { resolveQueryToRegions, getRegionLabel } from "./providers/flags"; export type { + StickerBrowseResult, + StickerBrowseSection, + StickerCategory, StickerItem, StickerProvider, StickerResolveOptions, diff --git a/apps/web/src/lib/stickers/providers/countries-data.ts b/apps/web/src/lib/stickers/providers/countries-data.ts new file mode 100644 index 00000000..1bcc1f50 --- /dev/null +++ b/apps/web/src/lib/stickers/providers/countries-data.ts @@ -0,0 +1,1819 @@ +export const REGIONS = [ + { id: "Western Europe", aliases: ["west europe"] }, + { id: "Eastern Europe", aliases: ["east europe"] }, + { id: "Northern Europe", aliases: ["north europe"] }, + { id: "Southern Europe", aliases: ["south europe"] }, + { id: "South Asia", aliases: ["southern asia"] }, + { id: "Southeast Asia", aliases: ["south east asia"] }, + { id: "East Asia", aliases: ["eastern asia", "far east"] }, + { id: "Central Asia", aliases: [] }, + { id: "Middle East", aliases: ["west asia"] }, + { id: "North Africa", aliases: ["northern africa"] }, + { id: "Sub-Saharan Africa", aliases: ["subsaharan africa"] }, + { id: "North America", aliases: ["northern america"] }, + { id: "South America", aliases: ["southern america", "latin america"] }, + { id: "Central America", aliases: ["central am"] }, + { id: "Caribbean", aliases: ["caribbean islands"] }, + { id: "Oceania", aliases: ["pacific", "pacific islands"] }, + { id: "Antarctica", aliases: [] }, + { id: "Atlantic Ocean", aliases: ["atlantic"] }, + { id: "North Atlantic", aliases: [] }, +] as const; + +export type RegionId = (typeof REGIONS)[number]["id"]; + +export const REGION_GROUPS: Partial<Record<string, RegionId[]>> = { + europe: ["Western Europe", "Eastern Europe", "Northern Europe", "Southern Europe"], + asia: ["South Asia", "Southeast Asia", "East Asia", "Central Asia"], + africa: ["Sub-Saharan Africa", "North Africa"], + america: ["North America", "South America", "Central America", "Caribbean"], +}; + +export interface CountryRecord { + name: string; + code: string; + languages?: string[]; + flag_colors?: string[]; + region?: RegionId; +} + +export const COUNTRIES: CountryRecord[] = [ + { + "name": "Andorra", + "code": "AD", + "languages": ["catalan"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Western Europe" + }, + { + "name": "United Arab Emirates", + "code": "AE", + "languages": ["arabic"], + "flag_colors": ["red", "green", "white", "black"], + "region": "Middle East" + }, + { + "name": "Afghanistan", + "code": "AF", + "languages": ["dari", "pashto"], + "flag_colors": ["black", "red", "green"], + "region": "South Asia" + }, + { + "name": "Antigua and Barbuda", + "code": "AG", + "languages": ["english"], + "flag_colors": ["red", "black", "blue", "white", "yellow"], + "region": "Caribbean" + }, + { + "name": "Anguilla", + "code": "AI", + "languages": ["english"], + "flag_colors": ["blue", "white", "orange"], + "region": "Caribbean" + }, + { + "name": "Albania", + "code": "AL", + "languages": ["albanian"], + "flag_colors": ["red", "black"], + "region": "Southern Europe" + }, + { + "name": "Armenia", + "code": "AM", + "languages": ["armenian"], + "flag_colors": ["red", "blue", "orange"], + "region": "Middle East" + }, + { + "name": "Angola", + "code": "AO", + "languages": ["portuguese"], + "flag_colors": ["red", "black", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Antarctica", + "code": "AQ", + "languages": [], + "flag_colors": [], + "region": "Antarctica" + }, + { + "name": "Argentina", + "code": "AR", + "languages": ["spanish"], + "flag_colors": ["blue", "white", "yellow"], + "region": "South America" + }, + { + "name": "American Samoa", + "code": "AS", + "languages": ["english", "samoan"], + "flag_colors": ["red", "white", "blue"], + "region": "Oceania" + }, + { + "name": "Austria", + "code": "AT", + "languages": ["german"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Australia", + "code": "AU", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "Aruba", + "code": "AW", + "languages": ["dutch", "papiamento"], + "flag_colors": ["blue", "yellow", "red", "white"], + "region": "Caribbean" + }, + { + "name": "Aland Islands", + "code": "AX", + "languages": ["swedish"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Northern Europe" + }, + { + "name": "Azerbaijan", + "code": "AZ", + "languages": ["azerbaijani"], + "flag_colors": ["blue", "red", "green", "white"], + "region": "Middle East" + }, + { + "name": "Bosnia and Herzegovina", + "code": "BA", + "languages": ["bosnian", "croatian", "serbian"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Southern Europe" + }, + { + "name": "Barbados", + "code": "BB", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "black"], + "region": "Caribbean" + }, + { + "name": "Bangladesh", + "code": "BD", + "languages": ["bengali"], + "flag_colors": ["green", "red"], + "region": "South Asia" + }, + { + "name": "Belgium", + "code": "BE", + "languages": ["french", "dutch", "german"], + "flag_colors": ["black", "yellow", "red"], + "region": "Western Europe" + }, + { + "name": "Burkina Faso", + "code": "BF", + "languages": ["french"], + "flag_colors": ["red", "green", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Bulgaria", + "code": "BG", + "languages": ["bulgarian"], + "flag_colors": ["white", "green", "red"], + "region": "Eastern Europe" + }, + { + "name": "Bahrain", + "code": "BH", + "languages": ["arabic"], + "flag_colors": ["red", "white"], + "region": "Middle East" + }, + { + "name": "Burundi", + "code": "BI", + "languages": ["kirundi", "french", "english"], + "flag_colors": ["red", "green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Benin", + "code": "BJ", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Saint Barthelemy", + "code": "BL", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Bermuda", + "code": "BM", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "North Atlantic" + }, + { + "name": "Brunei", + "code": "BN", + "languages": ["malay"], + "flag_colors": ["yellow", "white", "black", "red"], + "region": "Southeast Asia" + }, + { + "name": "Bolivia", + "code": "BO", + "languages": ["spanish", "quechua", "aymara"], + "flag_colors": ["red", "yellow", "green"], + "region": "South America" + }, + { + "name": "Caribbean Netherlands", + "code": "BQ", + "languages": ["dutch", "papiamento", "english"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Brazil", + "code": "BR", + "languages": ["portuguese"], + "flag_colors": ["green", "yellow", "blue", "white"], + "region": "South America" + }, + { + "name": "Bahamas", + "code": "BS", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "black"], + "region": "Caribbean" + }, + { + "name": "Bhutan", + "code": "BT", + "languages": ["dzongkha"], + "flag_colors": ["yellow", "orange", "white"], + "region": "South Asia" + }, + { + "name": "Bouvet Island", + "code": "BV", + "languages": [], + "flag_colors": ["red", "white", "blue"], + "region": "Antarctica" + }, + { + "name": "Botswana", + "code": "BW", + "languages": ["english", "tswana"], + "flag_colors": ["blue", "black", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Belarus", + "code": "BY", + "languages": ["belarusian", "russian"], + "flag_colors": ["red", "green", "white"], + "region": "Eastern Europe" + }, + { + "name": "Belize", + "code": "BZ", + "languages": ["english"], + "flag_colors": ["blue", "red", "white"], + "region": "Central America" + }, + { + "name": "Canada", + "code": "CA", + "languages": ["english", "french"], + "flag_colors": ["red", "white"], + "region": "North America" + }, + { + "name": "Cocos (Keeling) Islands", + "code": "CC", + "languages": ["english"], + "flag_colors": ["green", "red", "yellow", "white"], + "region": "Southeast Asia" + }, + { + "name": "Democratic Republic of the Congo", + "code": "CD", + "languages": ["french", "lingala", "swahili", "tshiluba", "kikongo"], + "flag_colors": ["blue", "red", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Central African Republic", + "code": "CF", + "languages": ["french", "sango"], + "flag_colors": ["blue", "white", "green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Republic of the Congo", + "code": "CG", + "languages": ["french", "lingala", "kituba"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Switzerland", + "code": "CH", + "languages": ["german", "french", "italian"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Cote d'Ivoire", + "code": "CI", + "languages": ["french"], + "flag_colors": ["orange", "white", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Cook Islands", + "code": "CK", + "languages": ["english", "cook islands maori"], + "flag_colors": ["blue", "white"], + "region": "Oceania" + }, + { + "name": "Chile", + "code": "CL", + "languages": ["spanish"], + "flag_colors": ["white", "red", "blue"], + "region": "South America" + }, + { + "name": "Cameroon", + "code": "CM", + "languages": ["english", "french"], + "flag_colors": ["green", "red", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "China", + "code": "CN", + "languages": ["mandarin"], + "flag_colors": ["red", "yellow"], + "region": "East Asia" + }, + { + "name": "Colombia", + "code": "CO", + "languages": ["spanish"], + "flag_colors": ["yellow", "blue", "red"], + "region": "South America" + }, + { + "name": "Costa Rica", + "code": "CR", + "languages": ["spanish"], + "flag_colors": ["blue", "white", "red"], + "region": "Central America" + }, + { + "name": "Cuba", + "code": "CU", + "languages": ["spanish"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Cape Verde", + "code": "CV", + "languages": ["portuguese"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Curacao", + "code": "CW", + "languages": ["dutch", "papiamento", "english"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Caribbean" + }, + { + "name": "Christmas Island", + "code": "CX", + "languages": ["english"], + "flag_colors": ["green", "blue", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Cyprus", + "code": "CY", + "languages": ["greek", "turkish"], + "flag_colors": ["white", "orange", "green"], + "region": "Middle East" + }, + { + "name": "Czechia", + "code": "CZ", + "languages": ["czech"], + "flag_colors": ["white", "red", "blue"], + "region": "Eastern Europe" + }, + { + "name": "Germany", + "code": "DE", + "languages": ["german"], + "flag_colors": ["black", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Djibouti", + "code": "DJ", + "languages": ["arabic", "french", "afar", "somali"], + "flag_colors": ["blue", "green", "white", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Denmark", + "code": "DK", + "languages": ["danish"], + "flag_colors": ["red", "white"], + "region": "Northern Europe" + }, + { + "name": "Dominica", + "code": "DM", + "languages": ["english"], + "flag_colors": ["green", "yellow", "black", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Dominican Republic", + "code": "DO", + "languages": ["spanish"], + "flag_colors": ["red", "white", "blue"], + "region": "Caribbean" + }, + { + "name": "Algeria", + "code": "DZ", + "languages": ["arabic", "tamazight"], + "flag_colors": ["green", "white", "red"], + "region": "North Africa" + }, + { + "name": "Ecuador", + "code": "EC", + "languages": ["spanish"], + "flag_colors": ["yellow", "blue", "red"], + "region": "South America" + }, + { + "name": "Estonia", + "code": "EE", + "languages": ["estonian"], + "flag_colors": ["blue", "black", "white"], + "region": "Northern Europe" + }, + { + "name": "Egypt", + "code": "EG", + "languages": ["arabic"], + "flag_colors": ["red", "white", "black", "yellow"], + "region": "North Africa" + }, + { + "name": "Western Sahara", + "code": "EH", + "languages": ["arabic", "spanish"], + "flag_colors": ["black", "white", "green", "red"], + "region": "North Africa" + }, + { + "name": "Eritrea", + "code": "ER", + "languages": ["tigrinya", "arabic", "english"], + "flag_colors": ["red", "green", "blue", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Spain", + "code": "ES", + "languages": ["spanish"], + "flag_colors": ["red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Ethiopia", + "code": "ET", + "languages": ["amharic", "oromo"], + "flag_colors": ["green", "yellow", "red", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "European Union", + "code": "EU", + "languages": ["english", "french", "german"], + "flag_colors": ["blue", "yellow"], + "region": "Western Europe" + }, + { + "name": "Finland", + "code": "FI", + "languages": ["finnish", "swedish"], + "flag_colors": ["white", "blue"], + "region": "Northern Europe" + }, + { + "name": "Fiji", + "code": "FJ", + "languages": ["english", "fijian", "hindi"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "Oceania" + }, + { + "name": "Falkland Islands", + "code": "FK", + "languages": ["english"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "South America" + }, + { + "name": "Micronesia", + "code": "FM", + "languages": ["english"], + "flag_colors": ["blue", "white"], + "region": "Oceania" + }, + { + "name": "Faroe Islands", + "code": "FO", + "languages": ["faroese", "danish"], + "flag_colors": ["white", "red", "blue"], + "region": "Northern Europe" + }, + { + "name": "France", + "code": "FR", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Western Europe" + }, + { + "name": "Gabon", + "code": "GA", + "languages": ["french"], + "flag_colors": ["green", "yellow", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "England", + "code": "GB-ENG", + "languages": ["english"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Northern Ireland", + "code": "GB-NIR", + "languages": ["english", "irish"], + "flag_colors": ["white", "red"], + "region": "Western Europe" + }, + { + "name": "Scotland", + "code": "GB-SCT", + "languages": ["english", "scottish gaelic"], + "flag_colors": ["blue", "white"], + "region": "Western Europe" + }, + { + "name": "Wales", + "code": "GB-WLS", + "languages": ["english", "welsh"], + "flag_colors": ["green", "white", "red"], + "region": "Western Europe" + }, + { + "name": "United Kingdom", + "code": "GB", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "Western Europe" + }, + { + "name": "Grenada", + "code": "GD", + "languages": ["english"], + "flag_colors": ["red", "yellow", "green", "black"], + "region": "Caribbean" + }, + { + "name": "Georgia", + "code": "GE", + "languages": ["georgian"], + "flag_colors": ["white", "red"], + "region": "Middle East" + }, + { + "name": "French Guiana", + "code": "GF", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "South America" + }, + { + "name": "Guernsey", + "code": "GG", + "languages": ["english", "french"], + "flag_colors": ["white", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Ghana", + "code": "GH", + "languages": ["english"], + "flag_colors": ["red", "yellow", "green", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Gibraltar", + "code": "GI", + "languages": ["english"], + "flag_colors": ["red", "white", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Greenland", + "code": "GL", + "languages": ["kalaallisut", "danish"], + "flag_colors": ["red", "white"], + "region": "North America" + }, + { + "name": "Gambia", + "code": "GM", + "languages": ["english"], + "flag_colors": ["red", "blue", "green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Guinea", + "code": "GN", + "languages": ["french"], + "flag_colors": ["red", "yellow", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Guadeloupe", + "code": "GP", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Equatorial Guinea", + "code": "GQ", + "languages": ["spanish", "french", "portuguese"], + "flag_colors": ["green", "white", "red", "blue", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Greece", + "code": "GR", + "languages": ["greek"], + "flag_colors": ["blue", "white"], + "region": "Southern Europe" + }, + { + "name": "South Georgia and the South Sandwich Islands", + "code": "GS", + "languages": [], + "flag_colors": ["blue", "white", "yellow"], + "region": "Atlantic Ocean" + }, + { + "name": "Guatemala", + "code": "GT", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Guam", + "code": "GU", + "languages": ["english", "chamorro"], + "flag_colors": ["blue", "red"], + "region": "Oceania" + }, + { + "name": "Guinea-Bissau", + "code": "GW", + "languages": ["portuguese"], + "flag_colors": ["red", "yellow", "green", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Guyana", + "code": "GY", + "languages": ["english"], + "flag_colors": ["green", "yellow", "red", "black", "white"], + "region": "South America" + }, + { + "name": "Hong Kong", + "code": "HK", + "languages": ["cantonese", "english"], + "flag_colors": ["red", "white"], + "region": "East Asia" + }, + { + "name": "Heard Island and McDonald Islands", + "code": "HM", + "languages": [], + "flag_colors": ["blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "Honduras", + "code": "HN", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Croatia", + "code": "HR", + "languages": ["croatian"], + "flag_colors": ["red", "white", "blue"], + "region": "Southern Europe" + }, + { + "name": "Haiti", + "code": "HT", + "languages": ["haitian creole", "french"], + "flag_colors": ["blue", "red", "white"], + "region": "Caribbean" + }, + { + "name": "Hungary", + "code": "HU", + "languages": ["hungarian"], + "flag_colors": ["red", "white", "green"], + "region": "Eastern Europe" + }, + { + "name": "Indonesia", + "code": "ID", + "languages": ["indonesian"], + "flag_colors": ["red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Ireland", + "code": "IE", + "languages": ["english", "irish"], + "flag_colors": ["green", "white", "orange"], + "region": "Western Europe" + }, + { + "name": "Israel", + "code": "IL", + "languages": ["hebrew", "arabic"], + "flag_colors": ["blue", "white"], + "region": "Middle East" + }, + { + "name": "Isle of Man", + "code": "IM", + "languages": ["english", "manx"], + "flag_colors": ["red", "yellow"], + "region": "Western Europe" + }, + { + "name": "India", + "code": "IN", + "languages": ["hindi", "english"], + "flag_colors": ["orange", "white", "green", "blue"], + "region": "South Asia" + }, + { + "name": "British Indian Ocean Territory", + "code": "IO", + "languages": ["english"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "South Asia" + }, + { + "name": "Iraq", + "code": "IQ", + "languages": ["arabic", "kurdish"], + "flag_colors": ["red", "white", "black", "green"], + "region": "Middle East" + }, + { + "name": "Iran", + "code": "IR", + "languages": ["persian"], + "flag_colors": ["green", "white", "red"], + "region": "Middle East" + }, + { + "name": "Iceland", + "code": "IS", + "languages": ["icelandic"], + "flag_colors": ["blue", "white", "red"], + "region": "Northern Europe" + }, + { + "name": "Italy", + "code": "IT", + "languages": ["italian"], + "flag_colors": ["green", "white", "red"], + "region": "Southern Europe" + }, + { + "name": "Jersey", + "code": "JE", + "languages": ["english", "french"], + "flag_colors": ["white", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Jamaica", + "code": "JM", + "languages": ["english"], + "flag_colors": ["green", "yellow", "black"], + "region": "Caribbean" + }, + { + "name": "Jordan", + "code": "JO", + "languages": ["arabic"], + "flag_colors": ["black", "white", "green", "red"], + "region": "Middle East" + }, + { + "name": "Japan", + "code": "JP", + "languages": ["japanese"], + "flag_colors": ["red", "white"], + "region": "East Asia" + }, + { + "name": "Kenya", + "code": "KE", + "languages": ["english", "swahili"], + "flag_colors": ["black", "red", "green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Kyrgyzstan", + "code": "KG", + "languages": ["kyrgyz", "russian"], + "flag_colors": ["red", "yellow"], + "region": "Central Asia" + }, + { + "name": "Cambodia", + "code": "KH", + "languages": ["khmer"], + "flag_colors": ["blue", "red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Kiribati", + "code": "KI", + "languages": ["english", "kiribati"], + "flag_colors": ["red", "blue", "white", "yellow"], + "region": "Oceania" + }, + { + "name": "Comoros", + "code": "KM", + "languages": ["comorian", "arabic", "french"], + "flag_colors": ["green", "yellow", "white", "red", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Saint Kitts and Nevis", + "code": "KN", + "languages": ["english"], + "flag_colors": ["green", "red", "black", "yellow", "white"], + "region": "Caribbean" + }, + { + "name": "North Korea", + "code": "KP", + "languages": ["korean"], + "flag_colors": ["red", "blue", "white"], + "region": "East Asia" + }, + { + "name": "South Korea", + "code": "KR", + "languages": ["korean"], + "flag_colors": ["white", "red", "blue", "black"], + "region": "East Asia" + }, + { + "name": "Kuwait", + "code": "KW", + "languages": ["arabic"], + "flag_colors": ["green", "white", "red", "black"], + "region": "Middle East" + }, + { + "name": "Cayman Islands", + "code": "KY", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Kazakhstan", + "code": "KZ", + "languages": ["kazakh", "russian"], + "flag_colors": ["blue", "yellow"], + "region": "Central Asia" + }, + { + "name": "Laos", + "code": "LA", + "languages": ["lao"], + "flag_colors": ["red", "blue", "white"], + "region": "Southeast Asia" + }, + { + "name": "Lebanon", + "code": "LB", + "languages": ["arabic"], + "flag_colors": ["red", "white", "green"], + "region": "Middle East" + }, + { + "name": "Saint Lucia", + "code": "LC", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "black", "white"], + "region": "Caribbean" + }, + { + "name": "Liechtenstein", + "code": "LI", + "languages": ["german"], + "flag_colors": ["blue", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Sri Lanka", + "code": "LK", + "languages": ["sinhala", "tamil"], + "flag_colors": ["yellow", "green", "orange", "red"], + "region": "South Asia" + }, + { + "name": "Liberia", + "code": "LR", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Lesotho", + "code": "LS", + "languages": ["sesotho", "english"], + "flag_colors": ["blue", "white", "green", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Lithuania", + "code": "LT", + "languages": ["lithuanian"], + "flag_colors": ["yellow", "green", "red"], + "region": "Northern Europe" + }, + { + "name": "Luxembourg", + "code": "LU", + "languages": ["luxembourgish", "french", "german"], + "flag_colors": ["red", "white", "blue"], + "region": "Western Europe" + }, + { + "name": "Latvia", + "code": "LV", + "languages": ["latvian"], + "flag_colors": ["red", "white"], + "region": "Northern Europe" + }, + { + "name": "Libya", + "code": "LY", + "languages": ["arabic"], + "flag_colors": ["red", "black", "green", "white"], + "region": "North Africa" + }, + { + "name": "Morocco", + "code": "MA", + "languages": ["arabic", "tamazight"], + "flag_colors": ["red", "green"], + "region": "North Africa" + }, + { + "name": "Monaco", + "code": "MC", + "languages": ["french"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Moldova", + "code": "MD", + "languages": ["romanian"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Eastern Europe" + }, + { + "name": "Montenegro", + "code": "ME", + "languages": ["montenegrin"], + "flag_colors": ["red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Saint Martin", + "code": "MF", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Madagascar", + "code": "MG", + "languages": ["malagasy", "french"], + "flag_colors": ["white", "red", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Marshall Islands", + "code": "MH", + "languages": ["marshallese", "english"], + "flag_colors": ["blue", "white", "orange"], + "region": "Oceania" + }, + { + "name": "North Macedonia", + "code": "MK", + "languages": ["macedonian", "albanian"], + "flag_colors": ["red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Mali", + "code": "ML", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Myanmar", + "code": "MM", + "languages": ["burmese"], + "flag_colors": ["yellow", "green", "red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Mongolia", + "code": "MN", + "languages": ["mongolian"], + "flag_colors": ["red", "blue", "yellow"], + "region": "East Asia" + }, + { + "name": "Macao", + "code": "MO", + "languages": ["cantonese", "portuguese"], + "flag_colors": ["green", "white", "yellow"], + "region": "East Asia" + }, + { + "name": "Northern Mariana Islands", + "code": "MP", + "languages": ["english", "chamorro", "carolinian"], + "flag_colors": ["blue", "white"], + "region": "Oceania" + }, + { + "name": "Martinique", + "code": "MQ", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Mauritania", + "code": "MR", + "languages": ["arabic"], + "flag_colors": ["green", "yellow", "red"], + "region": "North Africa" + }, + { + "name": "Montserrat", + "code": "MS", + "languages": ["english"], + "flag_colors": ["blue", "green", "white", "yellow", "red"], + "region": "Caribbean" + }, + { + "name": "Malta", + "code": "MT", + "languages": ["maltese", "english"], + "flag_colors": ["white", "red"], + "region": "Southern Europe" + }, + { + "name": "Mauritius", + "code": "MU", + "languages": ["english", "french", "mauritian creole"], + "flag_colors": ["red", "blue", "yellow", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Maldives", + "code": "MV", + "languages": ["divehi"], + "flag_colors": ["red", "green", "white"], + "region": "South Asia" + }, + { + "name": "Malawi", + "code": "MW", + "languages": ["english", "chichewa"], + "flag_colors": ["black", "red", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Mexico", + "code": "MX", + "languages": ["spanish"], + "flag_colors": ["green", "white", "red"], + "region": "North America" + }, + { + "name": "Malaysia", + "code": "MY", + "languages": ["malay"], + "flag_colors": ["red", "white", "blue", "yellow"], + "region": "Southeast Asia" + }, + { + "name": "Mozambique", + "code": "MZ", + "languages": ["portuguese"], + "flag_colors": ["green", "black", "yellow", "white", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Namibia", + "code": "NA", + "languages": ["english"], + "flag_colors": ["blue", "red", "green", "white", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "New Caledonia", + "code": "NC", + "languages": ["french"], + "flag_colors": ["blue", "red", "green", "yellow", "black"], + "region": "Oceania" + }, + { + "name": "Niger", + "code": "NE", + "languages": ["french"], + "flag_colors": ["orange", "white", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Norfolk Island", + "code": "NF", + "languages": ["english", "norfuk"], + "flag_colors": ["green", "white"], + "region": "Oceania" + }, + { + "name": "Nigeria", + "code": "NG", + "languages": ["english"], + "flag_colors": ["green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Nicaragua", + "code": "NI", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Netherlands", + "code": "NL", + "languages": ["dutch"], + "flag_colors": ["red", "white", "blue"], + "region": "Western Europe" + }, + { + "name": "Norway", + "code": "NO", + "languages": ["norwegian"], + "flag_colors": ["red", "white", "blue"], + "region": "Northern Europe" + }, + { + "name": "Nepal", + "code": "NP", + "languages": ["nepali"], + "flag_colors": ["red", "blue", "white"], + "region": "South Asia" + }, + { + "name": "Nauru", + "code": "NR", + "languages": ["nauruan", "english"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Niue", + "code": "NU", + "languages": ["niuean", "english"], + "flag_colors": ["yellow", "blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "New Zealand", + "code": "NZ", + "languages": ["english", "maori"], + "flag_colors": ["blue", "red", "white"], + "region": "Oceania" + }, + { + "name": "Oman", + "code": "OM", + "languages": ["arabic"], + "flag_colors": ["red", "white", "green"], + "region": "Middle East" + }, + { + "name": "Panama", + "code": "PA", + "languages": ["spanish"], + "flag_colors": ["red", "white", "blue"], + "region": "Central America" + }, + { + "name": "Peru", + "code": "PE", + "languages": ["spanish", "quechua", "aymara"], + "flag_colors": ["red", "white"], + "region": "South America" + }, + { + "name": "French Polynesia", + "code": "PF", + "languages": ["french", "tahitian"], + "flag_colors": ["red", "white"], + "region": "Oceania" + }, + { + "name": "Papua New Guinea", + "code": "PG", + "languages": ["english", "tok pisin", "hiri motu"], + "flag_colors": ["red", "black", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Philippines", + "code": "PH", + "languages": ["filipino", "english"], + "flag_colors": ["blue", "red", "white", "yellow"], + "region": "Southeast Asia" + }, + { + "name": "Pakistan", + "code": "PK", + "languages": ["urdu", "english"], + "flag_colors": ["green", "white"], + "region": "South Asia" + }, + { + "name": "Poland", + "code": "PL", + "languages": ["polish"], + "flag_colors": ["white", "red"], + "region": "Eastern Europe" + }, + { + "name": "Saint Pierre and Miquelon", + "code": "PM", + "languages": ["french"], + "flag_colors": ["blue", "white", "red", "yellow", "black"], + "region": "North America" + }, + { + "name": "Pitcairn Islands", + "code": "PN", + "languages": ["english", "pitkern"], + "flag_colors": ["blue", "green", "yellow", "red", "white"], + "region": "Oceania" + }, + { + "name": "Puerto Rico", + "code": "PR", + "languages": ["spanish", "english"], + "flag_colors": ["red", "white", "blue"], + "region": "Caribbean" + }, + { + "name": "Palestine", + "code": "PS", + "languages": ["arabic"], + "flag_colors": ["red", "green", "white", "black"], + "region": "Middle East" + }, + { + "name": "Portugal", + "code": "PT", + "languages": ["portuguese"], + "flag_colors": ["green", "red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Palau", + "code": "PW", + "languages": ["palauan", "english"], + "flag_colors": ["blue", "yellow"], + "region": "Oceania" + }, + { + "name": "Paraguay", + "code": "PY", + "languages": ["spanish", "guarani"], + "flag_colors": ["red", "white", "blue", "yellow"], + "region": "South America" + }, + { + "name": "Qatar", + "code": "QA", + "languages": ["arabic"], + "flag_colors": ["red", "white"], + "region": "Middle East" + }, + { + "name": "Reunion", + "code": "RE", + "languages": ["french", "reunion creole"], + "flag_colors": ["blue", "white", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Romania", + "code": "RO", + "languages": ["romanian"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Eastern Europe" + }, + { + "name": "Serbia", + "code": "RS", + "languages": ["serbian"], + "flag_colors": ["red", "blue", "white", "yellow"], + "region": "Eastern Europe" + }, + { + "name": "Russia", + "code": "RU", + "languages": ["russian"], + "flag_colors": ["white", "blue", "red"], + "region": "Eastern Europe" + }, + { + "name": "Rwanda", + "code": "RW", + "languages": ["kinyarwanda", "french", "english"], + "flag_colors": ["blue", "yellow", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Saudi Arabia", + "code": "SA", + "languages": ["arabic"], + "flag_colors": ["green", "white"], + "region": "Middle East" + }, + { + "name": "Solomon Islands", + "code": "SB", + "languages": ["english"], + "flag_colors": ["blue", "green", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Seychelles", + "code": "SC", + "languages": ["seychellois creole", "english", "french"], + "flag_colors": ["blue", "yellow", "red", "white", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Sudan", + "code": "SD", + "languages": ["arabic", "english"], + "flag_colors": ["red", "white", "black", "green"], + "region": "North Africa" + }, + { + "name": "Sweden", + "code": "SE", + "languages": ["swedish"], + "flag_colors": ["blue", "yellow"], + "region": "Northern Europe" + }, + { + "name": "Singapore", + "code": "SG", + "languages": ["english", "malay", "mandarin", "tamil"], + "flag_colors": ["red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Saint Helena", + "code": "SH", + "languages": ["english"], + "flag_colors": ["blue", "red", "white", "yellow", "green"], + "region": "Atlantic Ocean" + }, + { + "name": "Slovenia", + "code": "SI", + "languages": ["slovenian"], + "flag_colors": ["white", "blue", "red"], + "region": "Southern Europe" + }, + { + "name": "Svalbard and Jan Mayen", + "code": "SJ", + "languages": ["norwegian"], + "flag_colors": ["red", "white", "blue"], + "region": "Northern Europe" + }, + { + "name": "Slovakia", + "code": "SK", + "languages": ["slovak"], + "flag_colors": ["white", "blue", "red"], + "region": "Eastern Europe" + }, + { + "name": "Sierra Leone", + "code": "SL", + "languages": ["english"], + "flag_colors": ["green", "white", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "San Marino", + "code": "SM", + "languages": ["italian"], + "flag_colors": ["white", "blue"], + "region": "Southern Europe" + }, + { + "name": "Senegal", + "code": "SN", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Somalia", + "code": "SO", + "languages": ["somali", "arabic"], + "flag_colors": ["blue", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Suriname", + "code": "SR", + "languages": ["dutch"], + "flag_colors": ["green", "white", "red", "yellow"], + "region": "South America" + }, + { + "name": "South Sudan", + "code": "SS", + "languages": ["english"], + "flag_colors": ["black", "red", "green", "blue", "white", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Sao Tome and Principe", + "code": "ST", + "languages": ["portuguese"], + "flag_colors": ["green", "yellow", "red", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "El Salvador", + "code": "SV", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Sint Maarten", + "code": "SX", + "languages": ["dutch", "english"], + "flag_colors": ["red", "white", "blue"], + "region": "Caribbean" + }, + { + "name": "Syria", + "code": "SY", + "languages": ["arabic"], + "flag_colors": ["red", "white", "black", "green"], + "region": "Middle East" + }, + { + "name": "Eswatini", + "code": "SZ", + "languages": ["swazi", "english"], + "flag_colors": ["blue", "red", "yellow", "black", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Turks and Caicos Islands", + "code": "TC", + "languages": ["english"], + "flag_colors": ["blue", "red", "white", "yellow", "green"], + "region": "Caribbean" + }, + { + "name": "Chad", + "code": "TD", + "languages": ["french", "arabic"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "French Southern Territories", + "code": "TF", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "Togo", + "code": "TG", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Thailand", + "code": "TH", + "languages": ["thai"], + "flag_colors": ["red", "white", "blue"], + "region": "Southeast Asia" + }, + { + "name": "Tajikistan", + "code": "TJ", + "languages": ["tajik"], + "flag_colors": ["red", "white", "green", "yellow"], + "region": "Central Asia" + }, + { + "name": "Tokelau", + "code": "TK", + "languages": ["tokelauan", "english"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Timor-Leste", + "code": "TL", + "languages": ["tetum", "portuguese"], + "flag_colors": ["red", "yellow", "black", "white"], + "region": "Southeast Asia" + }, + { + "name": "Turkmenistan", + "code": "TM", + "languages": ["turkmen"], + "flag_colors": ["green", "red", "white"], + "region": "Central Asia" + }, + { + "name": "Tunisia", + "code": "TN", + "languages": ["arabic"], + "flag_colors": ["red", "white"], + "region": "North Africa" + }, + { + "name": "Tonga", + "code": "TO", + "languages": ["tongan", "english"], + "flag_colors": ["red", "white"], + "region": "Oceania" + }, + { + "name": "Turkey", + "code": "TR", + "languages": ["turkish"], + "flag_colors": ["red", "white"], + "region": "Middle East" + }, + { + "name": "Trinidad and Tobago", + "code": "TT", + "languages": ["english"], + "flag_colors": ["red", "black", "white"], + "region": "Caribbean" + }, + { + "name": "Tuvalu", + "code": "TV", + "languages": ["tuvaluan", "english"], + "flag_colors": ["blue", "yellow", "red", "white"], + "region": "Oceania" + }, + { + "name": "Taiwan", + "code": "TW", + "languages": ["mandarin"], + "flag_colors": ["red", "blue", "white"], + "region": "East Asia" + }, + { + "name": "Tanzania", + "code": "TZ", + "languages": ["swahili", "english"], + "flag_colors": ["green", "yellow", "black", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Ukraine", + "code": "UA", + "languages": ["ukrainian"], + "flag_colors": ["blue", "yellow"], + "region": "Eastern Europe" + }, + { + "name": "Uganda", + "code": "UG", + "languages": ["english", "swahili"], + "flag_colors": ["black", "yellow", "red", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "United States", + "code": "US", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "North America" + }, + { + "name": "Uruguay", + "code": "UY", + "languages": ["spanish"], + "flag_colors": ["white", "blue", "yellow"], + "region": "South America" + }, + { + "name": "Uzbekistan", + "code": "UZ", + "languages": ["uzbek"], + "flag_colors": ["blue", "white", "green", "red"], + "region": "Central Asia" + }, + { + "name": "Vatican City", + "code": "VA", + "languages": ["italian", "latin"], + "flag_colors": ["yellow", "white"], + "region": "Southern Europe" + }, + { + "name": "Saint Vincent and the Grenadines", + "code": "VC", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "green", "white"], + "region": "Caribbean" + }, + { + "name": "Venezuela", + "code": "VE", + "languages": ["spanish"], + "flag_colors": ["yellow", "blue", "red", "white"], + "region": "South America" + }, + { + "name": "British Virgin Islands", + "code": "VG", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "U.S. Virgin Islands", + "code": "VI", + "languages": ["english"], + "flag_colors": ["red", "white", "blue", "yellow"], + "region": "Caribbean" + }, + { + "name": "Vietnam", + "code": "VN", + "languages": ["vietnamese"], + "flag_colors": ["red", "yellow"], + "region": "Southeast Asia" + }, + { + "name": "Vanuatu", + "code": "VU", + "languages": ["bislama", "english", "french"], + "flag_colors": ["red", "green", "black", "yellow"], + "region": "Oceania" + }, + { + "name": "Wallis and Futuna", + "code": "WF", + "languages": ["french", "wallisian", "futunan"], + "flag_colors": ["red", "white", "blue"], + "region": "Oceania" + }, + { + "name": "Samoa", + "code": "WS", + "languages": ["samoan", "english"], + "flag_colors": ["red", "blue", "white"], + "region": "Oceania" + }, + { + "name": "Kosovo", + "code": "XK", + "languages": ["albanian", "serbian"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Eastern Europe" + }, + { + "name": "Yemen", + "code": "YE", + "languages": ["arabic"], + "flag_colors": ["red", "white", "black"], + "region": "Middle East" + }, + { + "name": "Mayotte", + "code": "YT", + "languages": ["french", "shimaore", "kibushi"], + "flag_colors": ["white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "South Africa", + "code": "ZA", + "languages": ["zulu", "xhosa", "afrikaans", "english"], + "flag_colors": ["red", "blue", "green", "yellow", "black", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Zambia", + "code": "ZM", + "languages": ["english"], + "flag_colors": ["green", "red", "black", "orange"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Zimbabwe", + "code": "ZW", + "languages": ["english", "shona", "ndebele"], + "flag_colors": ["green", "yellow", "red", "black", "white"], + "region": "Sub-Saharan Africa" + } +]; diff --git a/apps/web/src/lib/stickers/providers/emoji.ts b/apps/web/src/lib/stickers/providers/emoji.ts deleted file mode 100644 index cc198236..00000000 --- a/apps/web/src/lib/stickers/providers/emoji.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - POPULAR_COLLECTIONS, - getIconSvgUrl, - searchIcons, -} from "@/lib/iconify-api"; -import { buildStickerId, parseStickerId } from "../sticker-id"; -import type { - StickerItem, - StickerProvider, - StickerSearchResult, -} from "../types"; - -const EMOJI_PROVIDER_ID = "emoji"; -const DEFAULT_SEARCH_LIMIT = 100; - -const EMOJI_PREFIXES = POPULAR_COLLECTIONS.emoji.map( - (collection) => collection.prefix, -); - -const DEFAULT_EMOJI_BROWSE = [ - "noto:grinning-face", - "noto:smiling-face-with-heart-eyes", - "noto:fire", - "noto:rocket", - "noto:party-popper", - "noto:clapping-hands", - "noto:sparkles", - "noto:red-heart", - "noto:thumbs-up", - "noto:eyes", - "noto:thinking-face", - "noto:hundred-points", -]; - -function getDisplayNameFromIconName({ - iconName, -}: { - iconName: string; -}): string { - const parts = iconName.split(":"); - const rawName = parts[parts.length - 1] ?? iconName; - return rawName.replaceAll("-", " ").replaceAll("_", " "); -} - -function toStickerItem({ iconName }: { iconName: string }): StickerItem { - return { - id: buildStickerId({ - providerId: EMOJI_PROVIDER_ID, - providerValue: iconName, - }), - provider: EMOJI_PROVIDER_ID, - name: getDisplayNameFromIconName({ iconName }), - previewUrl: getIconSvgUrl(iconName, { width: 64, height: 64 }), - metadata: { iconName }, - }; -} - -function computeHasMore({ - total, - limit, - start = 0, -}: { - total: number; - limit: number; - start?: number; -}): boolean { - return start + limit < total; -} - -export const emojiProvider: StickerProvider = { - id: EMOJI_PROVIDER_ID, - async search({ - query, - options, - }: { - query: string; - options?: { limit?: number }; - }): Promise<StickerSearchResult> { - const limit = options?.limit ?? DEFAULT_SEARCH_LIMIT; - const searchResult = await searchIcons(query, limit, EMOJI_PREFIXES); - return { - items: searchResult.icons.map((iconName) => toStickerItem({ iconName })), - total: searchResult.total, - hasMore: computeHasMore({ - total: searchResult.total, - limit: searchResult.limit, - start: searchResult.start, - }), - }; - }, - async browse({ - options, - }: { - options?: { page?: number; limit?: number }; - }): Promise<StickerSearchResult> { - const limit = options?.limit ?? DEFAULT_EMOJI_BROWSE.length; - const items = DEFAULT_EMOJI_BROWSE.slice(0, limit).map((iconName) => - toStickerItem({ iconName }), - ); - return { - items, - total: items.length, - hasMore: false, - }; - }, - resolveUrl({ - stickerId, - options, - }: { - stickerId: string; - options?: { width?: number; height?: number }; - }): string { - const { providerValue } = parseStickerId({ stickerId }); - return getIconSvgUrl(providerValue, { - width: options?.width, - height: options?.height, - }); - }, -}; diff --git a/apps/web/src/lib/stickers/providers/flags.ts b/apps/web/src/lib/stickers/providers/flags.ts index 91cc5489..0cca292f 100644 --- a/apps/web/src/lib/stickers/providers/flags.ts +++ b/apps/web/src/lib/stickers/providers/flags.ts @@ -1,23 +1,17 @@ import { buildStickerId, parseStickerId } from "../sticker-id"; import type { + StickerBrowseResult, StickerItem, StickerProvider, StickerSearchResult, } from "../types"; +import { REGIONS, REGION_GROUPS } from "./countries-data"; +import type { CountryRecord, RegionId } from "./countries-data"; const FLAGS_PROVIDER_ID = "flags"; -const FLAGS_DATASET_URL = "/countries.json"; const DEFAULT_SEARCH_LIMIT = 100; const DEFAULT_FLAGS_BASE_URL = "/flags"; -type CountryRecord = { - name: string; - code: string; - languages?: string[]; - flag_colors?: string[]; - region?: string; -}; - let countriesPromise: Promise<CountryRecord[]> | null = null; function getFlagsBaseUrl(): string { @@ -25,7 +19,7 @@ function getFlagsBaseUrl(): string { } function buildFlagUrl({ code }: { code: string }): string { - const normalizedCode = code.toUpperCase(); + const normalizedCode = code.toLowerCase(); return `${getFlagsBaseUrl()}/${encodeURIComponent(normalizedCode)}.svg`; } @@ -34,13 +28,8 @@ async function loadCountries(): Promise<CountryRecord[]> { return countriesPromise; } - countriesPromise = fetch(FLAGS_DATASET_URL) - .then(async (response) => { - if (!response.ok) { - throw new Error(`Failed to load countries: ${response.status}`); - } - return (await response.json()) as CountryRecord[]; - }) + countriesPromise = import("./countries-data") + .then((m) => m.COUNTRIES) .catch((error) => { console.error("Failed to load countries dataset:", error); return []; @@ -72,6 +61,44 @@ function normalizeQuery({ query }: { query: string }): string { return query.trim().toLowerCase(); } +function findMatchingRegions({ + query, +}: { + query: string; +}): (typeof REGIONS)[number][] { + return REGIONS.filter( + (r) => + r.id.toLowerCase() === query || + r.aliases.some((alias) => alias === query), + ); +} + +export function resolveQueryToRegions({ + query, +}: { + query: string; +}): Set<RegionId> | null { + const group = REGION_GROUPS[query]; + if (group) { + return new Set(group); + } + + const matched = findMatchingRegions({ query }); + return matched.length > 0 ? new Set(matched.map((r) => r.id)) : null; +} + +export function getRegionLabel({ query }: { query: string }): string { + if (REGION_GROUPS[query]) { + return query + .split(" ") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); + } + + const matched = findMatchingRegions({ query }); + return matched[0]?.id ?? query; +} + function filterCountriesByQuery({ countries, query, @@ -83,15 +110,16 @@ function filterCountriesByQuery({ return countries; } + const regionIds = resolveQueryToRegions({ query }); + + if (regionIds) { + return countries.filter((country) => country.region && regionIds.has(country.region)); + } + return countries.filter((country) => { const normalizedName = country.name.toLowerCase(); const normalizedCode = country.code.toLowerCase(); - const normalizedRegion = country.region?.toLowerCase() ?? ""; - return ( - normalizedName.includes(query) || - normalizedCode.includes(query) || - normalizedRegion.includes(query) - ); + return normalizedName.includes(query) || normalizedCode.includes(query); }); } @@ -102,13 +130,15 @@ function paginateCountries({ countries: CountryRecord[]; options?: { page?: number; limit?: number }; }): { items: CountryRecord[]; hasMore: boolean; total: number } { - const page = Math.max(1, options?.page ?? 1); - const limit = Math.max(1, options?.limit ?? DEFAULT_SEARCH_LIMIT); + if (options?.limit === undefined) { + return { items: countries, hasMore: false, total: countries.length }; + } + const page = Math.max(1, options.page ?? 1); + const limit = Math.max(1, options.limit); const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; - const pagedItems = countries.slice(startIndex, endIndex); return { - items: pagedItems, + items: countries.slice(startIndex, endIndex), hasMore: endIndex < countries.length, total: countries.length, }; @@ -146,13 +176,18 @@ export const flagsProvider: StickerProvider = { options, }: { options?: { page?: number; limit?: number }; - }): Promise<StickerSearchResult> { + }): Promise<StickerBrowseResult> { const countries = await loadCountries(); const paged = paginateCountries({ countries, options }); return { - items: paged.items.map((country) => toStickerItem({ country })), - total: paged.total, - hasMore: paged.hasMore, + sections: [ + { + id: "all", + items: paged.items.map((country) => toStickerItem({ country })), + hasMore: paged.hasMore, + layout: "grid", + }, + ], }; }, resolveUrl({ diff --git a/apps/web/src/lib/stickers/providers/icons.ts b/apps/web/src/lib/stickers/providers/icons.ts deleted file mode 100644 index 06b2a4c2..00000000 --- a/apps/web/src/lib/stickers/providers/icons.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - POPULAR_COLLECTIONS, - getIconSvgUrl, - searchIcons, -} from "@/lib/iconify-api"; -import { buildStickerId, parseStickerId } from "../sticker-id"; -import type { - StickerItem, - StickerProvider, - StickerSearchResult, -} from "../types"; - -const ICONS_PROVIDER_ID = "icons"; -const DEFAULT_SEARCH_LIMIT = 100; - -const ICONS_PREFIXES = Array.from( - new Set( - [...POPULAR_COLLECTIONS.general, ...POPULAR_COLLECTIONS.brands].map( - (collection) => collection.prefix, - ), - ), -); - -const DEFAULT_ICONS_BROWSE = [ - "mdi:home", - "mdi:star", - "mdi:heart", - "mdi:check-circle", - "mdi:account", - "mdi:camera", - "mdi:music", - "mdi:map-marker", - "mdi:calendar", - "mdi:lightning-bolt", - "mdi:cog", - "mdi:rocket", -]; - -function getDisplayNameFromIconName({ - iconName, -}: { - iconName: string; -}): string { - const [, rawName = iconName] = iconName.split(":"); - return rawName.replaceAll("-", " ").replaceAll("_", " "); -} - -function toStickerItem({ iconName }: { iconName: string }): StickerItem { - return { - id: buildStickerId({ - providerId: ICONS_PROVIDER_ID, - providerValue: iconName, - }), - provider: ICONS_PROVIDER_ID, - name: getDisplayNameFromIconName({ iconName }), - previewUrl: getIconSvgUrl(iconName, { width: 64, height: 64 }), - metadata: { iconName }, - }; -} - -function computeHasMore({ - total, - limit, - start = 0, -}: { - total: number; - limit: number; - start?: number; -}): boolean { - return start + limit < total; -} - -export const iconsProvider: StickerProvider = { - id: ICONS_PROVIDER_ID, - async search({ - query, - options, - }: { - query: string; - options?: { limit?: number }; - }): Promise<StickerSearchResult> { - const limit = options?.limit ?? DEFAULT_SEARCH_LIMIT; - const searchResult = await searchIcons(query, limit, ICONS_PREFIXES); - return { - items: searchResult.icons.map((iconName) => toStickerItem({ iconName })), - total: searchResult.total, - hasMore: computeHasMore({ - total: searchResult.total, - limit: searchResult.limit, - start: searchResult.start, - }), - }; - }, - async browse({ - options, - }: { - options?: { page?: number; limit?: number }; - }): Promise<StickerSearchResult> { - const limit = options?.limit ?? DEFAULT_ICONS_BROWSE.length; - const items = DEFAULT_ICONS_BROWSE.slice(0, limit).map((iconName) => - toStickerItem({ iconName }), - ); - return { - items, - total: items.length, - hasMore: false, - }; - }, - resolveUrl({ - stickerId, - options, - }: { - stickerId: string; - options?: { width?: number; height?: number }; - }): string { - const { providerValue } = parseStickerId({ stickerId }); - return getIconSvgUrl(providerValue, { - width: options?.width, - height: options?.height, - }); - }, -}; diff --git a/apps/web/src/lib/stickers/providers/index.ts b/apps/web/src/lib/stickers/providers/index.ts index 3c4faee9..b0a298cb 100644 --- a/apps/web/src/lib/stickers/providers/index.ts +++ b/apps/web/src/lib/stickers/providers/index.ts @@ -1,13 +1,11 @@ -import { hasProvider, registerProvider } from "../registry"; -import type { StickerProvider } from "@/types/stickers"; -import { emojiProvider } from "./emoji"; +import { stickersRegistry } from "../registry"; +import type { StickerProvider } from "@/lib/stickers/types"; import { flagsProvider } from "./flags"; -import { iconsProvider } from "./icons"; +import { logosProvider } from "./logos"; import { shapesProvider } from "./shapes"; const defaultProviders: StickerProvider[] = [ - iconsProvider, - emojiProvider, + logosProvider, flagsProvider, shapesProvider, ]; @@ -18,9 +16,9 @@ export function registerDefaultStickerProviders({ providersToRegister?: StickerProvider[]; } = {}): void { for (const provider of providersToRegister) { - if (hasProvider({ providerId: provider.id })) { + if (stickersRegistry.has(provider.id)) { continue; } - registerProvider({ provider }); + stickersRegistry.register(provider.id, provider); } } diff --git a/apps/web/src/lib/stickers/providers/logos.ts b/apps/web/src/lib/stickers/providers/logos.ts new file mode 100644 index 00000000..5be2e9a2 --- /dev/null +++ b/apps/web/src/lib/stickers/providers/logos.ts @@ -0,0 +1,31 @@ +import type { + StickerBrowseResult, + StickerItem, + StickerProvider, + StickerSearchResult, +} from "../types"; + +const LOGOS_PROVIDER_ID = "logos"; + +const EMPTY_SEARCH_RESULT: StickerSearchResult = { + items: [] as StickerItem[], + total: 0, + hasMore: false, +}; + +const EMPTY_BROWSE_RESULT: StickerBrowseResult = { + sections: [], +}; + +export const logosProvider: StickerProvider = { + id: LOGOS_PROVIDER_ID, + async search(): Promise<StickerSearchResult> { + return EMPTY_SEARCH_RESULT; + }, + async browse(): Promise<StickerBrowseResult> { + return EMPTY_BROWSE_RESULT; + }, + resolveUrl({ stickerId }: { stickerId: string }): string { + return stickerId; + }, +}; diff --git a/apps/web/src/lib/stickers/providers/shapes.ts b/apps/web/src/lib/stickers/providers/shapes.ts index 81873dcb..a23f14a7 100644 --- a/apps/web/src/lib/stickers/providers/shapes.ts +++ b/apps/web/src/lib/stickers/providers/shapes.ts @@ -1,5 +1,8 @@ +import { buildGraphicPreviewUrl, buildDefaultGraphicInstance, graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; +import type { ParamValues } from "@/lib/params"; import { buildStickerId, parseStickerId } from "../sticker-id"; import type { + StickerBrowseResult, StickerItem, StickerProvider, StickerSearchResult, @@ -7,60 +10,140 @@ import type { const SHAPES_PROVIDER_ID = "shapes"; -const SHAPES = [ - { key: "circle", name: "Circle" }, - { key: "square", name: "Square" }, - { key: "triangle", name: "Triangle" }, - { key: "star", name: "Star" }, - { key: "hexagon", name: "Hexagon" }, - { key: "diamond", name: "Diamond" }, -] as const; +type ShapeGraphicPreset = { + shapeKey: string; + name: string; + definitionId: string; + params?: ParamValues; +}; + +const LEGACY_SHAPE_PRESETS: Record<string, ShapeGraphicPreset> = { + square: { shapeKey: "square", name: "Square", definitionId: "rectangle" }, + circle: { shapeKey: "circle", name: "Circle", definitionId: "ellipse" }, + triangle: { + shapeKey: "triangle", + name: "Triangle", + definitionId: "polygon", + params: { sides: 3 }, + }, + hexagon: { + shapeKey: "hexagon", + name: "Hexagon", + definitionId: "polygon", + params: { sides: 6 }, + }, + diamond: { + shapeKey: "diamond", + name: "Diamond", + definitionId: "polygon", + params: { sides: 4 }, + }, + star: { shapeKey: "star", name: "Star", definitionId: "star" }, +}; + +function getShapePresets(): ShapeGraphicPreset[] { + registerDefaultGraphics(); + return graphicsRegistry.getAll().map((definition) => ({ + shapeKey: definition.id, + name: definition.name, + definitionId: definition.id, + })); +} + +function getShapePreset({ + shapeKey, +}: { + shapeKey: string; +}): ShapeGraphicPreset | null { + return ( + getShapePresets().find((preset) => preset.shapeKey === shapeKey) ?? + LEGACY_SHAPE_PRESETS[shapeKey] ?? + null + ); +} + +function getShapeParams({ + preset, +}: { + preset: ShapeGraphicPreset; +}): ParamValues { + return { + ...buildDefaultGraphicInstance({ definitionId: preset.definitionId }).params, + ...preset.params, + }; +} + +export function parseShapeStickerId({ + stickerId, +}: { + stickerId: string; +}): ShapeGraphicPreset | null { + try { + const { providerValue } = parseStickerId({ stickerId }); + return getShapePreset({ shapeKey: providerValue }); + } catch { + return null; + } +} function buildShapeUrl({ shapeKey }: { shapeKey: string }): string { - return `/shapes/${shapeKey}.svg`; + const preset = getShapePreset({ shapeKey }); + if (!preset) { + return buildGraphicPreviewUrl({ definitionId: "rectangle" }); + } + return buildGraphicPreviewUrl({ + definitionId: preset.definitionId, + params: getShapeParams({ preset }), + }); } function toStickerItem({ - shape, + preset, }: { - shape: (typeof SHAPES)[number]; + preset: ShapeGraphicPreset; }): StickerItem { return { id: buildStickerId({ providerId: SHAPES_PROVIDER_ID, - providerValue: shape.key, + providerValue: preset.shapeKey, }), provider: SHAPES_PROVIDER_ID, - name: shape.name, - previewUrl: buildShapeUrl({ shapeKey: shape.key }), - metadata: { shape: shape.key }, + name: preset.name, + previewUrl: buildShapeUrl({ shapeKey: preset.shapeKey }), + metadata: { + definitionId: preset.definitionId, + params: preset.params ?? {}, + }, }; } -function filterShapesByQuery({ - query, -}: { - query: string; -}): Array<(typeof SHAPES)[number]> { +function filterShapesByQuery({ query }: { query: string }): ShapeGraphicPreset[] { const normalizedQuery = query.trim().toLowerCase(); + const presets = getShapePresets(); if (!normalizedQuery) { - return [...SHAPES]; + return presets; } - return SHAPES.filter((shape) => - shape.name.toLowerCase().includes(normalizedQuery), - ); + return presets.filter((preset) => { + const definition = graphicsRegistry.get(preset.definitionId); + return ( + preset.name.toLowerCase().includes(normalizedQuery) || + definition.keywords.some((keyword) => + keyword.toLowerCase().includes(normalizedQuery), + ) + ); + }); } function paginateShapes({ shapes, options, }: { - shapes: Array<(typeof SHAPES)[number]>; + shapes: ShapeGraphicPreset[]; options?: { page?: number; limit?: number }; -}): { items: Array<(typeof SHAPES)[number]>; hasMore: boolean; total: number } { +}): { items: ShapeGraphicPreset[]; hasMore: boolean; total: number } { const page = Math.max(1, options?.page ?? 1); - const limit = Math.max(1, options?.limit ?? SHAPES.length); + const limit = Math.max(1, options?.limit ?? getShapePresets().length); const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const pagedItems = shapes.slice(startIndex, endIndex); @@ -83,10 +166,10 @@ export const shapesProvider: StickerProvider = { const filteredShapes = filterShapesByQuery({ query }); const paged = paginateShapes({ shapes: filteredShapes, - options: { page: 1, limit: options?.limit ?? SHAPES.length }, + options: { page: 1, limit: options?.limit ?? getShapePresets().length }, }); return { - items: paged.items.map((shape) => toStickerItem({ shape })), + items: paged.items.map((preset) => toStickerItem({ preset })), total: paged.total, hasMore: paged.hasMore, }; @@ -95,15 +178,20 @@ export const shapesProvider: StickerProvider = { options, }: { options?: { page?: number; limit?: number }; - }): Promise<StickerSearchResult> { + }): Promise<StickerBrowseResult> { const paged = paginateShapes({ - shapes: [...SHAPES], + shapes: getShapePresets(), options, }); return { - items: paged.items.map((shape) => toStickerItem({ shape })), - total: paged.total, - hasMore: paged.hasMore, + sections: [ + { + id: "all", + items: paged.items.map((preset) => toStickerItem({ preset })), + hasMore: paged.hasMore, + layout: "grid", + }, + ], }; }, resolveUrl({ @@ -112,7 +200,7 @@ export const shapesProvider: StickerProvider = { stickerId: string; options?: { width?: number; height?: number }; }): string { - const { providerValue } = parseStickerId({ stickerId }); - return buildShapeUrl({ shapeKey: providerValue }); + const preset = parseShapeStickerId({ stickerId }); + return buildShapeUrl({ shapeKey: preset?.shapeKey ?? "rectangle" }); }, }; diff --git a/apps/web/src/lib/stickers/registry.ts b/apps/web/src/lib/stickers/registry.ts index 0fbad594..7148c09d 100644 --- a/apps/web/src/lib/stickers/registry.ts +++ b/apps/web/src/lib/stickers/registry.ts @@ -1,31 +1,10 @@ -import type { StickerProvider } from "@/types/stickers"; +import type { StickerProvider } from "@/lib/stickers/types"; +import { DefinitionRegistry } from "@/lib/registry"; -const providers = new Map<string, StickerProvider>(); - -export function registerProvider({ - provider, -}: { - provider: StickerProvider; -}): void { - providers.set(provider.id, provider); -} - -export function hasProvider({ providerId }: { providerId: string }): boolean { - return providers.has(providerId); -} - -export function getProvider({ - providerId, -}: { - providerId: string; -}): StickerProvider { - const provider = providers.get(providerId); - if (!provider) { - throw new Error(`Unknown sticker provider: ${providerId}`); +export class StickersRegistry extends DefinitionRegistry<string, StickerProvider> { + constructor() { + super("sticker provider"); } - return provider; } -export function getAllProviders(): StickerProvider[] { - return Array.from(providers.values()); -} +export const stickersRegistry = new StickersRegistry(); diff --git a/apps/web/src/lib/stickers/resolver.ts b/apps/web/src/lib/stickers/resolver.ts index 3252cd60..0002e716 100644 --- a/apps/web/src/lib/stickers/resolver.ts +++ b/apps/web/src/lib/stickers/resolver.ts @@ -1,7 +1,7 @@ -import { getProvider } from "./registry"; +import { stickersRegistry } from "./registry"; import { parseStickerId } from "./sticker-id"; import { registerDefaultStickerProviders } from "./providers"; -import type { StickerResolveOptions } from "@/types/stickers"; +import type { StickerResolveOptions } from "@/lib/stickers/types"; export function resolveStickerId({ stickerId, @@ -13,9 +13,7 @@ export function resolveStickerId({ registerDefaultStickerProviders(); const parsedStickerId = parseStickerId({ stickerId }); - return getProvider({ - providerId: parsedStickerId.providerId, - }).resolveUrl({ + return stickersRegistry.get(parsedStickerId.providerId).resolveUrl({ stickerId, options, }); diff --git a/apps/web/src/lib/stickers/types.ts b/apps/web/src/lib/stickers/types.ts index 91cbcaae..9d16e4be 100644 --- a/apps/web/src/lib/stickers/types.ts +++ b/apps/web/src/lib/stickers/types.ts @@ -1,8 +1,71 @@ -export type { - StickerItem, - StickerProvider, - StickerProviderBrowseOptions, - StickerProviderSearchOptions, - StickerResolveOptions, - StickerSearchResult, -} from "@/types/stickers"; +import type { STICKER_CATEGORIES } from "@/constants/sticker-constants"; + +export type StickerCategory = keyof typeof STICKER_CATEGORIES; + +export interface StickerItem { + id: string; + provider: string; + name: string; + previewUrl: string; + metadata: Record<string, unknown>; +} + +export interface StickerSearchResult { + items: StickerItem[]; + total: number; + hasMore: boolean; +} + +export interface StickerBrowseSection { + id: string; + title?: string; + items: StickerItem[]; + hasMore?: boolean; + layout?: "grid" | "row"; + action?: { + type: "see-all"; + category?: StickerCategory; + sectionId?: string; + }; +} + +export interface StickerBrowseResult { + sections: StickerBrowseSection[]; +} + +export interface StickerProviderSearchOptions { + limit?: number; +} + +export interface StickerProviderBrowseOptions { + page?: number; + limit?: number; +} + +export interface StickerResolveOptions { + width?: number; + height?: number; +} + +export interface StickerProvider { + id: string; + search({ + query, + options, + }: { + query: string; + options?: StickerProviderSearchOptions; + }): Promise<StickerSearchResult>; + browse({ + options, + }: { + options?: StickerProviderBrowseOptions; + }): Promise<StickerBrowseResult>; + resolveUrl({ + stickerId, + options, + }: { + stickerId: string; + options?: StickerResolveOptions; + }): string; +} |
