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/vue

2. 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.js

3. 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 pinia

Calligraphy 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>
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