Skip to content

Commit e301f96

Browse files
committed
add frame font feature
1 parent 2cdd0a1 commit e301f96

10 files changed

Lines changed: 323 additions & 29 deletions

src/components/BatchExportFieldsGuide.vue

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const closeAccordion = () => {
1818
}
1919
2020
const simpleFields: Array<{
21-
name: keyof SimpleCSVData
21+
name: string
2222
required: boolean
2323
description: string
2424
example: string
@@ -40,11 +40,17 @@ const simpleFields: Array<{
4040
required: false,
4141
description: 'Custom filename for the exported QR code',
4242
example: 'my_custom_name'
43+
},
44+
{
45+
name: 'frameFontFamily',
46+
required: false,
47+
description: 'Font family for the frame text (must be one of the predefined options)',
48+
example: 'Poppins'
4349
}
4450
]
4551
4652
const vCardFields: Array<{
47-
name: keyof VCardCSVData
53+
name: string
4854
required: boolean
4955
description: string
5056
example: string
@@ -75,17 +81,23 @@ const vCardFields: Array<{
7581
required: false,
7682
description: 'Custom filename for the exported QR code',
7783
example: 'john_doe_contact'
84+
},
85+
{
86+
name: 'frameFontFamily',
87+
required: false,
88+
description: 'Font family for the frame text (must be one of the predefined options)',
89+
example: 'Roboto'
7890
}
7991
]
8092
81-
const simpleCsvExample = `url,frameText,fileName
82-
https://example.com,Visit us,site
83-
https://github.com/user,GitHub,github
84-
https://linkedin.com,LinkedIn,linkedin`
93+
const simpleCsvExample = `url,frameText,fileName,frameFontFamily
94+
https://example.com,Visit us,site,Poppins
95+
https://github.com/user,GitHub,github,Roboto
96+
https://linkedin.com,LinkedIn,linkedin,`
8597
86-
const vCardCsvExample = `firstName,lastName,org,email,frameText
87-
John,Doe,Acme,john@ex.com,Contact
88-
Jane,Smith,Tech,jane@ex.com,Manager`
98+
const vCardCsvExample = `firstName,lastName,org,email,frameText,frameFontFamily
99+
John,Doe,Acme,john@ex.com,Contact,Roboto
100+
Jane,Smith,Tech,jane@ex.com,Manager,`
89101
</script>
90102

91103
<template>

src/components/QRCodeCreate.vue

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,18 @@ import { getNumericCSSValue } from '@/utils/formatting'
3636
import {
3737
allFramePresets,
3838
defaultFramePreset,
39+
FONT_OPTIONS,
40+
loadGoogleFont,
3941
type FramePreset,
4042
type FrameStyle
4143
} from '@/utils/framePresets'
4244
import { allQrCodePresets, defaultPreset, type Preset } from '@/utils/qrCodePresets'
4345
import {
4446
CUSTOM_LOADED_PRESET_KEYS,
45-
LAST_LOADED_LOCALLY_PRESET_KEY,
46-
LOADED_FROM_FILE_PRESET_KEY,
4747
hasStoredQRConfig,
4848
isLocalStorageEnabled,
49+
LAST_LOADED_LOCALLY_PRESET_KEY,
50+
LOADED_FROM_FILE_PRESET_KEY,
4951
loadQRConfig,
5052
saveQRConfig,
5153
serializeQRConfig,
@@ -378,6 +380,17 @@ const frameSettings = computed(() => ({
378380
position: frameTextPosition.value,
379381
style: frameStyle.value
380382
}))
383+
384+
function onFontFamilyChange(value: string): Promise<void> {
385+
// value may be a label name (e.g. "Poppins" from CSV) or a full CSS value (e.g. "'Poppins', sans-serif" from UI)
386+
const font = FONT_OPTIONS.find((f) => f.value === value || f.label === value)
387+
const resolvedValue = font ? font.value : value
388+
frameStyle.value = { ...frameStyle.value, fontFamily: resolvedValue || undefined }
389+
if (font?.googleFontName) {
390+
return loadGoogleFont(font.googleFontName)
391+
}
392+
return Promise.resolve()
393+
}
381394
//#endregion
382395
383396
//#region /* Frame text autofill */ Fill if empty */
@@ -539,7 +552,7 @@ function downloadQRImage(format: 'png' | 'svg' | 'jpg') {
539552
el,
540553
formatConfig.filename,
541554
{ ...getExportDimensions(), ...formatConfig.extraOptions },
542-
styledBorderRadiusFormatted.value
555+
exportBorderRadius.value
543556
)
544557
} else {
545558
generateBatchQRCodes(format)
@@ -587,6 +600,14 @@ function applyQRConfig(config: QRCodeConfig, key?: string) {
587600
frameTextPosition.value = config.frame.position || 'bottom'
588601
frameStyle.value = { ...frameStyle.value, ...config.frame.style }
589602
603+
const restoredFontFamily = config.frame.style.fontFamily
604+
if (restoredFontFamily) {
605+
const font = FONT_OPTIONS.find((f) => f.value === restoredFontFamily)
606+
if (font?.googleFontName) {
607+
loadGoogleFont(font.googleFontName)
608+
}
609+
}
610+
590611
const framePreset: FramePreset = {
591612
name: key || LAST_LOADED_LOCALLY_PRESET_KEY,
592613
style: config.frame.style,
@@ -672,6 +693,7 @@ const exportMode = ref(ExportMode.Single)
672693
const dataStringsFromCsv = ref<string[]>([])
673694
const frameTextsFromCsv = ref<string[]>([])
674695
const fileNamesFromCsv = ref<string[]>([])
696+
const fontFamiliesFromCsv = ref<string[]>([])
675697
676698
const inputFileForBatchEncoding = ref<File | null>(null)
677699
const fileInput = ref<HTMLInputElement | null>(null)
@@ -709,6 +731,7 @@ const resetData = () => {
709731
dataStringsFromCsv.value = []
710732
frameTextsFromCsv.value = []
711733
fileNamesFromCsv.value = []
734+
fontFamiliesFromCsv.value = []
712735
isValidCsv.value = true
713736
resetBatchExportProgress()
714737
isBatchExportSuccess.value = false
@@ -727,6 +750,10 @@ watch(previewRowIndex, (newIndex) => {
727750
) {
728751
data.value = dataStringsFromCsv.value[newIndex]
729752
frameText.value = frameTextsFromCsv.value[newIndex] || defaultFrameText.value
753+
const fontFamily = fontFamiliesFromCsv.value[newIndex]
754+
if (fontFamily !== undefined) {
755+
onFontFamilyChange(fontFamily)
756+
}
730757
}
731758
})
732759
@@ -778,6 +805,7 @@ const onBatchInputFileUpload = (event: Event) => {
778805
dataStringsFromCsv.value = batchResult.urls
779806
frameTextsFromCsv.value = batchResult.frameTexts
780807
fileNamesFromCsv.value = batchResult.fileNames
808+
fontFamiliesFromCsv.value = batchResult.fontFamilies
781809
showFrame.value = batchResult.hasCustomFrameText
782810
isValidCsv.value = true
783811
previewRowIndex.value = 0 // Reset preview to first row on new upload
@@ -786,6 +814,10 @@ const onBatchInputFileUpload = (event: Event) => {
786814
if (batchResult.urls.length > 0) {
787815
data.value = batchResult.urls[0]
788816
frameText.value = batchResult.frameTexts[0] || defaultFrameText.value
817+
const firstFontFamily = batchResult.fontFamilies[0]
818+
if (firstFontFamily) {
819+
onFontFamilyChange(firstFontFamily)
820+
}
789821
}
790822
}
791823
@@ -837,22 +869,26 @@ async function generateBatchQRCodes(format: 'png' | 'svg' | 'jpg') {
837869
currentExportedQrCodeIndex.value = index
838870
const url = dataStringsFromCsv.value[index]
839871
const currentFrameText = frameTextsFromCsv.value[index]
872+
const currentFontFamily = fontFamiliesFromCsv.value[index]
840873
data.value = url
841874
frameText.value = currentFrameText
875+
if (currentFontFamily !== undefined) {
876+
await onFontFamilyChange(currentFontFamily)
877+
}
842878
await sleep(1000)
843879
let dataUrl: string = ''
844880
if (format === 'png') {
845-
dataUrl = await getPngElement(el, getExportDimensions(), styledBorderRadiusFormatted.value)
881+
dataUrl = await getPngElement(el, getExportDimensions(), exportBorderRadius.value)
846882
} else if (format === 'jpg') {
847883
const jpgBgcolor =
848884
styleBackground.value === 'transparent' ? '#ffffff' : styleBackground.value
849885
dataUrl = await getJpgElement(
850886
el,
851887
{ ...getExportDimensions(), bgcolor: jpgBgcolor },
852-
styledBorderRadiusFormatted.value
888+
exportBorderRadius.value
853889
)
854890
} else {
855-
dataUrl = await getSvgString(el, getExportDimensions(), styledBorderRadiusFormatted.value)
891+
dataUrl = await getSvgString(el, getExportDimensions(), exportBorderRadius.value)
856892
}
857893
createZipFile(zip, dataUrl, index, format)
858894
numQrCodesCreated++
@@ -1425,6 +1461,26 @@ const updateDataFromModal = (newData: string) => {
14251461
placeholder="16px"
14261462
/>
14271463
</div>
1464+
<div class="sm:col-span-2">
1465+
<label for="frame-font-family" class="mb-1 block text-sm">{{
1466+
t('Font family')
1467+
}}</label>
1468+
<select
1469+
id="frame-font-family"
1470+
class="w-full text-input"
1471+
:value="frameStyle.fontFamily ?? ''"
1472+
@change="onFontFamilyChange(($event.target as HTMLSelectElement).value)"
1473+
>
1474+
<option
1475+
v-for="font in FONT_OPTIONS"
1476+
:key="font.value"
1477+
:value="font.value"
1478+
:style="font.value ? { fontFamily: font.value } : {}"
1479+
>
1480+
{{ font.label }}
1481+
</option>
1482+
</select>
1483+
</div>
14281484
</div>
14291485
</div>
14301486
</template>
@@ -1626,6 +1682,17 @@ const updateDataFromModal = (newData: string) => {
16261682
{{ fileNamesFromCsv[previewRowIndex] }}
16271683
</code>
16281684
</div>
1685+
<div v-if="fontFamiliesFromCsv[previewRowIndex]">
1686+
<span
1687+
class="text-xs font-medium text-gray-500 dark:text-gray-400"
1688+
>{{ $t('Font family') }}</span
1689+
>
1690+
<code
1691+
class="rounded bg-white px-2 py-1 font-mono text-sm dark:bg-gray-900"
1692+
>
1693+
{{ fontFamiliesFromCsv[previewRowIndex] }}
1694+
</code>
1695+
</div>
16291696
</div>
16301697
</div>
16311698
<div class="mt-2 flex items-center justify-between">

src/components/QRCodeFrame.vue

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface FrameStyle {
66
borderWidth?: string
77
borderRadius?: string
88
padding?: string
9+
fontFamily?: string
910
}
1011
1112
interface Props {
@@ -44,16 +45,17 @@ withDefaults(defineProps<Props>(), {
4445
}"
4546
>
4647
<slot name="qr-code"></slot>
47-
<p
48-
:style="{
49-
color: frameStyle.textColor,
50-
margin: 0,
51-
textAlign: 'center',
52-
[textPosition === 'left' || textPosition === 'right' ? 'width' : 'maxWidth']: '200px',
53-
whiteSpace: 'pre-line'
54-
}"
55-
>
56-
{{ frameText }}
57-
</p>
48+
<p
49+
:style="{
50+
color: frameStyle.textColor,
51+
fontFamily: frameStyle.fontFamily || undefined,
52+
margin: 0,
53+
textAlign: 'center',
54+
[textPosition === 'left' || textPosition === 'right' ? 'width' : 'maxWidth']: '200px',
55+
whiteSpace: 'pre-line'
56+
}"
57+
>
58+
{{ frameText }}
59+
</p>
5860
</div>
5961
</template>

src/utils/csv.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,53 @@ https://docs.example.com/,,documentation`
107107
})
108108
})
109109

110+
it('parses simple CSV with frameFontFamily column correctly', () => {
111+
const csvContent = `url,frameText,fileName,frameFontFamily
112+
https://example.com,Visit us,site,Poppins
113+
https://github.com,GitHub,github,Roboto
114+
https://linkedin.com,LinkedIn,linkedin,`
115+
116+
const result = parseCSV(csvContent)
117+
expect(result.isValid).toBe(true)
118+
expect(result.data).toHaveLength(3)
119+
expect(result.data[0]).toEqual({
120+
url: 'https://example.com',
121+
frameText: 'Visit us',
122+
fileName: 'site',
123+
frameFontFamily: 'Poppins'
124+
})
125+
expect(result.data[1]).toEqual({
126+
url: 'https://github.com',
127+
frameText: 'GitHub',
128+
fileName: 'github',
129+
frameFontFamily: 'Roboto'
130+
})
131+
expect(result.data[2]).toEqual({
132+
url: 'https://linkedin.com',
133+
frameText: 'LinkedIn',
134+
fileName: 'linkedin',
135+
frameFontFamily: undefined
136+
})
137+
})
138+
139+
it('parses vCard CSV with frameFontFamily column correctly', () => {
140+
const csvContent = `firstname,lastname,email,frameText,frameFontFamily
141+
John,Doe,john@example.com,Contact John,Poppins
142+
Jane,Smith,jane@example.com,Contact Jane,`
143+
144+
const result = parseCSV(csvContent)
145+
expect(result.isValid).toBe(true)
146+
expect(result.data).toHaveLength(2)
147+
expect(result.data[0]).toMatchObject({
148+
firstName: 'John',
149+
lastName: 'Doe',
150+
email: 'john@example.com',
151+
frameText: 'Contact John',
152+
frameFontFamily: 'Poppins'
153+
})
154+
expect((result.data[1] as any).frameFontFamily).toBeUndefined()
155+
})
156+
110157
it('handles empty CSV content', () => {
111158
const result = parseCSV('')
112159
expect(result.isValid).toBe(false)

src/utils/csv.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export interface SimpleCSVData {
22
url: string
33
frameText?: string
44
fileName?: string
5+
frameFontFamily?: string
56
}
67

78
export type VCardOptionalFields =
@@ -24,6 +25,7 @@ export interface VCardCSVData extends Partial<Record<VCardOptionalFields, string
2425
firstName: string
2526
lastName: string
2627
fileName?: string
28+
frameFontFamily?: string
2729
}
2830

2931
export type CSVData = SimpleCSVData | VCardCSVData
@@ -163,6 +165,12 @@ export const parseCSV = (csvContent: string): CSVParsingResult => {
163165
vCardData.fileName = values[fileNameIndex] || ''
164166
}
165167

168+
// Handle frameFontFamily separately (CSV column: frameFontFamily)
169+
const frameFontFamilyIndex = headers.indexOf('framefontfamily')
170+
if (frameFontFamilyIndex !== -1 && values[frameFontFamilyIndex]) {
171+
vCardData.frameFontFamily = values[frameFontFamilyIndex]
172+
}
173+
166174
data.push(vCardData as VCardCSVData)
167175
}
168176
} else {
@@ -187,7 +195,10 @@ export const parseCSV = (csvContent: string): CSVParsingResult => {
187195
values.push(currentValue.trim().replace(/^["']|["']$/g, ''))
188196

189197
const [url, frameText, fileName] = values
190-
data.push({ url, frameText, fileName })
198+
const frameFontFamilyIndex = headers.indexOf('framefontfamily')
199+
const frameFontFamily =
200+
frameFontFamilyIndex >= 0 ? values[frameFontFamilyIndex] || undefined : undefined
201+
data.push({ url, frameText, fileName, frameFontFamily })
191202
}
192203
}
193204

0 commit comments

Comments
 (0)