Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ jobs:

- name: Typecheck
run: pnpm run typecheck

- name: Test
run: pnpm test
50 changes: 41 additions & 9 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,50 @@ function removeCategory(categoryId: string) {
/>
</div>

<!-- Language Selector -->
<DevOnly>
<div right-16 top-16 fixed z-50>
<SelectRoot v-model="$i18n.locale">
<SelectTrigger
outline="~ 1.5 neutral-300"
flex="~ items-center gap-8" shadow-sm font-medium py-8 rounded-8 bg-white cursor-pointer text-f-sm f-px-md
>
<SelectValue placeholder="Language" />
<Icon name="i-tabler:chevron-down" />
</SelectTrigger>
<SelectContent
position="popper" outline="~ 1.5 neutral-200"
rounded-8 bg-white max-h-256 shadow z-50 of-auto
>
<SelectViewport f-p-xs>
<SelectItem
v-for="locale in $i18n.locales.value"
:key="locale.code"
:value="locale.code"
flex="~ items-center gap-8" text="f-sm neutral-800 data-[highlighted]:neutral-900"
bg="data-[highlighted]:neutral-50" py-10 outline-none rounded-6 cursor-pointer
transition-colors f-px-md
>
{{ locale.name }}
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectRoot>
</div>
</DevOnly>

<div mx-auto max-w-640 relative z-1 f-px-md>
<div f-mb-2xl>
<h1 text="neutral-900 f-2xl" font-bold f-mb-xs>
Spend your crypto in Lugano
{{ $t('hero.title') }}
</h1>
<p text="neutral-600 f-md" f-mb-lg>
Discover places that accept cryptocurrency payments
{{ $t('hero.subtitle') }}
</p>

<ComboboxRoot v-model="selectedCategories" multiple>
<ComboboxAnchor w-full>
<ComboboxInput v-model="searchQuery" placeholder="Search locations or add category filters..." nq-input-box :display-value="() => searchQuery" @focus="refreshCategories" />
<ComboboxInput v-model="searchQuery" :placeholder="$t('search.placeholder')" nq-input-box :display-value="() => searchQuery" @focus="refreshCategories" />
</ComboboxAnchor>

<ComboboxContent position="popper" bg="white" outline="~ 1.5 neutral-200" rounded-t-8 max-h-256 w-full shadow z-50 of-auto>
Expand All @@ -57,7 +89,7 @@ function removeCategory(categoryId: string) {
{{ category.name }}
</ComboboxItem>
<ComboboxEmpty v-if="!categories?.length" f-p-md text="f-sm neutral-500 center">
No categories found
{{ $t('search.noCategoriesFound') }}
</ComboboxEmpty>
</ComboboxViewport>
</ComboboxContent>
Expand Down Expand Up @@ -87,7 +119,7 @@ function removeCategory(categoryId: string) {
text="14 neutral-800 hocus:neutral"
font-medium py-4 rounded-full cursor-pointer transition-colors f-px-2xs
>
Open now
{{ $t('filters.openNow') }}
</ToggleGroupItem>
<ToggleGroupItem
value="walkable"
Expand All @@ -96,7 +128,7 @@ function removeCategory(categoryId: string) {
text="14 neutral-800 hocus:neutral"
font-medium py-4 rounded-full cursor-pointer transition-colors f-px-2xs
>
Walkable distance
{{ $t('filters.walkableDistance') }}
</ToggleGroupItem>
</ToggleGroupRoot>
</div>
Expand Down Expand Up @@ -161,7 +193,7 @@ function removeCategory(categoryId: string) {
:to="location.website"
target="_blank" external w-fit f-mt-sm nq-arrow nq-pill-blue
>
Visit Website
{{ $t('location.visitWebsite') }}
</NuxtLink>
</div>
</div>
Expand All @@ -175,10 +207,10 @@ function removeCategory(categoryId: string) {
shadow-md text-center rounded-12 f-py-2xl f-mt-xl
>
<p text="neutral-500" font-medium f-text-lg>
No locations found
{{ $t('empty.title') }}
</p>
<p text="neutral-400 f-sm" f-mt-xs>
Try adjusting your search or filters
{{ $t('empty.subtitle') }}
</p>
</div>
</div>
Expand Down
21 changes: 21 additions & 0 deletions i18n/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"hero": {
"title": "Gib deine Krypto in Lugano aus",
"subtitle": "Entdecke Orte, die Kryptowährungszahlungen akzeptieren"
},
"search": {
"placeholder": "Suche Standorte oder füge Kategoriefilter hinzu...",
"noCategoriesFound": "Keine Kategorien gefunden"
},
"filters": {
"openNow": "Jetzt geöffnet",
"walkableDistance": "Fußläufige Entfernung"
},
"location": {
"visitWebsite": "Website besuchen"
},
"empty": {
"title": "Keine Standorte gefunden",
"subtitle": "Versuche, deine Suche oder Filter anzupassen"
}
}
21 changes: 21 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"hero": {
"title": "Spend your crypto in Lugano",
"subtitle": "Discover places that accept cryptocurrency payments"
},
"search": {
"placeholder": "Search locations or add category filters...",
"noCategoriesFound": "No categories found"
},
"filters": {
"openNow": "Open now",
"walkableDistance": "Walkable distance"
},
"location": {
"visitWebsite": "Visit Website"
},
"empty": {
"title": "No locations found",
"subtitle": "Try adjusting your search or filters"
}
}
21 changes: 21 additions & 0 deletions i18n/locales/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"hero": {
"title": "Gasta tus criptomonedas en Lugano",
"subtitle": "Descubre lugares que aceptan pagos con criptomonedas"
},
"search": {
"placeholder": "Busca ubicaciones o agrega filtros de categoría...",
"noCategoriesFound": "No se encontraron categorías"
},
"filters": {
"openNow": "Abierto ahora",
"walkableDistance": "Distancia caminable"
},
"location": {
"visitWebsite": "Visitar sitio web"
},
"empty": {
"title": "No se encontraron ubicaciones",
"subtitle": "Intenta ajustar tu búsqueda o filtros"
}
}
21 changes: 21 additions & 0 deletions i18n/locales/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"hero": {
"title": "Dépensez vos cryptos à Lugano",
"subtitle": "Découvrez des endroits qui acceptent les paiements en cryptomonnaies"
},
"search": {
"placeholder": "Rechercher des emplacements ou ajouter des filtres de catégorie...",
"noCategoriesFound": "Aucune catégorie trouvée"
},
"filters": {
"openNow": "Ouvert maintenant",
"walkableDistance": "Distance à pied"
},
"location": {
"visitWebsite": "Visiter le site web"
},
"empty": {
"title": "Aucun emplacement trouvé",
"subtitle": "Essayez d'ajuster votre recherche ou vos filtres"
}
}
21 changes: 21 additions & 0 deletions i18n/locales/pt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"hero": {
"title": "Gaste suas criptomoedas em Lugano",
"subtitle": "Descubra lugares que aceitam pagamentos com criptomoedas"
},
"search": {
"placeholder": "Pesquisar locais ou adicionar filtros de categoria...",
"noCategoriesFound": "Nenhuma categoria encontrada"
},
"filters": {
"openNow": "Aberto agora",
"walkableDistance": "Distância a pé"
},
"location": {
"visitWebsite": "Visitar site"
},
"empty": {
"title": "Nenhum local encontrado",
"subtitle": "Tente ajustar sua pesquisa ou filtros"
}
}
14 changes: 14 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default defineNuxtConfig({
'@nuxt/icon',
'reka-ui/nuxt',
'@nuxt/image',
'@nuxtjs/i18n',
],
hub: {
database: true,
Expand Down Expand Up @@ -49,6 +50,19 @@ export default defineNuxtConfig({
icon: {
collections: ['tabler'],
customCollections: [nimiqIcons],
clientBundle: {
sizeLimitKb: 512, // 512KB
},
},
i18n: {
locales: [
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
{ code: 'es', language: 'es-ES', name: 'Español', file: 'es.json' },
{ code: 'de', language: 'de-DE', name: 'Deutsch', file: 'de.json' },
{ code: 'fr', language: 'fr-FR', name: 'Français', file: 'fr.json' },
{ code: 'pt', language: 'pt-PT', name: 'Português', file: 'pt.json' },
],
defaultLocale: 'en',
},
compatibilityDate: '2025-10-01',
})
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
"db:generate": "drizzle-kit generate",
"lint": "eslint . --cache",
"lint:fix": "eslint . --fix --cache",
"test": "vitest run",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@nuxt/fonts": "catalog:",
"@nuxt/icon": "catalog:",
"@nuxt/image": "catalog:",
"@nuxthub/core": "catalog:",
"@nuxtjs/i18n": "catalog:",
"@vueuse/nuxt": "catalog:",
"consola": "catalog:",
"drizzle-orm": "catalog:",
Expand All @@ -36,15 +38,20 @@
"devDependencies": {
"@antfu/eslint-config": "catalog:",
"@nuxt/eslint": "catalog:",
"@nuxt/test-utils": "catalog:",
"@unocss/eslint-plugin": "catalog:",
"@unocss/nuxt": "catalog:",
"@unocss/preset-attributify": "catalog:",
"@vue/test-utils": "catalog:",
"drizzle-kit": "catalog:",
"eslint": "catalog:",
"eslint-plugin-format": "catalog:",
"happy-dom": "catalog:",
"nimiq-css": "catalog:",
"playwright-core": "catalog:",
"typescript": "catalog:",
"unocss-preset-onmax": "catalog:",
"vitest": "catalog:",
"wrangler": "catalog:"
}
}
14 changes: 14 additions & 0 deletions plugins/i18n-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default defineNuxtPlugin(async () => {
const { setLocale, locales } = useI18n()
const route = useRoute()

// Get available locale codes
const availableLocales = (locales.value as { code: string }[]).map(l => l.code)

// Check for locale in query param
const queryLocale = route.query.locale as string | undefined

if (queryLocale && availableLocales.includes(queryLocale)) {
await setLocale(queryLocale)
}
})
Loading
Loading