added Contact Info QR Code generator

This commit is contained in:
Taltos 2025-03-07 10:58:26 +01:00
parent a6fc0b5448
commit 2a5fa4bab1
6 changed files with 116 additions and 135 deletions

View file

@ -257,6 +257,10 @@ tools:
title: WiFi QR Code generator title: WiFi QR Code generator
description: Generate and download QR codes for quick connections to WiFi networks. description: Generate and download QR codes for quick connections to WiFi networks.
qr-contact-info-generator:
title: QR Contact Info generator
description: Generate and download QR codes for Contact information (vCard), and customize the background and foreground colors.
xml-formatter: xml-formatter:
title: XML formatter title: XML formatter
description: Prettify your XML string into a friendly, human-readable format. description: Prettify your XML string into a friendly, human-readable format.

View file

@ -142,7 +142,7 @@ export const toolsByCategory: ToolCategory[] = [
}, },
{ {
name: 'Images and videos', name: 'Images and videos',
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], components: [qrCodeGenerator, wifiQrCodeGenerator, qrContactInfoGenerator, svgPlaceholderGenerator, cameraRecorder],
}, },
{ {
name: 'Development', name: 'Development',

View file

@ -1,13 +1,13 @@
import { Qrcode } from '@vicons/tabler'; import { Qrcode } from "@vicons/tabler";
import { defineTool } from '../tool'; import { defineTool } from "../tool";
import { translate } from '@/plugins/i18n.plugin'; import { translate } from "@/plugins/i18n.plugin";
export const tool = defineTool({ export const tool = defineTool({
name: translate('tools.qr-contact-info-generator.title'), name: translate("tools.qr-contact-info-generator.title"),
path: '/qr-contact-info-generator', path: "/qr-contact-info-generator",
description: translate('tools.qr-contact-info-generator.description'), description: translate("tools.qr-contact-info-generator.description"),
keywords: ['qr', 'contact', 'info', 'generator'], keywords: ["qr", "contact", "vcard", "generator", "business", "networking"],
component: () => import('./qr-contact-info-generator.vue'), component: () => import("./qr-contact-info-generator.vue"),
icon: Qrcode, icon: Qrcode,
createdAt: new Date('2025-02-17'), createdAt: new Date(),
}); });

View file

@ -1,56 +0,0 @@
import { type MaybeRef, get } from '@vueuse/core';
import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode';
import { isRef, ref, watch } from 'vue';
interface ContactInfo {
firstName: string;
lastName: string;
jobRole: string;
phoneNumber: string;
emailAddress: string;
companyName: string;
companyWebsite: string;
companyAddress: string;
}
interface QRCodeOptions {
foregroundColor?: string;
backgroundColor?: string;
}
export function generateContactQRCode(contact: MaybeRef<ContactInfo>, options: MaybeRef<QRCodeOptions> = {}): Promise<string> {
const vCard = ref(`
BEGIN:VCARD
VERSION:3.0
FN:${get(contact).firstName} ${get(contact).lastName}
ORG:${get(contact).companyName}
TITLE:${get(contact).jobRole}
TEL:${get(contact).phoneNumber}
EMAIL:${get(contact).emailAddress}
URL:${get(contact).companyWebsite}
ADR:${get(contact).companyAddress}
END:VCARD
`);
const qrOptions: QRCodeToDataURLOptions = {
errorCorrectionLevel: 'H' as QRCodeErrorCorrectionLevel,
type: 'image/png',
margin: 1,
color: {
dark: get(options).foregroundColor || '#000000ff',
light: get(options).backgroundColor || '#ffffffff',
},
};
const qrCodeDataUrl = ref('');
watch(
[vCard, options].filter(isRef),
async () => {
qrCodeDataUrl.value = await QRCode.toDataURL(get(vCard), qrOptions);
},
{ immediate: true },
);
return qrCodeDataUrl.value;
}

View file

@ -1,77 +1,61 @@
<template> <script setup lang="ts">
<div> import { useContactQRCode } from "./useContactQRCode";
<h1>Contact QR Code Generator</h1> import { useDownloadFileFromBase64 } from "@/composable/downloadBase64";
<form @submit.prevent="generateQRCode">
<input v-model="contact.firstName" placeholder="First Name" required />
<input v-model="contact.lastName" placeholder="Last Name" required />
<input v-model="contact.jobRole" placeholder="Job Role" required />
<input v-model="contact.phoneNumber" placeholder="Phone Number" required />
<input v-model="contact.emailAddress" placeholder="Email Address" required />
<input v-model="contact.companyName" placeholder="Company Name" required />
<input v-model="contact.companyWebsite" placeholder="Company Website" required />
<input v-model="contact.companyAddress" placeholder="Company Address" required />
<input v-model="foregroundColor" placeholder="Foreground Color" />
<input v-model="backgroundColor" placeholder="Background Color" />
<button type="submit">Generate QR Code</button>
</form>
<div v-if="qrCodeDataUrl">
<img :src="qrCodeDataUrl" alt="Contact QR Code" />
</div>
</div>
</template>
<script lang="ts"> const fullName = ref("");
import { defineComponent, ref } from 'vue'; const jobRole = ref("");
import { generateContactQRCode } from './qr-contact-info-generator'; const phoneNumber = ref("");
const email = ref("");
const website = ref("");
const address = ref("");
export default defineComponent({ const foreground = ref("#000000ff");
name: 'ContactQRCodeGenerator', const background = ref("#ffffffff");
setup() {
const contact = ref({
firstName: '',
lastName: '',
jobRole: '',
phoneNumber: '',
emailAddress: '',
companyName: '',
companyWebsite: '',
companyAddress: ''
});
const foregroundColor = ref('#000000ff');
const backgroundColor = ref('#ffffffff');
const qrCodeDataUrl = ref<string | null>(null);
const generateQRCode = async () => { const { qrcode } = useContactQRCode({
qrCodeDataUrl.value = await generateContactQRCode(contact.value, { fullName,
foregroundColor: foregroundColor.value, jobRole,
backgroundColor: backgroundColor.value phoneNumber,
}); email,
}; website,
address,
color: { foreground, background },
options: { width: 1024 },
});
return { const { download } = useDownloadFileFromBase64({
contact, source: qrcode,
foregroundColor, filename: "contact-info-qr.png",
backgroundColor,
qrCodeDataUrl,
generateQRCode
};
}
}); });
</script> </script>
<style scoped> <template>
form { <c-card>
display: flex; <div grid grid-cols-1 gap-12>
flex-direction: column; <div>
gap: 10px; <c-input-text v-model:value="fullName" label="Full Name" placeholder="John Doe" mb-4 />
} <c-input-text v-model:value="jobRole" label="Job Role" placeholder="Software Engineer" mb-4 />
<c-input-text v-model:value="phoneNumber" label="Phone Number" placeholder="+1 234 567 8901" mb-4 />
<c-input-text v-model:value="email" label="Email Address" placeholder="john.doe@example.com" mb-4 />
<c-input-text v-model:value="website" label="Website" placeholder="https://acme.com" mb-4 />
<c-input-text v-model:value="address" label="Company Address" placeholder="123 Main St, City" mb-4 />
button { <n-form label-width="130" label-placement="left">
margin-top: 10px; <n-form-item label="Foreground color:">
} <n-color-picker v-model:value="foreground" :modes="['hex']" />
</n-form-item>
<n-form-item label="Background color:">
<n-color-picker v-model:value="background" :modes="['hex']" />
</n-form-item>
</n-form>
</div>
img { <div v-if="qrcode">
margin-top: 20px; <div flex flex-col items-center gap-3>
max-width: 100%; <img alt="contact-info-qrcode" :src="qrcode" width="200" />
} <c-button @click="download">Download QR Code</c-button>
</style> </div>
</div>
</div>
</c-card>
</template>

View file

@ -0,0 +1,49 @@
import { computed, ref, watchEffect } from "vue";
import QRCode, { type QRCodeToDataURLOptions } from "qrcode";
import { get, MaybeRef } from "@vueuse/core";
interface IContactQRCodeOptions {
fullName: MaybeRef<string>;
jobRole: MaybeRef<string>;
phoneNumber: MaybeRef<string>;
email: MaybeRef<string>;
website: MaybeRef<string>;
address: MaybeRef<string>;
color: { foreground: MaybeRef<string>; background: MaybeRef<string> };
options?: QRCodeToDataURLOptions;
}
// Computed vCard to ensure reactivity works correctly
const useVCard = (options: IContactQRCodeOptions) => {
return computed(() => {
return `BEGIN:VCARD
VERSION:3.0
${get(options.fullName) ? `FN:${get(options.fullName)}` : ""}
${get(options.jobRole) ? `TITLE:${get(options.jobRole)}` : ""}
${get(options.phoneNumber) ? `TEL:${get(options.phoneNumber)}` : ""}
${get(options.email) ? `EMAIL:${get(options.email)}` : ""}
${get(options.website) ? `URL:${get(options.website)}` : ""}
${get(options.address) ? `ADR:${get(options.address)}` : ""}
END:VCARD`.trim();
});
};
export function useContactQRCode(options: IContactQRCodeOptions) {
const qrcode = ref("");
const vCardText = useVCard(options); // Computed property for reactivity
watchEffect(async () => {
if (vCardText.value) {
qrcode.value = await QRCode.toDataURL(vCardText.value, {
color: {
dark: get(options.color.foreground),
light: get(options.color.background),
},
errorCorrectionLevel: "M",
...options.options,
});
}
});
return { qrcode };
}