V
Vue.js Integration
Build beautiful calligraphy apps with Vue.js
Progressive FrameworkComposition APITypeScript Support
Quick Start
1. Install Dependencies
bash
Click to copy
# Create new Vue project
npm create vue@latest calligraphy-app
# Navigate to project
cd calligraphy-app
# Install dependencies
npm install
# Install HTTP client
npm install axios
# Install additional UI library (optional)
npm install @headlessui/vue @heroicons/vue2. Project Structure
text
Click to copy
src/
├── components/
│ ├── CalligraphyGenerator.vue
│ ├── CalligraphyDisplay.vue
│ └── CalligraphyResults.vue
├── composables/
│ ├── useCalligraphyAPI.js
│ └── useImageDownload.js
├── services/
│ └── calligraphyService.js
├── types/
│ └── calligraphy.ts
└── stores/
└── calligraphy.js3. Basic Component
vue
Click to copy
<!-- components/CalligraphyGenerator.vue -->
<template>
<div class="calligraphy-generator">
<div class="form-container">
<h2 class="text-2xl font-bold mb-6">Calligraphy Generator</h2>
<form @submit.prevent="generateCalligraphy" class="space-y-4">
<div>
<label for="text" class="block text-sm font-medium text-gray-700">
Enter Text
</label>
<textarea
id="text"
v-model="formData.text"
rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="Enter text to convert to calligraphy..."
required
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="language" class="block text-sm font-medium text-gray-700">
Language
</label>
<select
id="language"
v-model="formData.language"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
>
<option value="hindi">Hindi</option>
<option value="english">English</option>
<option value="marathi">Marathi</option>
<option value="gujarati">Gujarati</option>
</select>
</div>
<div>
<label for="fontStyle" class="block text-sm font-medium text-gray-700">
Font Style
</label>
<select
id="fontStyle"
v-model="formData.fontStyle"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
>
<option value="calligraphy">Calligraphy</option>
<option value="modern">Modern</option>
<option value="traditional">Traditional</option>
</select>
</div>
</div>
<button
type="submit"
:disabled="loading"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ loading ? 'Generating...' : 'Generate Calligraphy' }}
</button>
</form>
</div>
<!-- Error Display -->
<div v-if="error" class="mt-6 p-4 bg-red-50 border border-red-200 rounded-md">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error</h3>
<p class="mt-1 text-sm text-red-700">{{ error }}</p>
</div>
</div>
</div>
<!-- Results -->
<CalligraphyResults
v-if="results.length > 0"
:results="results"
@download="downloadImage"
@download-svg="downloadSVG"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useCalligraphyAPI } from '@/composables/useCalligraphyAPI'
import { useImageDownload } from '@/composables/useImageDownload'
import CalligraphyResults from './CalligraphyResults.vue'
const { generateCalligraphy, downloadSVG: downloadSVGData, loading, error } = useCalligraphyAPI()
const { downloadImage, downloadSVG } = useImageDownload()
const formData = reactive({
text: 'नमस्ते दुनिया',
language: 'hindi',
fontStyle: 'calligraphy'
})
const results = ref([])
const generateCalligraphy = async () => {
try {
const response = await generateCalligraphy({
text: formData.text,
language: formData.language,
fontStyle: formData.fontStyle,
count: 3
})
if (response && response.length > 0) {
results.value = response
}
} catch (err) {
console.error('Error generating calligraphy:', err)
}
}
</script>
<style scoped>
.calligraphy-generator {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.form-container {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
</style>Composables & Services
API Composable
javascript
Click to copy
// composables/useCalligraphyAPI.js
import { ref } from 'vue'
import { calligraphyService } from '@/services/calligraphyService'
export function useCalligraphyAPI() {
const loading = ref(false)
const error = ref(null)
const generateCalligraphy = async (options) => {
loading.value = true
error.value = null
try {
const response = await calligraphyService.generate(options)
return response.data.results
} catch (err) {
error.value = handleError(err)
throw err
} finally {
loading.value = false
}
}
const downloadSVG = async (resultText, fontName) => {
loading.value = true
error.value = null
try {
const response = await calligraphyService.downloadSVG(resultText, fontName)
return response.data
} catch (err) {
error.value = handleError(err)
throw err
} finally {
loading.value = false
}
}
const handleError = (err) => {
if (err.response) {
const { status, data } = err.response
switch (status) {
case 401:
return 'Invalid API key. Please check your credentials.'
case 402:
return 'Insufficient credits. Please upgrade your plan.'
case 429:
return 'Rate limit exceeded. Please try again later.'
case 400:
return data.error || 'Invalid request parameters.'
default:
return data.error || 'Server error occurred.'
}
}
if (err.code === 'NETWORK_ERROR') {
return 'Network error. Please check your internet connection.'
}
return err.message || 'An unexpected error occurred.'
}
return {
generateCalligraphy,
downloadSVG,
loading,
error
}
}Service Layer
javascript
Click to copy
// services/calligraphyService.js
import axios from 'axios'
class CalligraphyService {
constructor() {
this.baseURL = 'https://api.calligraphymaker.com/api/v2'
this.apiKey = import.meta.env.VITE_CALLIGRAPHY_API_KEY || ''
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey
},
timeout: 30000
})
// Request interceptor
this.client.interceptors.request.use(
(config) => {
console.log('API Request:', config.method?.toUpperCase(), config.url)
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor
this.client.interceptors.response.use(
(response) => {
console.log('API Response:', response.status, response.config.url)
return response
},
(error) => {
console.error('API Error:', error.response?.status, error.config?.url)
return Promise.reject(error)
}
)
}
async generate(options) {
const requestData = {
text: options.text,
language: options.language || 'hindi',
fontStyle: options.fontStyle || 'calligraphy',
count: options.count || 1,
imageFormat: options.imageFormat || 'png'
}
const response = await this.client.post('/generate', requestData)
if (!response.data.success) {
throw new Error(response.data.error || 'Generation failed')
}
return response.data
}
async downloadSVG(resultText, fontName) {
const requestData = {
resultText,
fontName
}
const response = await this.client.post('/download-svg', requestData)
if (!response.data.success) {
throw new Error(response.data.error || 'SVG download failed')
}
return response.data
}
async batchGenerate(requests) {
const response = await this.client.post('/batch/generate', { requests })
if (!response.data.success) {
throw new Error(response.data.error || 'Batch generation failed')
}
return response.data
}
}
export const calligraphyService = new CalligraphyService()Download Composable
javascript
Click to copy
// composables/useImageDownload.js
import { ref } from 'vue'
export function useImageDownload() {
const downloading = ref(false)
const downloadImage = async (imageUrl, filename = 'calligraphy.png') => {
downloading.value = true
try {
const response = await fetch(imageUrl)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
return true
} catch (error) {
console.error('Download failed:', error)
throw new Error('Failed to download image')
} finally {
downloading.value = false
}
}
const downloadSVG = async (svgContent, filename = 'calligraphy.svg') => {
try {
const blob = new Blob([svgContent], { type: 'image/svg+xml' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
return true
} catch (error) {
console.error('SVG download failed:', error)
throw new Error('Failed to download SVG')
}
}
const shareImage = async (imageUrl, text = 'Beautiful Calligraphy') => {
if (navigator.share) {
try {
const response = await fetch(imageUrl)
const blob = await response.blob()
const file = new File([blob], 'calligraphy.png', { type: blob.type })
await navigator.share({
title: 'Beautiful Calligraphy',
text: text,
files: [file]
})
return true
} catch (error) {
console.error('Sharing failed:', error)
// Fallback to download
return await downloadImage(imageUrl)
}
} else {
// Fallback to download
return await downloadImage(imageUrl)
}
}
return {
downloadImage,
downloadSVG,
shareImage,
downloading
}
}Results Component
CalligraphyResults.vue
vue
Click to copy
<!-- components/CalligraphyResults.vue -->
<template>
<div class="results-container">
<h3 class="text-xl font-semibold mb-6">Generated Calligraphy</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(result, index) in results"
:key="index"
class="result-card"
>
<div class="image-container">
<img
:src="result.cdnUrl"
:alt="`Calligraphy result ${index + 1}`"
class="calligraphy-image"
@load="onImageLoad"
@error="onImageError"
/>
<div class="image-overlay">
<button
@click="$emit('download', result.cdnUrl, `calligraphy-${index + 1}.png`)"
class="action-button"
title="Download PNG"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
<button
@click="downloadSVG(result)"
class="action-button"
title="Download SVG"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</button>
</div>
</div>
<div class="result-info">
<p class="font-medium text-sm text-gray-900">{{ result.fontName }}</p>
<p class="text-xs text-gray-500 truncate">{{ result.resultText }}</p>
</div>
</div>
</div>
<!-- Batch Actions -->
<div class="mt-8 flex flex-wrap gap-3">
<button
@click="downloadAll"
class="btn btn-primary"
:disabled="downloading"
>
<svg v-if="downloading" class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ downloading ? 'Downloading...' : 'Download All' }}
</button>
<button
@click="shareFirst"
class="btn btn-secondary"
v-if="results.length > 0"
>
Share First Result
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useCalligraphyAPI } from '@/composables/useCalligraphyAPI'
import { useImageDownload } from '@/composables/useImageDownload'
const props = defineProps({
results: {
type: Array,
required: true
}
})
const emit = defineEmits(['download', 'download-svg'])
const { downloadSVG: downloadSVGData } = useCalligraphyAPI()
const { downloadImage, downloadSVG: saveSVG, shareImage } = useImageDownload()
const downloading = ref(false)
const onImageLoad = (event) => {
console.log('Image loaded:', event.target.src)
}
const onImageError = (event) => {
console.error('Image failed to load:', event.target.src)
event.target.src = '/placeholder-calligraphy.png' // Fallback image
}
const downloadSVG = async (result) => {
try {
const svgData = await downloadSVGData(result.resultText, result.fontName)
if (svgData && svgData.svgContent) {
await saveSVG(svgData.svgContent, `${result.fontName}-${Date.now()}.svg`)
}
} catch (error) {
console.error('SVG download failed:', error)
}
}
const downloadAll = async () => {
downloading.value = true
try {
const promises = props.results.map((result, index) =>
downloadImage(result.cdnUrl, `calligraphy-${index + 1}.png`)
)
await Promise.all(promises)
} catch (error) {
console.error('Batch download failed:', error)
} finally {
downloading.value = false
}
}
const shareFirst = async () => {
if (props.results.length > 0) {
try {
await shareImage(props.results[0].cdnUrl, props.results[0].resultText)
} catch (error) {
console.error('Sharing failed:', error)
}
}
}
</script>
<style scoped>
.results-container {
margin-top: 32px;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.result-card {
background: #f9fafb;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
transition: transform 0.2s, box-shadow 0.2s;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15);
}
.image-container {
position: relative;
overflow: hidden;
}
.calligraphy-image {
width: 100%;
height: 200px;
object-fit: contain;
background: white;
}
.image-overlay {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
}
.result-card:hover .image-overlay {
opacity: 1;
}
.action-button {
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 4px;
padding: 8px;
cursor: pointer;
transition: background-color 0.2s;
}
.action-button:hover {
background: rgba(0, 0, 0, 0.9);
}
.result-info {
padding: 16px;
}
.btn {
display: inline-flex;
align-items: center;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>State Management with Pinia
Install Pinia
bash
Click to copy
npm install piniaCalligraphy Store
javascript
Click to copy
// stores/calligraphy.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { calligraphyService } from '@/services/calligraphyService'
export const useCalligraphyStore = defineStore('calligraphy', () => {
// State
const results = ref([])
const history = ref([])
const loading = ref(false)
const error = ref(null)
const currentRequest = ref(null)
// Getters
const hasResults = computed(() => results.value.length > 0)
const latestResult = computed(() => results.value[0] || null)
const totalGenerations = computed(() => history.value.length)
// Actions
const generateCalligraphy = async (options) => {
loading.value = true
error.value = null
currentRequest.value = options
try {
const response = await calligraphyService.generate(options)
if (response.data.results && response.data.results.length > 0) {
results.value = response.data.results
// Add to history
history.value.unshift({
id: Date.now(),
timestamp: new Date().toISOString(),
request: { ...options },
results: response.data.results,
resultCount: response.data.results.length
})
// Keep only last 50 generations in history
if (history.value.length > 50) {
history.value = history.value.slice(0, 50)
}
return response.data.results
} else {
throw new Error('No results generated')
}
} catch (err) {
error.value = err.message || 'Generation failed'
throw err
} finally {
loading.value = false
}
}
const downloadSVG = async (resultText, fontName) => {
try {
const response = await calligraphyService.downloadSVG(resultText, fontName)
return response.data
} catch (err) {
error.value = err.message || 'SVG download failed'
throw err
}
}
const clearResults = () => {
results.value = []
error.value = null
}
const clearHistory = () => {
history.value = []
}
const removeFromHistory = (id) => {
const index = history.value.findIndex(item => item.id === id)
if (index > -1) {
history.value.splice(index, 1)
}
}
const regenerateFromHistory = async (historyItem) => {
return await generateCalligraphy(historyItem.request)
}
const getStats = () => {
const languageStats = {}
const fontStats = {}
history.value.forEach(item => {
const lang = item.request.language
const font = item.request.fontStyle
languageStats[lang] = (languageStats[lang] || 0) + 1
fontStats[font] = (fontStats[font] || 0) + 1
})
return {
totalGenerations: totalGenerations.value,
languageStats,
fontStats,
mostUsedLanguage: Object.keys(languageStats).reduce((a, b) =>
languageStats[a] > languageStats[b] ? a : b, 'N/A'
),
mostUsedFont: Object.keys(fontStats).reduce((a, b) =>
fontStats[a] > fontStats[b] ? a : b, 'N/A'
)
}
}
return {
// State
results,
history,
loading,
error,
currentRequest,
// Getters
hasResults,
latestResult,
totalGenerations,
// Actions
generateCalligraphy,
downloadSVG,
clearResults,
clearHistory,
removeFromHistory,
regenerateFromHistory,
getStats
}
})
// Persist store to localStorage
import { createPersistedState } from 'pinia-plugin-persistedstate'
export const persistedCalligraphyStore = useCalligraphyStore.$persist({
key: 'calligraphy-store',
storage: localStorage,
paths: ['history'] // Only persist history, not current results or loading state
})Using Store in Components
vue
Click to copy
<!-- components/CalligraphyWithStore.vue -->
<template>
<div class="calligraphy-app">
<!-- Generator Form -->
<CalligraphyForm @generate="handleGenerate" />
<!-- Results -->
<CalligraphyResults
v-if="store.hasResults"
:results="store.results"
:loading="store.loading"
/>
<!-- History -->
<CalligraphyHistory
:history="store.history"
@regenerate="handleRegenerate"
@remove="store.removeFromHistory"
/>
<!-- Stats -->
<CalligraphyStats :stats="stats" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useCalligraphyStore } from '@/stores/calligraphy'
import CalligraphyForm from './CalligraphyForm.vue'
import CalligraphyResults from './CalligraphyResults.vue'
import CalligraphyHistory from './CalligraphyHistory.vue'
import CalligraphyStats from './CalligraphyStats.vue'
const store = useCalligraphyStore()
const stats = computed(() => store.getStats())
const handleGenerate = async (options) => {
try {
await store.generateCalligraphy(options)
} catch (error) {
console.error('Generation failed:', error)
}
}
const handleRegenerate = async (historyItem) => {
try {
await store.regenerateFromHistory(historyItem)
} catch (error) {
console.error('Regeneration failed:', error)
}
}
</script>Quick Links
What You'll Build
Reactive UI
Vue.js 3 with Composition API
State Management
Pinia store with persistence
Composable Logic
Reusable business logic
File Downloads
PNG and SVG download support
Requirements
Vue.js 3.0+
Node.js 16+
Vite build tool
API Key from Calligraphy API