Every business always has a change in staff on an annual basis. Contacting the graphic designer for new designs and new cards is always a hustle. In this article, we review how we can develop a digital business card generator to remove the hustle from this process using Cloudinary and Nuxt.
To view the final product visit the Codesandbox below :
The projects Github repository can be found Here
📦 nuxtjs-business-card-generator ┣ 📦 components ┣ ┣ 📜 CardPreview.vue ┣ ┣ 📜 DetailsForm.vue ┣ ┣ 📜. Header.vue ┣ ┣ 📜 NuxtLogo.vue ┣ ┣ 📜 Steps.vue ┣ ┣ 📦 layouts ┣ ┗ 📜 default.vue ┣ ┣ 📦 pages ┣ ┗ 📜 customize.vue ┣ ┗ 📜 details.vue ┣ ┗ 📜 download.vue ┣ ┗ 📜 index.vue ┣ 📦 static ┣ ┗ 📜 favicon.ico ┣ 📦 store ┣ ┗ 📜 index.js ┣ ┗ 📜 README.md ┣ ┣ 📜 .editorconfig ┣ 📜 .env.example ┣ 📜 .gitignore ┣ 📜 README.md ┣ 📜 nuxt.config.js ┣ 📜 package.json ┗ 📜 yarn.lock
This tutorial assumes you’re already familiar with Vue and Javascript.
NodeJS is the platform needed for VueJS development. Check out the official Node.js website. You could also look into NVM, a node version manager.This will enable you to switch between different nodejs versions.
To begin, make sure you have npx installed on your system. This can be accomplished by:
yarn global add npx
# OR
npm install -g npx
Code language: PHP (php)
Then open up your terminal/command line and navigate to your desired Project folder. Run the following command to create your project.
yarn create nuxt-app nuxtjs-business-card-generator
This command above will ask you a series of questions. We recommend using the following defaults for this project:
-
Project Name: nuxtjs-business-card-generator
-
Programming Language -> JavaScript
-
Package manager -> Yarn
-
UI Framework -> TailwindCSS
-
Nuxt.Js modules -> None
-
Linting tools -> None
-
Rendering mode -> Universal (SSR/SSG)
-
Deployment target -> Static (Static/JAMStack hosting)
-
Development tools -> None
-
Countinous integration -> None
-
Version control -> Git
After installation, navigate to the project folder and launch it with the following command:
yarn dev
We shall use Cloudinary for our image transformation. Run the following command to install the NuxtJs package:
npm install @nuxtjs/cloudinary
# OR
yarn add @nuxtjs/cloudinary
Code language: CSS (css)
Add @nuxtjs/cloudinary
as a module in your nuxt.config.js
file.
export default {
modules: [
'@nuxtjs/cloudinary'
]
}
Code language: CSS (css)
In your nuxt.config.js, create a cloudinary section for your cloudinary configurations. Do this by adding a cloudinary object within the default export object.
export default {
...
cloudinary: {
cloudName: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,
useComponent: true
}
}
Code language: JavaScript (javascript)
Our business card generator has some template files. Here they are with the corresponding public ids:
- business-card-generator/logo-icon – image
- business-card-generator/globe-icon – image
- business-card-generator/mail-icon – image
- business-card-generator/smartphone-icon – image
- business-card-generator/pin-icon – image
- business-card-generator/business-card-template – image
- business-card-generator/logo-uploads/image_ftlr6k – image
Grab these images and place them in the appropriate folder based on the public-id.
This package will allow us to display a user-friendly color picker to our customers. You may install it using the following command:
yarn add vue-swatches
# or use npm
npm install --save vue-swatches
Code language: PHP (php)
To use the package with NuxtJs, we’ll need to add it to our config file:
{
modules: ['vue-swatches/nuxt']
}
Code language: CSS (css)
The data that we’ll be displayed on the business card is going to be changed over a number of pages. This means that we will need to be able to access and edit that same data across the whole app. To do this, we will use vuex, a Vue state management solution. It allows us to have a single source of truth.
To initialize our store, simply create an index.js
file in our store
folder.
The state is the single object that contains all your application-level data and serves as the single state of truth. The data we store here must follow the same rules as the data in a Vue instance, ie must be plain.
We are going to initialize our state by adding the following code:
export const state = () => ({
logo: 'business-card-generator/logo-uploads/image_ftlr6k',
details: {
full_name: 'Jack Dorsey',
position: 'Founder',
address: '1355 Market St. Ste. 900 San Francisco, CA 94103',
phone_number: '1-415-222-9670',
email: 'jack@twitter.com',
website: 'twitter.com'
},
customization: {
accent: '1DA1F2',
font: 'roboto'
}
});
Code language: JavaScript (javascript)
Getters allow us to compute derived state based on store state. Here are the getters we are going to set up for this project:
export const getters = {
logo: state => state.logo,
details: state => state.details,
customization: state => state.customization,
address: state => state.details.address.replace(
/[^ A-Za-z0-9_@.#&+-]/gi,
''
),
qrCodeLink: state => `https://qrtag.net/api/qr_4.png?url=https://${state.details.website}`,
};
Code language: JavaScript (javascript)
The above getters return data from the state. We ensure that our address is safe for sending to Cloudinary by removing all data, not in the following regex group /[^ A-Za-z0-9_@.#&+-]/gi
To display our QR code, we are using qrtag.net. Their API allows us to generate QR codes for free.
Mutations are the only way to change data in our Vuex store. We will use the following mutations in our project:
export const mutations = {
changeLogo(state, public_id) {
state.logo = public_id;
},
updateDetails(state, details) {
state.details = details;
return state.details;
},
updateCustomization(state, customization) {
state.customization.accent = customization.accent ? customization.accent.replace('#', '') : state.customization.accent;
return state.customization;
}
}
Code language: JavaScript (javascript)
The reason why we remove the #
prefix from the color code is because it shouldn’t have the #
when being passed in the text overlay transformation.
We commit mutations in actions instead of mutating the state directly. Actions can also contain additional logic like asynchronous operations but we won’t need them in our context.
export const actions = {
async changeLogo({ commit }, instance) {
commit('changeLogo', instance.public_id);
return instance.public_id;
},
updateDetails({ commit }, details) {
return commit('updateDetails', details);
},
updateCustomization({ commit }, customization) {
return commit('updateCustomization', customization);
}
}
Code language: JavaScript (javascript)
We have set up our Vuex store which contains our source of truth. We will now use this data in a set of transformations that will generate our business cards.
We need to create a component that will contain our code. We will call this component CardPreview.vue
. Let’s create this component in the components
folder.
As we set up earlier, we’ll need to pre-configure some data in our component. This data is the URLs to the icons we’ll be using and the default logo. Here is the code we’ll use to achieve this:
data() {
return {
icons: {
globe:
'https://res.cloudinary.com/hackit-africa/image/upload/v1625643063/Business-Card-Generator/globe-icon.png',
logo: 'https://res.cloudinary.com/hackit-africa/image/upload/v1625643154/Business-Card-Generator/logo-icon.png',
mail: 'https://res.cloudinary.com/hackit-africa/image/upload/v1625642967/Business-Card-Generator/mail-icon.png',
pin: 'https://res.cloudinary.com/hackit-africa/image/upload/v1625642735/Business-Card-Generator/pin-icon.png',
smartphone: 'https://res.cloudinary.com/hackit-africa/image/upload/v1625642848/Business-Card-Generator/smartphone-icon.png',
},
}
}
Code language: JavaScript (javascript)
To access data from our Vuex store, we’ll use the mapGetters
helper. It simply maps store getters to local computed properties in the component.
computed: {
...mapGetters({
logo: 'logo',
details: 'details',
customization: 'customization',
address: 'address',
qrCodeLink: 'qrCodeLink',
}),
},
Code language: JavaScript (javascript)
Our component will need to update anytime we change data in our Vuex store. To achieve this, we’ll use watchers. This is the generic way to react to data changes in our component.
watch: {
$route: {
deep: true,
handler: function (newVal, oldVal) {
this.$forceUpdate()
},
},
logo(newVal, oldVal) {
this.$forceUpdate()
},
details: {
deep: true,
handler: function (newVal, oldVal) {
this.$forceUpdate()
},
},
customization: {
deep: true,
handler: function (newVal, oldVal) {
this.$forceUpdate()
},
},
},
Code language: PHP (php)
The above code will force the component to rerender each time the $route
, logo
, details
, and customization
data changes. When we are watching an object, we’ll want the handler to be triggered when nested data changes. This is why we specify the `deep: true option.
We’ll use the blank template as the background for our transformations.
<!-- components/CardPreview.vue -->
<cld-image
public-id="business-card-generator/assets/business-card-template"
crop="fill"
alt="Front side of business card"
>
...
</cld-image>
Code language: HTML, XML (xml)
To display text, we’ll use the text overlay transformation.
<!-- components/CardPreview.vue -->
<cld-transformation
:overlay="`text:${customization.font}_64_bold:${details.full_name},co_rgb:000000`"
gravity="north_west"
x="80"
y="80"
/>
Code language: HTML, XML (xml)
To display images, we’ll use fetch overlay transformation.
<!-- components/CardPreview.vue -->
<cld-transformation
:overlay="`fetch:${icons.pin}`"
gravity="north_west"
x="80"
y="280"
width="40"
/>
Code language: HTML, XML (xml)
Here is the compilation of all the transformations together:
<!-- components/CardPreview.vue -->
<cld-image
public-id="business-card-generator/assets/business-card-template"
crop="fill"
alt="Front side of business card"
>
<!-- Name -->
<cld-transformation
:overlay="`text:${customization.font}_64_bold:${details.full_name},co_rgb:000000`"
gravity="north_west"
x="80"
y="80"
/>
<!-- Position -->
<cld-transformation
:overlay="`text:${customization.font}_42:${details.position},co_rgb:${customization.accent}`"
gravity="north_west"
x="80"
y="160"
/>
<!-- Location -->
<cld-transformation
:overlay="`fetch:${icons.pin}`"
gravity="north_west"
x="80"
y="280"
width="40"
/>
<cld-transformation
:overlay="`text:${customization.font}_24:${address},co_rgb:${customization.accent}`"
gravity="north_west"
x="150"
y="290"
/>
<!-- Phone -->
<cld-transformation
:overlay="`fetch:${icons.smartphone}`"
gravity="north_west"
x="80"
y="350"
width="40"
/>
<cld-transformation
:overlay="`text:${customization.font}_24:${details.phone_number},co_rgb:${customization.accent}`"
gravity="north_west"
x="150"
y="360"
/>
<!-- Email -->
<cld-transformation
:overlay="`fetch:${icons.mail}`"
gravity="north_west"
x="80"
y="420"
width="40"
/>
<cld-transformation
:overlay="`text:${customization.font}_24:${details.email},co_rgb:${customization.accent}`"
gravity="north_west"
x="150"
y="430"
/>
<!-- Web -->
<cld-transformation
:overlay="`fetch:${icons.globe}`"
gravity="north_west"
x="80"
y="500"
width="40"
/>
<cld-transformation
:overlay="`text:${customization.font}_24:${encodeURIComponent(
details.website
)},co_rgb:${customization.accent}`"
gravity="north_west"
x="150"
y="510"
/>
<!-- QR Code -->
<cld-transformation
:overlay="`fetch:${qrCodeLink}`"
gravity="south_east"
x="80"
y="80"
width="200"
/>
</cld-image>
Code language: HTML, XML (xml)
For the backside of the card, we will only render the logo at the center:
<!-- components/CardPreview.vue -->
<cld-image
public-id="business-card-generator/assets/business-card-template"
crop="fill"
alt="Backside of business card"
class="mt-10"
>
<!-- Logo -->
<cld-transformation
:overlay="`fetch:${$cloudinary.image.url(logo)}`"
width="300"
/>
</cld-image>
Code language: HTML, XML (xml)
To update the logo, we’ll upload the new logo to Cloudinary then update the logo public_id.
<!-- pages/index.vue -->
<input
v-if="!uploading"
type="file"
accept=".jpeg,.jpg,.png,image/jpeg,image/png"
aria-label="upload image button"
@change="selectFile"
/>
<div v-else>Please wait, upload in progress</div>
Code language: HTML, XML (xml)
We will then navigate to the details
page immediately after our upload is complete and our Vuex action has been dispatched.
// pages/index.vue
methods: {
async selectFile(e) {
this.uploading = true
const file = e.target.files[0]
/* Make sure file exists */
if (!file) return
/* create a reader */
const readData = (f) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.readAsDataURL(f)
})
/* Read data */
const data = await readData(file)
/* upload the converted data */
const instance = await this.$cloudinary.upload(data, {
folder: 'business-card-generator/logo-uploads',
uploadPreset: 'business-card-logo-uploads',
})
this.$store
.dispatch('changeLogo', instance)
.then(() => this.$router.push('/details'))
.finally(() => (this.uploading = false))
},
},
Code language: JavaScript (javascript)
On the details
page, we will render the details form with all the card details. To update the Vuex store, we’ll dispatch the updateDetails
action when submitting the form.
// components/DetailsForm.vue
submit() {
this.$store
.dispatch('updateDetails', this.details)
.then(() => this.$router.push('/customize'))
},
Code language: JavaScript (javascript)
To allow the user to change their accent color, we’ll display a simple color swatch to them.
<!-- pages/customize.vue -->
<v-swatches v-model="customize.accent" inline></v-swatches>
Code language: HTML, XML (xml)
We will then trigger the action on form submission
// pages/customize.vue
methods: {
submit() {
this.$store
.dispatch('updateCustomization', this.customize)
.then(() => this.$router.push('/download'))
},
},
Code language: JavaScript (javascript)
To allow the user to download their business cards, we need to get the image links after transformation. We will traverse the DOM to locate images under the #card-holder
element and grab the image src
links. If the images are not found, we will wait till they full load.
// pages/download.vue
getImageLinks() {
const holder = document.getElementById('card-holder')
if (holder === null) {
setTimeout(() => {
this.getImageLinks()
}, 1000)
return
}
const images = holder.getElementsByTagName('img')
if (images === null) {
setTimeout(() => {
this.getImageLinks()
}, 1000)
return
}
for (let image of images) {
if (image.src) {
this.downloadLinks.push({
alt: image.alt,
src: image.src,
})
}
}
if (this.downloadLinks.length < 2) {
setTimeout(() => {
this.getImageLinks()
}, 1000)
return
}
},
Code language: JavaScript (javascript)
Lastly, we will then render the download links.
<!-- components/download.vue -->
<ul class="list-disc text-center">
<li v-for="(link, index) in downloadLinks" :key="index">
<a :href="link.src" download target="_blank"> {{ link.alt }} </a>
</li>
</ul>
Code language: HTML, XML (xml)
From the above, we can see how combining Nuxt and Cloudinary can enable one to create powerful and robust applications. Feel free to fork the application and add other awesome features.
Here are some more resources that might interest you:
- [Nuxt.js Documentation] (https://nuxtjs.org/docs/2.x/get-started/installation)
- Image Optimization in Nuxt
- Cloudinary Nuxt.js Intergration