feat: list logos from the api instead of hardcoding it

This commit is contained in:
2026-02-21 22:05:26 +01:00
parent 43c91b3c54
commit 81987ac8f1
4 changed files with 32 additions and 55 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const baseApiUrl = useBaseApiUrl(); const baseApiUrl = useBaseApiUrl();
const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl); const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl);
const { data: logos } = await useFetch<string[]>("/api/logos");
</script> </script>
<template> <template>
@@ -10,8 +11,8 @@ const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl);
<p> <p>
You can easily generate QRCodes by using the API, with no rate You can easily generate QRCodes by using the API, with no rate
limitation. limitation.
<br /> <br >
<br /> <br >
If you are not sure how to use the API, you can fill the QRCode form If you are not sure how to use the API, you can fill the QRCode form
and copy the generated API URL. and copy the generated API URL.
</p> </p>
@@ -58,7 +59,7 @@ const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl);
<div class="flex pb-4 justify-between gap-x-5"> <div class="flex pb-4 justify-between gap-x-5">
<span class="text-primary font-semibold">logo</span> <span class="text-primary font-semibold">logo</span>
<span class="text-slate-700 dark:text-slate-200 text-right">{{ <span class="text-slate-700 dark:text-slate-200 text-right">{{
arrayToUnion(LOGOS) arrayToUnion(logos ?? [])
}}</span> }}</span>
</div> </div>

View File

@@ -11,10 +11,12 @@ const apiModal = overlay.create(LazyApiModal);
const isQRCodeEmpty = computed(() => qrCode.value === "/default.webp"); const isQRCodeEmpty = computed(() => qrCode.value === "/default.webp");
const { data: logos } = await useFetch<string[]>("/api/logos");
const formSchema = z const formSchema = z
.object({ .object({
hasLogo: z.boolean(), hasLogo: z.boolean(),
logo: z.enum(LOGOS).optional(), logo: z.string().optional(),
format: z.enum(IMAGE_FORMATS).default("png"), format: z.enum(IMAGE_FORMATS).default("png"),
content: z content: z
.string() .string()
@@ -103,7 +105,7 @@ const {
<img <img
:src="qrCode" :src="qrCode"
class="w-full max-w-[375px] max-h-[375px] sm:max-w-[315px] sm:max-h-[315px] md:max-w-[375px] md:max-h-[375px] m-auto aspect-square border border-gray-100 dark:border-gray-800" class="w-full max-w-[375px] max-h-[375px] sm:max-w-[315px] sm:max-h-[315px] md:max-w-[375px] md:max-h-[375px] m-auto aspect-square border border-gray-100 dark:border-gray-800"
/> >
<div class="flex-1 flex flex-col justify-center"> <div class="flex-1 flex flex-col justify-center">
<UForm <UForm
@@ -138,7 +140,7 @@ const {
<USelectMenu <USelectMenu
v-model="formState.logo" v-model="formState.logo"
icon="i-heroicons-photo" icon="i-heroicons-photo"
:items="unreadonly(LOGOS)" :items="logos ?? []"
:disabled="!formState.hasLogo" :disabled="!formState.hasLogo"
placeholder="Select logo" placeholder="Select logo"
class="w-full" class="w-full"

22
server/api/logos.ts Normal file
View File

@@ -0,0 +1,22 @@
import { readdir } from "fs/promises";
import { join } from "path";
import { createHash } from "crypto";
const logosDir = import.meta.dev
? join(process.cwd(), "public/logos")
: join(process.cwd(), ".output/public/logos");
export default defineCachedEventHandler(async () => {
const files = await readdir(logosDir);
return files
.filter((f) => f.endsWith(".png"))
.map((f) => f.replace(/\.png$/, ""))
.sort();
}, {
maxAge: 60 * 60 * 24,
getKey: async () => {
const files = await readdir(logosDir);
const key = files.filter((f) => f.endsWith(".png")).sort().join(",");
return createHash("sha256").update(key).digest("hex");
},
});

View File

@@ -1,60 +1,12 @@
import { z } from "zod"; import { z } from "zod";
// TODO: might be better to load these dynamically lol
export const LOGOS = [
"bereal",
"bitcoin",
"buymeacoffee",
"diaspora",
"discord",
"dropbox",
"ello",
"facebook",
"flickr",
"github",
"googlemaps",
"googlemeet",
"googlemessages",
"imessage",
"instagram",
"kik",
"line",
"linkedin",
"litecoin",
"mastodon",
"medium",
"messenger",
"monero",
"onlyfans",
"patreon",
"paypal",
"peertube",
"pinterest",
"reddit",
"session",
"signal",
"snapchat",
"spotify",
"substack",
"telegram",
"threema",
"twitch",
"venmo",
"viber",
"wechat",
"whatsapp",
"x",
"youtube",
"zoom",
] as const;
export const IMAGE_FORMATS = ["png", "jpeg", "webp"] as const; export const IMAGE_FORMATS = ["png", "jpeg", "webp"] as const;
export type ImageFormat = (typeof IMAGE_FORMATS)[number]; export type ImageFormat = (typeof IMAGE_FORMATS)[number];
export const settingsSchema = z.object({ export const settingsSchema = z.object({
format: z.enum(IMAGE_FORMATS).default("png"), format: z.enum(IMAGE_FORMATS).default("png"),
logo: z.enum(LOGOS).optional(), logo: z.string().optional(),
content: z.string().min(1, "Required"), content: z.string().min(1, "Required"),
}); });