feat: init

This commit is contained in:
2026-02-13 22:02:30 +01:00
commit 8f9ff830fb
16711 changed files with 3307340 additions and 0 deletions

View 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">&mdash;</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>