feat: init
This commit is contained in:
139
app/components/ApiPlayground.vue
Normal file
139
app/components/ApiPlayground.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
const { rates } = useVatRates()
|
||||
|
||||
const selectedCode = ref('DE')
|
||||
const response = ref('')
|
||||
const statusCode = ref<number | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const showAllRates = ref(false)
|
||||
|
||||
const endpointUrl = computed(() =>
|
||||
showAllRates.value
|
||||
? 'https://vat-api.eu/api/v1/rates'
|
||||
: `https://vat-api.eu/api/v1/rates/${selectedCode.value}`,
|
||||
)
|
||||
|
||||
function syntaxHighlight(json: string): string {
|
||||
return json
|
||||
.replace(/("(?:\\.|[^"\\])*")\s*:/g, '<span style="color:#79c0ff">$1</span>:')
|
||||
.replace(/:\s*("(?:\\.|[^"\\])*")/g, ': <span style="color:#a5d6ff">$1</span>')
|
||||
.replace(/:\s*(\d+\.?\d*)/g, ': <span style="color:#79c0ff">$1</span>')
|
||||
.replace(/(\[|\])/g, '<span style="color:#ff7b72">$1</span>')
|
||||
.replace(/^(\{|})/gm, '<span style="color:#ff7b72">$1</span>')
|
||||
}
|
||||
|
||||
async function sendRequest() {
|
||||
isLoading.value = true
|
||||
response.value = ''
|
||||
statusCode.value = null
|
||||
|
||||
try {
|
||||
const url = showAllRates.value
|
||||
? '/api/v1/rates'
|
||||
: `/api/v1/rates/${selectedCode.value}`
|
||||
const data = await $fetch(url)
|
||||
statusCode.value = 200
|
||||
response.value = syntaxHighlight(JSON.stringify(data, null, 2))
|
||||
} catch (err: any) {
|
||||
statusCode.value = err?.statusCode ?? 500
|
||||
const errorBody = err?.data ?? { error: err?.statusMessage ?? 'Request failed' }
|
||||
response.value = syntaxHighlight(JSON.stringify(errorBody, null, 2))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-send on mount
|
||||
onMounted(() => sendRequest())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="playground" class="section-padding bg-surface-soft">
|
||||
<div class="section-container">
|
||||
<div class="text-center mb-10 animate-on-scroll">
|
||||
<h2 class="text-title md:text-display-sm text-ink">
|
||||
Try it out
|
||||
</h2>
|
||||
<p class="mt-3 text-ink-muted max-w-lg mx-auto">
|
||||
Build your request, send it, and see the response.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="animate-on-scroll max-w-3xl mx-auto">
|
||||
<div class="bg-white rounded-2xl border border-surface-border shadow-card overflow-hidden">
|
||||
<!-- Controls -->
|
||||
<div class="p-5 border-b border-surface-border">
|
||||
<div class="flex flex-wrap gap-3 items-end">
|
||||
<!-- Endpoint toggle -->
|
||||
<div class="flex-shrink-0">
|
||||
<label class="block text-xs font-medium text-ink-muted mb-1.5">Endpoint</label>
|
||||
<div class="flex rounded-lg border border-surface-border overflow-hidden text-sm">
|
||||
<button
|
||||
class="px-3 py-2 font-medium transition-colors"
|
||||
:class="!showAllRates ? 'bg-eu-blue text-white' : 'bg-white text-ink-secondary hover:bg-surface-soft'"
|
||||
@click="showAllRates = false"
|
||||
>
|
||||
Single
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-2 font-medium transition-colors"
|
||||
:class="showAllRates ? 'bg-eu-blue text-white' : 'bg-white text-ink-secondary hover:bg-surface-soft'"
|
||||
@click="showAllRates = true"
|
||||
>
|
||||
All Rates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Country picker -->
|
||||
<div v-if="!showAllRates" class="flex-shrink-0">
|
||||
<label class="block text-xs font-medium text-ink-muted mb-1.5">Country</label>
|
||||
<select
|
||||
v-model="selectedCode"
|
||||
class="px-3 py-2 rounded-lg border border-surface-border bg-white text-sm text-ink focus:outline-none focus:ring-2 focus:ring-eu-blue/20 focus:border-eu-blue/40 transition-all"
|
||||
>
|
||||
<option v-for="rate in rates" :key="rate.code" :value="rate.code">
|
||||
{{ rate.flag }} {{ rate.country }} ({{ rate.code }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Send button -->
|
||||
<button
|
||||
class="px-5 py-2 rounded-lg bg-eu-gold text-eu-blue-dark font-semibold text-sm hover:bg-eu-gold-dark transition-colors flex items-center gap-2"
|
||||
@click="sendRequest"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<svg v-if="isLoading" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<span>{{ isLoading ? 'Sending...' : 'Send Request' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- URL display -->
|
||||
<div class="mt-3.5 flex items-center gap-2 px-3.5 py-2.5 rounded-lg bg-surface-muted font-mono text-sm overflow-x-auto">
|
||||
<span class="flex-shrink-0 text-xs font-semibold px-1.5 py-0.5 rounded bg-green-100 text-green-700">GET</span>
|
||||
<span class="text-ink-secondary">{{ endpointUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response -->
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between px-5 py-3 bg-[#0d1117] border-b border-white/5">
|
||||
<span class="text-xs text-[#8b949e] font-mono">Response</span>
|
||||
<span v-if="statusCode === 200" class="text-xs font-mono px-2 py-0.5 rounded bg-green-500/20 text-green-400">200 OK</span>
|
||||
<span v-else-if="statusCode" class="text-xs font-mono px-2 py-0.5 rounded bg-red-500/20 text-red-400">{{ statusCode }} Error</span>
|
||||
</div>
|
||||
<div class="bg-[#0d1117] text-[#e6edf3] font-mono text-sm leading-relaxed p-5 max-h-80 overflow-y-auto">
|
||||
<pre v-if="response" class="text-[13px] leading-6" v-html="response" />
|
||||
<p v-else-if="isLoading" class="text-[#8b949e] text-sm">Loading...</p>
|
||||
<p v-else class="text-[#8b949e] text-sm">Click "Send Request" to see a response.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user