Skip to content

Commit

Permalink
Add UI customization and refactor code
Browse files Browse the repository at this point in the history
- Introduced a new app configuration file for defining UI colors
- Refactored the main Vue component to use an array of fields, making it easier to add or remove fields in the future
- Added more customization options for the signature preview, including font size, gap between elements, image size, text color, background color and alignment
- Created new components for layout navigation bar and signature preview
- Updated Nuxt configuration to include custom icon collections and added @nuxt/ui module
- Removed unused rename.sh script from scripts directory
- Added new types for form data and options related to signatures
  • Loading branch information
HugoRCD committed Aug 30, 2024
1 parent a6cb8e8 commit 13d94fc
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 134 deletions.
6 changes: 6 additions & 0 deletions app/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default defineAppConfig({
ui: {
primary: 'cyan',
gray: 'neutral',
},
})
209 changes: 101 additions & 108 deletions app/app.vue
Original file line number Diff line number Diff line change
@@ -1,125 +1,118 @@
<script setup lang="ts">
const fullName = ref('')
const jobTitle = ref('')
const company = ref('')
const email = ref('')
const phone = ref('')
const imagePreview = ref('')
const copyButtonText = ref('Copy Signature')
import type { Alignment, SignatureFormData, SignatureOptions } from '~~/types'
const onFileChange = (e) => {
const file = e.target.files[0]
if (file) {
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => {
imagePreview.value = e.target.result
}
reader.readAsDataURL(file)
} else {
alert('Please select an image file')
e.target.value = ''
}
}
}
const fields = [
{ name: 'image', label: 'Profile Picture', type: 'text' },
{ name: 'fullName', label: 'Full Name', type: 'text' },
{ name: 'jobTitle', label: 'Job Title', type: 'text' },
{ name: 'company', label: 'Company', type: 'text' },
{ name: 'email', label: 'Email', type: 'email' },
{ name: 'phone', label: 'Phone', type: 'tel' },
]
const data = ref<SignatureFormData>({
image: 'https://avatars.githubusercontent.com/u/71938701?v=4',
fullName: 'Hugo Richard',
jobTitle: 'Developer',
company: 'Apple',
email: '[email protected]',
phone: '0621562218',
})
const generateSignatureHTML = computed(() => {
return `
<table style="font-family: Arial, sans-serif; font-size: 14px; line-height: 1.4; color: #333333;">
<tr>
<td style="vertical-align: top; padding-right: 15px;">
${imagePreview.value ? `<img src="${imagePreview.value}" alt="Profile Picture" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover;">` : ''}
</td>
<td style="vertical-align: top;">
<strong style="font-size: 16px; color: #000000;">${fullName.value}</strong><br>
${jobTitle.value}<br>
${company.value}<br>
${email.value}<br>
${phone.value}
</td>
</tr>
</table>
`
const options = ref<SignatureOptions>({
fontSize: 14,
gap: 10,
imageSize: 80,
textColor: '#000000',
backgroundColor: '#ffffff',
align: 'center',
})
const copySignature = async () => {
try {
await navigator.clipboard.writeText(generateSignatureHTML.value)
copyButtonText.value = 'Copied!'
setTimeout(() => {
copyButtonText.value = 'Copy Signature'
}, 2000)
} catch (err) {
console.error('Failed to copy signature: ', err)
copyButtonText.value = 'Failed to copy'
}
function setAlign(newAlign: Alignment) {
options.value.align = newAlign
}
</script>

<template>
<div class="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
<div class="relative py-3 sm:max-w-xl sm:mx-auto">
<div class="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
<div class="max-w-md mx-auto">
<h1 class="text-2xl font-semibold mb-6 text-center">Email Signature Generator</h1>
<div class="divide-y divide-gray-200">
<div class="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
<div class="flex flex-col">
<label class="leading-loose">Profile Picture</label>
<input type="file" @change="onFileChange" accept="image/*" class="px-4 py-2 border focus:ring-gray-500 focus:border-gray-900 w-full sm:text-sm border-gray-300 rounded-md focus:outline-none text-gray-600">
<img v-if="imagePreview" :src="imagePreview" alt="Profile Picture Preview" class="mt-2 w-24 h-24 object-cover rounded-full">
</div>
<div class="flex flex-col">
<label class="leading-loose">Full Name</label>
<input type="text" v-model="fullName" class="px-4 py-2 border focus:ring-gray-500 focus:border-gray-900 w-full sm:text-sm border-gray-300 rounded-md focus:outline-none text-gray-600" placeholder="John Doe">
</div>
<div class="flex flex-col">
<label class="leading-loose">Job Title</label>
<input type="text" v-model="jobTitle" class="px-4 py-2 border focus:ring-gray-500 focus:border-gray-900 w-full sm:text-sm border-gray-300 rounded-md focus:outline-none text-gray-600" placeholder="Software Engineer">
</div>
<div class="flex flex-col">
<label class="leading-loose">Company</label>
<input type="text" v-model="company" class="px-4 py-2 border focus:ring-gray-500 focus:border-gray-900 w-full sm:text-sm border-gray-300 rounded-md focus:outline-none text-gray-600" placeholder="Acme Inc.">
</div>
<div class="flex flex-col">
<label class="leading-loose">Email</label>
<input type="email" v-model="email" class="px-4 py-2 border focus:ring-gray-500 focus:border-gray-900 w-full sm:text-sm border-gray-300 rounded-md focus:outline-none text-gray-600" placeholder="[email protected]">
</div>
<div class="flex flex-col">
<label class="leading-loose">Phone</label>
<input type="tel" v-model="phone" class="px-4 py-2 border focus:ring-gray-500 focus:border-gray-900 w-full sm:text-sm border-gray-300 rounded-md focus:outline-none text-gray-600" placeholder="+1 (555) 123-4567">
</div>
<Html class="h-full">
<Body class="h-screen">
<LayoutNavbar />
<div class="flex justify-center bg-black text-white">
<div class="relative mx-auto flex-1 bg-gray-900 p-5 shadow-lg sm:max-w-xl">
<div class="space-y-6">
<div v-for="field in fields" :key="field.name">
<label :for="field.name" class="mb-2 block text-sm font-medium">{{ field.label }}</label>
<UInput :id="field.name" v-model="data[field.name]" :type="field.type" />
</div>
<div class="pt-4 text-base leading-6 font-bold sm:text-lg sm:leading-7">
<p class="text-gray-900">Signature Preview</p>
<div class="mt-2 p-4 border rounded-md">
<div class="flex items-center space-x-4">
<img v-if="imagePreview" :src="imagePreview" alt="Profile Picture" class="w-16 h-16 object-cover rounded-full">
<div class="text-sm">
<p class="font-bold text-gray-900">{{ fullName }}</p>
<p class="text-gray-600">{{ jobTitle }}</p>
<p class="text-gray-600">{{ company }}</p>
<p class="text-gray-600">{{ email }}</p>
<p class="text-gray-600">{{ phone }}</p>
</div>

<div>
<label class="mb-2 block text-sm font-medium">Customization</label>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="fontSize" class="mb-1 block text-xs">Font Size</label>
<URange
id="fontSize"
v-model="options.fontSize"
type="range"
:min="12"
:max="24"
/>
</div>
<div>
<label for="gap" class="mb-1 block text-xs">Gap</label>
<URange
id="gap"
v-model="options.gap"
type="range"
:min="0"
:max="20"
class="w-full"
/>
</div>
<div>
<label for="imageSize" class="mb-1 block text-xs">Image Size</label>
<URange
id="imageSize"
v-model="options.imageSize"
type="range"
:min="40"
:max="120"
class="w-full"
/>
</div>
<div>
<label for="textColor" class="mb-1 block text-xs">Text Color</label>
<UInput id="textColor" v-model="options.textColor" type="color" />
</div>
<div class="flex flex-col">
<label for="backgroundColor" class="mb-1 block text-xs">Background Color</label>
<UInput id="backgroundColor" v-model="options.backgroundColor" type="color" />
</div>
<div>
<label for="align" class="mb-1 block text-xs">Alignment</label>
<UButtonGroup label="Alignment" class="w-full" orientation="horizontal">
<UButton
v-for="alignment in ['top', 'center', 'bottom']"
:key="alignment"
:color="alignment === options.align ? 'primary' : 'gray'"
@click="setAlign(alignment)"
>
{{ alignment }}
</UButton>
</UButtonGroup>
</div>
</div>
<div class="mt-4">
<button
@click="copySignature"
class="w-full px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
:disabled="!fullName"
:class="{ 'opacity-50 cursor-not-allowed': !fullName }"
aria-live="polite"
>
{{ copyButtonText }}
</button>
</div>
</div>
</div>
</div>
<div class="flex flex-1 flex-col items-center justify-center">
<SignaturePreview
:data
:options
/>
</div>
</div>
</div>
</div>
<UNotifications />
</Body>
</Html>
</template>

69 changes: 69 additions & 0 deletions app/components/SignaturePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { Signature } from '~~/types'
const { data, options } = defineProps<Signature>()
const toast = useToast()
const copyButtonText = ref('Copy Signature')
const generateSignatureHTML = computed(() => {
return `
<table style="
font-family: Arial,
sans-serif; font-size: ${options.fontSize}px;
line-height: 1.4;
color: ${options.textColor};
background-color: ${options.backgroundColor};
width: 100%;
">
<tr>
<td style="vertical-align: top; padding-right: ${options.gap}px;">
${data.image ? `<img src="${data.image}" alt="Profile Picture" style="width: ${options.imageSize}px; height: ${options.imageSize}px; border-radius: 50%; object-fit: cover;">` : ''}
</td>
<td style="vertical-align: top;">
<strong style="font-size: ${options.fontSize + 2}px;">${data.fullName}</strong><br>
${data.jobTitle}<br>
${data.company}<br>
${data.email}<br>
${data.phone}
</td>
</tr>
</table>
`
})
const copySignature = async () => {
try {
await navigator.clipboard.writeText(generateSignatureHTML.value)
copyButtonText.value = 'Copied!'
toast.add({ title: 'Copied!', description: 'Signature copied to clipboard', type: 'success', icon: 'lucide:clipboard-check' })
setTimeout(() => {
copyButtonText.value = 'Copy Signature'
}, 2000)
} catch (err) {
console.error('Failed to copy signature: ', err)
copyButtonText.value = 'Failed to copy'
}
}
</script>

<template>
<div class="flex w-full flex-col items-center justify-center space-y-4 px-4">
<div :style="{ color: options.textColor, backgroundColor: options.backgroundColor, width: `100%` }" class="rounded-md p-4">
<div class="flex" :style="{ gap: `${options.gap}px`, alignItems: options.align }">
<img :src="data.image" alt="Profile Picture" :style="{ width: `${options.imageSize}px`, height: `${options.imageSize}px` }" class="rounded-full object-cover">
<div :style="{ fontSize: `${options.fontSize}px` }">
<p class="font-bold">
{{ data.fullName }}
</p>
<p>{{ data.jobTitle }}</p>
<p>{{ data.company }}</p>
<p>{{ data.email }}</p>
<p>{{ data.phone }}</p>
</div>
</div>
</div>
<UButton label="Copy Signature" color="primary" @click="copySignature" />
</div>
</template>
18 changes: 18 additions & 0 deletions app/components/layout/Navbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
</script>

<template>
<div class="flex items-center justify-between bg-gray-900 px-4 py-2 shadow-lg">
<h1 class="font-instrument text-2xl font-bold text-white">
Inkly
</h1>
<UIcon name="lucide:github" class="text-white" />
</div>
</template>

<style scoped>
@font-face {
font-family: 'Instrument';
}
</style>
Binary file modified bun.lockb
Binary file not shown.
16 changes: 14 additions & 2 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
modules: ['@mockline/utils'],

future: {
compatibilityVersion: 4
}
},

icon: {
mode: 'svg',
customCollections: [
{
prefix: 'custom',
dir: './assets/icons',
},
],
},

modules: ['@nuxt/ui']
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
},
"devDependencies": {
"@hrcd/eslint-config": "^2.0.2",
"@nuxt/ui": "^2.18.4",
"automd": "^0.3.8",
"changelogen": "^0.5.5",
"eslint": "^9.9.1",
"nuxt": "^3.13.0",
"typescript": "^5.5.4",
"vitest": "^2.0.5",
"nuxt": "^3.13.0",
"vue": "latest"
},
"publishConfig": {
Expand Down
23 changes: 0 additions & 23 deletions scripts/rename.sh

This file was deleted.

Loading

0 comments on commit 13d94fc

Please sign in to comment.