feat: init
This commit is contained in:
125
app/components/VatRateTable.vue
Normal file
125
app/components/VatRateTable.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
interface RateRow {
|
||||
country: string
|
||||
code: string
|
||||
flag: string
|
||||
standard: number
|
||||
reduced: number[]
|
||||
}
|
||||
|
||||
function codeToFlag(code: string): string {
|
||||
return [...code.toUpperCase()].map(c => String.fromCodePoint(0x1F1E6 + c.charCodeAt(0) - 65)).join('')
|
||||
}
|
||||
|
||||
const rates = ref<RateRow[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await $fetch<any[]>('/api/v1/rates')
|
||||
rates.value = data.map(r => ({
|
||||
country: r.country,
|
||||
code: r.country_code,
|
||||
flag: codeToFlag(r.country_code),
|
||||
standard: r.standard_rate,
|
||||
reduced: r.reduced_rates,
|
||||
}))
|
||||
} catch {
|
||||
// Fallback to composable data if API fails
|
||||
const { rates: fallback } = useVatRates()
|
||||
rates.value = fallback.value
|
||||
}
|
||||
})
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = search.value.toLowerCase().trim()
|
||||
if (!q) return rates.value
|
||||
return rates.value.filter(
|
||||
r => r.country.toLowerCase().includes(q) || r.code.toLowerCase().includes(q),
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="rates" class="section-padding">
|
||||
<div class="section-container">
|
||||
<div class="text-center mb-10 animate-on-scroll">
|
||||
<h2 class="text-title md:text-display-sm text-ink">
|
||||
VAT Rates across the EU
|
||||
</h2>
|
||||
<p class="mt-3 text-ink-muted max-w-lg mx-auto">
|
||||
Current standard and reduced rates for all 27 member states.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="max-w-sm mx-auto mb-8 animate-on-scroll">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-ink-faint" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
placeholder="Search countries..."
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl border border-surface-border bg-white text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-eu-blue/20 focus:border-eu-blue/40 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="animate-on-scroll overflow-hidden rounded-2xl border border-surface-border bg-white shadow-soft">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-surface-soft border-b border-surface-border">
|
||||
<th class="text-left font-semibold text-ink-secondary px-5 py-3.5 font-heading">Country</th>
|
||||
<th class="text-left font-semibold text-ink-secondary px-5 py-3.5 font-heading w-20">Code</th>
|
||||
<th class="text-right font-semibold text-ink-secondary px-5 py-3.5 font-heading w-32">Standard</th>
|
||||
<th class="text-right font-semibold text-ink-secondary px-5 py-3.5 font-heading w-40">Reduced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="rate in filtered"
|
||||
:key="rate.code"
|
||||
class="border-b border-surface-border/60 last:border-0 hover:bg-eu-blue-100/40 transition-colors"
|
||||
>
|
||||
<td class="px-5 py-3">
|
||||
<span class="inline-flex items-center gap-2.5">
|
||||
<span class="text-lg leading-none">{{ rate.flag }}</span>
|
||||
<span class="font-medium text-ink">{{ rate.country }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-3">
|
||||
<span class="inline-flex px-2 py-0.5 rounded bg-surface-muted font-mono text-xs font-medium text-ink-secondary">
|
||||
{{ rate.code }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-3 text-right">
|
||||
<span class="font-semibold text-ink tabular-nums">{{ rate.standard }}%</span>
|
||||
</td>
|
||||
<td class="px-5 py-3 text-right">
|
||||
<span v-if="rate.reduced.length" class="text-ink-muted tabular-nums">
|
||||
{{ rate.reduced.map(r => `${r}%`).join(', ') }}
|
||||
</span>
|
||||
<span v-else class="text-ink-faint">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!filtered.length">
|
||||
<td colspan="4" class="px-5 py-10 text-center text-ink-muted">
|
||||
No countries matching "{{ search }}"
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-ink-faint mt-4">
|
||||
Data updated regularly from official EU sources.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user