Compare commits

...

12 Commits

123 changed files with 7197 additions and 9310 deletions

12
.prettierignore Normal file
View File

@@ -0,0 +1,12 @@
.test
.nuxt
.output
.data
dist
node_modules
*.JPG
*.png
*.webp
*.gltf
*.blend
*.blend1

View File

@@ -9,7 +9,7 @@ COPY . /app
WORKDIR /app WORKDIR /app
FROM base AS prod-deps FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile --ignore-scripts
FROM base AS build FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
@@ -17,6 +17,7 @@ RUN pnpm run build
FROM base FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/.output /app/.output COPY --from=build /app/.output /app/.output
EXPOSE 3000 EXPOSE 3000

View File

@@ -1,4 +1,4 @@
Copyright (c) 2024 Pihkaal <hello@pihkaal.me> Copyright (c) 2024-2026 Pihkaal <hello@pihkaal.me>
Permission is hereby granted, free of charge, to any person Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without files (the "Software"), to deal in the Software without

View File

@@ -20,7 +20,8 @@ useSeoMeta({
</script> </script>
<template> <template>
<div role="main" class="flex min-h-[100vh] items-center justify-center"> <UApp>
<div role="main" class="flex min-h-screen items-center justify-center">
<NuxtRouteAnnouncer /> <NuxtRouteAnnouncer />
<div <div
@@ -30,4 +31,5 @@ useSeoMeta({
<AppBody /> <AppBody />
</div> </div>
</div> </div>
</UApp>
</template> </template>

28
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,28 @@
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--ui-color-primary-50: oklch(98.2% 0.018 155.826);
--ui-color-primary-100: oklch(96.2% 0.044 156.743);
--ui-color-primary-200: oklch(92.5% 0.084 155.995);
--ui-color-primary-300: oklch(87.1% 0.15 154.449);
--ui-color-primary-400: oklch(79.5% 0.22 151.711);
--ui-color-primary-500: oklch(72.5% 0.232 149.579);
--ui-color-primary-600: oklch(62.9% 0.204 149.214);
--ui-color-primary-700: oklch(52.8% 0.162 150.069);
--ui-color-primary-800: oklch(44.9% 0.125 151.328);
--ui-color-primary-900: oklch(39.4% 0.1 152.535);
--ui-color-primary-950: oklch(26.7% 0.068 152.934);
--ui-color-neutral-50: oklch(98.5% 0 0);
--ui-color-neutral-100: oklch(96.7% 0.001 286.375);
--ui-color-neutral-200: oklch(92% 0.004 286.32);
--ui-color-neutral-300: oklch(87.1% 0.006 286.286);
--ui-color-neutral-400: oklch(70.5% 0.015 286.067);
--ui-color-neutral-500: oklch(55.2% 0.016 285.938);
--ui-color-neutral-600: oklch(44.2% 0.017 285.786);
--ui-color-neutral-700: oklch(37% 0.013 285.805);
--ui-color-neutral-800: oklch(27.4% 0.006 286.033);
--ui-color-neutral-900: oklch(21% 0.006 285.885);
--ui-color-neutral-950: oklch(14.1% 0.005 285.823);
}

View File

@@ -1,45 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
const app = useAppStore();
const closeModal = () => {
app.apiModalOpened = false;
};
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>
<UModal v-model="app.apiModalOpened"> <UModal title="API Documentation">
<UCard <template #body>
:ui="{
ring: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
}"
>
<template #header>
<div class="flex items-center justify-between">
<h3
class="text-xl font-semibold leading-6 text-gray-900 dark:text-white"
>
API Documentation
</h3>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="closeModal"
/>
</div>
</template>
<div class="flex flex-col space-y-8"> <div class="flex flex-col space-y-8">
<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>
@@ -47,23 +20,24 @@ const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl);
<div class="space-y-3"> <div class="space-y-3">
<h2 class="font-bold text-lg">Base API URL</h2> <h2 class="font-bold text-lg">Base API URL</h2>
<UButtonGroup size="sm" orientation="horizontal" class="w-full"> <UButtonGroup size="md" class="w-full">
<UInput <UInput
:model-value="baseApiUrl" :model-value="baseApiUrl"
size="sm" size="md"
class="w-full" class="w-full"
disabled disabled
:ui="{ base: '!ps-12 !cursor-text font-mono' }" :ui="{ base: '!ps-12.5 !cursor-text font-mono' }"
> >
<template #leading> <template #leading>
<span <span
class="text-white dark:text-gray-900 bg-primary py-0.5 -mx-1 px-2 text-xs rounded-sm" class="text-white dark:text-gray-900 bg-primary py-0.5 -mx-1 px-2 text-xs rounded-xs font-mono"
>GET</span ><span class="translate-y-px inline-block">GET</span></span
> >
</template> </template>
</UInput> </UInput>
<UButton <UButton
color="gray" color="neutral"
variant="subtle"
:icon="baseApiUrlIcon" :icon="baseApiUrlIcon"
@click="copyBaseApiUrl" @click="copyBaseApiUrl"
/> />
@@ -75,21 +49,21 @@ const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl);
<div <div
class="text-sm flex flex-col space-y-4 font-mono divide-y divide-gray-200 dark:divide-gray-800" class="text-sm flex flex-col space-y-4 font-mono divide-y divide-gray-200 dark:divide-gray-800"
> >
<div class="pt-1 flex justify-between gap-x-5"> <div class="pt-1 pb-4 flex justify-between gap-x-5">
<span class="text-primary font-semibold">format</span> <span class="text-primary font-semibold">format</span>
<span class="text-slate-700 dark:text-slate-200 text-right">{{ <span class="text-slate-700 dark:text-slate-200 text-right">{{
arrayToUnion(IMAGE_FORMATS) arrayToUnion(IMAGE_FORMATS)
}}</span> }}</span>
</div> </div>
<div class="pt-4 flex 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>
<div class="pt-4 flex justify-between gap-x-5"> <div class="flex justify-between gap-x-5">
<span class="text-primary font-semibold">content</span> <span class="text-primary font-semibold">content</span>
<span class="text-slate-700 dark:text-slate-200 text-right" <span class="text-slate-700 dark:text-slate-200 text-right"
>string</span >string</span
@@ -98,6 +72,6 @@ const { copy: copyBaseApiUrl, icon: baseApiUrlIcon } = useCopyable(baseApiUrl);
</div> </div>
</div> </div>
</div> </div>
</UCard> </template>
</UModal> </UModal>
</template> </template>

271
app/components/AppBody.vue Normal file
View File

@@ -0,0 +1,271 @@
<script setup lang="ts">
import { z } from "zod";
import { LazyApiModal } from "#components";
const qrCode = ref("/default.webp");
const form = useTemplateRef("form");
const baseApiUrl = useBaseApiUrl();
const overlay = useOverlay();
const apiModal = overlay.create(LazyApiModal);
const isQRCodeEmpty = computed(() => qrCode.value === "/default.webp");
const { data: logos } = await useFetch("/api/logos");
const logoItems = computed(
() =>
logos.value?.map((name) => ({
label: name,
value: name,
avatar: { src: `/logos/${name}.png`, alt: name },
})) ?? [],
);
const selectedLogoItem = computed({
get: () => logoItems.value?.find((i) => i.value === formState.logo),
set: (item) => {
formState.logo = item?.value as string | undefined;
},
});
const formSchema = z
.object({
hasLogo: z.boolean(),
logo: z.string().optional(),
format: z.enum(IMAGE_FORMATS).default("png"),
content: z
.string()
.optional()
.transform((v) => v ?? ""),
})
.superRefine((data, ctx) => {
if (!data.content)
ctx.addIssue({ code: "custom", message: "Required", path: ["content"] });
if (data.hasLogo && !data.logo)
ctx.addIssue({ code: "custom", message: "Required", path: ["logo"] });
});
const formState = reactive<z.input<typeof formSchema>>({
hasLogo: false,
logo: undefined,
format: IMAGE_FORMATS[0],
content: undefined,
});
const parsedFormState = computed(() => formSchema.safeParse(formState));
watch(formState, async () => {
await nextTick();
form.value?.validate({ silent: true });
updateQRCode();
});
const apiUrl = computed<string>((previous) => {
if (!parsedFormState.value.success) return previous ?? "";
const { content, format, hasLogo, logo } = parsedFormState.value.data;
const params = new URLSearchParams({
...(hasLogo && logo && { logo }),
format,
content,
});
return `${baseApiUrl}?${params}`;
});
const { icon: copyUrlIcon, copy: copyUrl } = useCopyable(apiUrl);
const updateQRCode = async () => {
if (!parsedFormState.value.success) return;
const { content, hasLogo, logo, format } = parsedFormState.value.data;
const logoUrl = hasLogo && logo ? `/logos/${logo}.png` : undefined;
const canvas = await renderQRCodeToCanvas(content, logoUrl);
qrCode.value = canvas.toDataURL(format);
};
const downloadQRCode = () => {
const link = document.createElement("a");
link.href = qrCode.value;
link.download = `qrcode.${formState.format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const {
copy: copyQRCode,
icon: copyImageIcon,
label: copyImageLabel,
} = useCopyable(async () => {
if (isQRCodeEmpty.value) return;
if (!parsedFormState.value.success) return;
const { content, hasLogo, logo } = parsedFormState.value.data;
const logoUrl = hasLogo && logo ? `/logos/${logo}.png` : undefined;
const canvas = await renderQRCodeToCanvas(content, logoUrl);
const qrCode = canvas.toDataURL("png");
const blob = await (await fetch(qrCode)).blob();
const item = new ClipboardItem({ "image/png": blob });
await navigator.clipboard.write([item]);
});
</script>
<template>
<div class="flex flex-col sm:flex-row justify-between gap-4">
<img
: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"
>
<div class="flex-1 flex flex-col justify-center">
<UForm
ref="form"
:schema="formSchema"
:state="formState"
:validate-on="['blur']"
class="space-y-4"
>
<UFormField
label="Username or link"
name="content"
:ui="{ error: 'hidden' }"
>
<UInput
v-model="formState.content"
icon="i-heroicons-user"
placeholder="Your username or profile link"
class="w-full"
/>
</UFormField>
<UFormField name="logo" :ui="{ error: 'hidden' }">
<template #label>
<UCheckbox
v-model="formState.hasLogo"
class="mb-1.5"
label="Logo"
/>
</template>
<USelectMenu
v-model="selectedLogoItem"
:items="logoItems"
:disabled="!formState.hasLogo"
placeholder="Select logo"
class="w-full"
>
<template #leading>
<NuxtImg
v-if="selectedLogoItem"
:src="selectedLogoItem.avatar?.src"
:alt="selectedLogoItem.label"
width="16"
height="16"
class="dark:invert"
/>
<UIcon
v-else
name="i-heroicons-photo"
class="size-4 text-dimmed"
/>
</template>
<template v-if="selectedLogoItem">{{
selectedLogoItem.label
}}</template>
<template #item-leading="{ item }">
<NuxtImg
:src="item.avatar?.src"
:alt="item.label"
width="16"
height="16"
class="dark:invert"
/>
</template>
</USelectMenu>
</UFormField>
<UFormField label="Format" name="format">
<USelectMenu
v-model="formState.format"
icon="i-heroicons-document-duplicate"
:items="unreadonly(IMAGE_FORMATS)"
placeholder="Select format"
class="w-full"
>
<template #item-label="{ item }">
{{ upperCase(item) }}
</template>
<span v-if="formState.format">{{
upperCase(formState.format)
}}</span>
</USelectMenu>
</UFormField>
<UFormField label="API">
<template #hint>
<UButton
size="md"
color="neutral"
variant="link"
icon="i-heroicons-question-mark-circle"
@click="apiModal.open()"
/>
</template>
<UButtonGroup size="sm" class="w-full">
<UInput
v-model="apiUrl"
disabled
placeholder="Please fill all fields first"
class="w-full"
/>
<UButton
color="neutral"
variant="subtle"
:disabled="!apiUrl"
:icon="copyUrlIcon"
@click="copyUrl"
/>
</UButtonGroup>
</UFormField>
<div class="flex space-x-4 pt-2">
<UButton
class="flex-1"
block
:icon="copyImageIcon"
size="md"
color="primary"
variant="solid"
:label="copyImageLabel"
:trailing="false"
:disabled="isQRCodeEmpty"
@click="copyQRCode"
/>
<UButton
class="flex-1"
block
icon="i-heroicons-arrow-down-tray"
size="md"
color="primary"
variant="solid"
label="Download"
:trailing="false"
:disabled="isQRCodeEmpty"
@click="downloadQRCode"
/>
</div>
</UForm>
</div>
</div>
</template>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
const colorMode = useColorMode();
const isDark = computed({
get() {
return colorMode.value === "dark";
},
set() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark";
},
});
</script>
<template>
<div
class="pb-1.5 border-b border-gray-100 dark:border-gray-800 flex justify-between items-center"
>
<h1 class="text-2xl sm:text-4xl font-bold">Simple QRCode</h1>
<ClientOnly>
<div class="flex gap-x-1 animate-fadeIn">
<UButton
color="neutral"
variant="ghost"
icon="i-bi-git"
aria-label="Git repo"
class="w-8 h-8"
target="_blank"
to="https://git.pihkaal.me/simple-qr"
/>
<UButton
:icon="
isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'
"
color="neutral"
variant="ghost"
aria-label="Theme"
class="w-8 h-8"
@click="isDark = !isDark"
/>
</div>
</ClientOnly>
</div>
</template>
<style scoped>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-fadeIn {
animation: fadeIn 200ms ease-in-out forwards;
}
</style>

View File

@@ -0,0 +1,40 @@
export const useCopyable = (
valueOrCallback:
| string
| Ref<string>
| (() => PromiseLike<string>)
| (() => PromiseLike<void>),
) => {
const icon = ref("i-heroicons-clipboard-document");
const label = ref("Copy");
const toast = useToast();
const copy = async () => {
try {
if (typeof valueOrCallback === "function") {
await valueOrCallback();
} else {
const value = unref(valueOrCallback);
await navigator.clipboard.writeText(value);
}
} catch {
toast.add({
title: "Failed to copy to clipboard",
color: "error",
icon: "i-lucide-circle-x",
});
return;
}
icon.value = "i-heroicons-clipboard-document-check";
label.value = "Copied!";
setTimeout(() => {
icon.value = "i-heroicons-clipboard-document";
label.value = "Copy";
}, 3000);
};
return { icon, copy, label };
};

View File

@@ -1,9 +0,0 @@
<template>
<div class="flex flex-col sm:flex-row justify-between gap-4">
<QRCodePreview />
<QRCodeForm />
</div>
<ApiModal />
</template>

View File

@@ -1,210 +0,0 @@
<script setup lang="ts">
const app = useAppStore();
const baseApiUrl = useBaseApiUrl();
const isQRCodeEmpty = computed(() => app.qrCode === "/default.webp");
const firstBlured = ref(false);
const state = reactive({
hasLogo: false,
logo: undefined,
format: IMAGE_FORMATS[0],
content: undefined,
});
const stateErrors = computed(() => ({
content: firstBlured.value && !state.content,
logo: firstBlured.value && state.hasLogo && !state.logo,
}));
const isValidState = computed(
() =>
((state.hasLogo && state.logo) || !state.hasLogo) &&
state.content &&
state.format,
);
const apiUrl = computed((previous) => {
if (!isValidState.value) return previous;
const params = new URLSearchParams({
...(state.hasLogo && { logo: state.logo }),
format: state.format,
content: state.content,
});
return `${baseApiUrl}?${params}`;
});
const isValidApiUrl = computed(() => !!apiUrl.value);
const { icon: copyUrlIcon, copy: copyUrl } = useCopyable(apiUrl);
const openApiModal = () => {
app.apiModalOpened = true;
};
const updateQRCode = async () => {
await nextTick();
if (!isValidState.value) return;
const logoUrl = state.hasLogo ? `/logos/${state.logo}.png` : undefined;
const canvas = await renderQRCodeToCanvas(state.content, logoUrl);
app.qrCode = canvas.toDataURL(`image/${state.format}`);
};
const downloadQRCode = () => {
const link = document.createElement("a");
link.href = app.qrCode;
link.download = `qrcode.${state.format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const {
copy: copyQRCode,
icon: copyImageIcon,
label: copyImageLabel,
} = useCopyable(async () => {
if (isQRCodeEmpty.value) return;
const logoUrl = state.hasLogo ? `/logos/${state.logo}.png` : undefined;
const canvas = await renderQRCodeToCanvas(state.content, logoUrl);
const qrCode = canvas.toDataURL(`image/png`);
const blob = await (await fetch(qrCode)).blob();
const item = new ClipboardItem({ "image/png": blob });
await navigator.clipboard.write([item]);
});
</script>
<template>
<div class="flex-1 flex flex-col justify-center">
<UForm ref="form" :state="state" class="space-y-4">
<UFormGroup
label="Username or link"
name="content"
:error="stateErrors.content"
>
<UInput
v-model="state.content"
icon="i-heroicons-user"
placeholder="Your username or profile link"
@input="updateQRCode"
@blur="firstBlured = true"
/>
</UFormGroup>
<UFormGroup name="logo" :error="stateErrors.logo">
<template #label>
<UCheckbox
v-model="state.hasLogo"
class="mb-1.5"
label="Logo"
@change="updateQRCode"
@blur="firstBlured = true"
/>
</template>
<USelectMenu
v-model="state.logo"
icon="i-heroicons-photo"
:options="LOGOS"
:disabled="!state.hasLogo"
placeholder="Select logo"
searchable
clear-search-on-close
@change="updateQRCode"
@blur="firstBlured = true"
>
<template #label>
<span v-if="state.logo">{{ capitalize(state.logo) }}</span>
</template>
<template #option="props">
<span>{{ capitalize(props.option) }}</span>
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup label="Format" name="format">
<USelectMenu
v-model="state.format"
icon="i-heroicons-document-duplicate"
:options="IMAGE_FORMATS"
placeholder="Select format"
@change="updateQRCode"
@blur="firstBlured = true"
>
<template #label>
<span v-if="state.format">{{ upperCase(state.format) }}</span>
</template>
<template #option="props">
<span>{{ upperCase(props.option) }}</span>
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup label="API">
<template #hint>
<UButton
size="md"
color="gray"
variant="link"
icon="i-heroicons-question-mark-circle"
@click="openApiModal"
/>
</template>
<UButtonGroup size="sm" orientation="horizontal" class="w-full">
<UInput
v-model="apiUrl"
disabled
placeholder="Please fill all fields first"
class="w-full"
/>
<UButton
color="gray"
:disabled="!isValidApiUrl"
:icon="copyUrlIcon"
@click="copyUrl"
/>
</UButtonGroup>
</UFormGroup>
<div class="flex space-x-4 pt-2">
<UButton
class="flex-1"
block
:icon="copyImageIcon"
size="md"
color="primary"
variant="solid"
:label="copyImageLabel"
:trailing="false"
:disabled="isQRCodeEmpty"
@click="copyQRCode"
/>
<UButton
class="flex-1"
block
icon="i-heroicons-arrow-down-tray"
size="md"
color="primary"
variant="solid"
label="Download"
:trailing="false"
:disabled="isQRCodeEmpty"
@click="downloadQRCode"
/>
</div>
</UForm>
</div>
</template>

View File

@@ -1,10 +0,0 @@
<script setup lang="ts">
const app = useAppStore();
</script>
<template>
<img
:src="app.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"
/>
</template>

View File

@@ -1,38 +0,0 @@
<template>
<div
class="pb-1.5 border-b border-gray-100 dark:border-gray-800 flex justify-between items-center"
>
<h1 class="text-2xl sm:text-4xl font-bold">Simple QRCode</h1>
<ClientOnly>
<div class="flex gap-x-1 animate-fadeIn">
<UButton
color="gray"
variant="ghost"
icon="i-uil-github"
aria-label="Github repo"
class="w-8 h-8"
target="_blank"
to="https://github.com/pihkaal/simple-qr"
/>
<ThemeSwitcher />
</div>
</ClientOnly>
</div>
</template>
<style scoped>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-fadeIn {
animation: fadeIn 200ms ease-in-out forwards;
}
</style>

View File

@@ -1,23 +0,0 @@
<script setup lang="ts">
const colorMode = useColorMode();
const isDark = computed({
get() {
return colorMode.value === "dark";
},
set() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark";
},
});
</script>
<template>
<UButton
:icon="isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"
color="gray"
variant="ghost"
aria-label="Theme"
class="w-8 h-8"
@click="isDark = !isDark"
/>
</template>

View File

@@ -1,25 +0,0 @@
export const useCopyable = (
valueOrCallback: string | Ref<string> | (() => PromiseLike<string>),
) => {
const icon = ref("i-heroicons-clipboard-document");
const label = ref("Copy");
const copy = async () => {
if (typeof valueOrCallback === "function") {
await valueOrCallback();
} else {
const value = unref(valueOrCallback);
await navigator.clipboard.writeText(value);
}
icon.value = "i-heroicons-clipboard-document-check";
label.value = "Copied!";
setTimeout(() => {
icon.value = "i-heroicons-clipboard-document";
label.value = "Copy";
}, 3000);
};
return { icon, copy, label };
};

View File

@@ -1,5 +1,3 @@
// @ts-check
import withNuxt from "./.nuxt/eslint.config.mjs"; import withNuxt from "./.nuxt/eslint.config.mjs";
export default withNuxt(); export default withNuxt();
// Your custom configs here

View File

@@ -1,8 +1,9 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: "2024-04-03", compatibilityDate: "2024-11-01",
devtools: { enabled: true }, devtools: { enabled: true },
modules: ["@nuxt/eslint", "@nuxt/ui", "@pinia/nuxt"], modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxt/image"],
css: ["~/assets/css/main.css"],
components: [ components: [
{ {
path: "~/components", path: "~/components",

View File

@@ -1,5 +1,5 @@
{ {
"name": "social-qr", "name": "simple-qr",
"description": "Simple, bullshit-free QR code generator", "description": "Simple, bullshit-free QR code generator",
"author": "Pihkaal <hello@pihkaal.me> (https://pihkaal.me)", "author": "Pihkaal <hello@pihkaal.me> (https://pihkaal.me)",
"private": true, "private": true,
@@ -11,25 +11,28 @@
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare", "postinstall": "nuxt prepare",
"lint": "eslint --fix --cache .", "lint": "eslint --fix --cache .",
"format": "pnpx prettier --cache --write ." "format": "prettier --cache --write ."
}, },
"dependencies": { "dependencies": {
"@iconify-json/heroicons": "^1.2.0", "@nuxt/image": "^2.0.0",
"@iconify-json/uil": "^1.2.1", "jszip": "^3.10.1",
"@nuxt/eslint": "^0.5.7",
"@nuxt/ui": "^2.18.6",
"@pinia/nuxt": "^0.5.5",
"canvas": "^2.11.2",
"nuxt": "^3.13.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sharp": "^0.33.5", "skia-canvas": "^3.0.8",
"vue": "latest", "zod": "^4.3.6"
"vue-router": "latest",
"zod": "^3.23.8"
}, },
"packageManager": "pnpm@9.11.0", "packageManager": "pnpm@10.30.1",
"devDependencies": { "devDependencies": {
"@types/qrcode": "^1.5.5", "@iconify-json/bi": "^1.2.7",
"typescript": "5.5.4" "@iconify-json/heroicons": "^1.2.3",
"@iconify-json/lucide": "^1.2.92",
"@iconify-json/uil": "^1.2.3",
"@nuxt/eslint": "^1.15.1",
"@nuxt/ui": "^3.3.7",
"@types/qrcode": "^1.5.6",
"eslint": "^9.39.1",
"nuxt": "^4.3.1",
"prettier": "^3.8.1",
"tailwindcss": "^4.2.0",
"typescript": "^5.6.3"
} }
} }

15343
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,6 @@
onlyBuiltDependencies:
- esbuild
- sharp
- skia-canvas
- unrs-resolver
- vue-demi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/logos/BeReal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/logos/Bitcoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/Codeberg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/Codecademy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/logos/Diaspora.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logos/Discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logos/Dropbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logos/Ello.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/logos/Facebook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logos/Flickr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
public/logos/Git.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/logos/GitHub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/GitLab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/logos/Gitea.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logos/Instagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/logos/Kik.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
public/logos/Line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/logos/LinkedIn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/logos/Litecoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/logos/Mastodon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logos/Medium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logos/Messenger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/logos/Monero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logos/Napster.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/logos/OnlyFans.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/logos/Patreon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
public/logos/PayPal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/logos/PeerTube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
public/logos/Pinterest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/logos/Reddit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/logos/Session.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/Signal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/logos/Snapchat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logos/Spotify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/logos/Substack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/logos/Telegram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/logos/Threema.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/logos/Twitch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
public/logos/Venmo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
public/logos/Viber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/logos/WeChat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/logos/WhatsApp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/logos/X.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/YouTube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
public/logos/Zoom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/iMessage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Some files were not shown because too many files have changed in this diff Show More