Files
coffee/src/views/HomeView.vue
Julien Calixte 645f93069c Initial commit: Coffee Map PWA
Vue 3 + Vite PWA backed by ATProto PDS (coffee.apoena.dev).
Stores coffee spots as dev.apoena.coffeespot records with name,
geolocation, note, and status. Map via MapLibre + OpenFreeMap,
auth via ATProto OAuth, deploy via Docker + Nginx on Coolify.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:01:17 +01:00

138 lines
4.2 KiB
Vue

<template>
<div class="h-screen flex flex-col bg-coffee-50">
<!-- Header -->
<header class="bg-coffee-800 text-white px-4 py-3 flex items-center justify-between shadow-md shrink-0">
<div class="flex items-center gap-2">
<span class="text-xl"></span>
<h1 class="font-semibold">Coffee Map</h1>
</div>
<div class="flex items-center gap-2">
<button
@click="showAddModal = true"
class="bg-coffee-600 hover:bg-coffee-500 text-white text-sm font-medium px-3 py-1.5 rounded-lg transition-colors"
>
+ Add
</button>
<button
@click="auth.signOut()"
class="text-coffee-300 hover:text-white text-sm transition-colors px-1"
title="Sign out"
>
</button>
</div>
</header>
<!-- View toggle -->
<div class="flex border-b border-coffee-200 bg-white shrink-0">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
class="flex-1 py-2.5 text-sm font-medium transition-colors"
:class="activeTab === tab.id
? 'text-coffee-800 border-b-2 border-coffee-700'
: 'text-coffee-400 hover:text-coffee-700'"
>
{{ tab.label }}
</button>
</div>
<!-- Filter bar -->
<div class="px-4 py-2 bg-white border-b border-coffee-100 flex gap-2 shrink-0">
<input
v-model="search"
type="search"
placeholder="Search…"
class="flex-1 px-3 py-1.5 text-sm border border-coffee-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-coffee-400"
/>
<select
v-model="statusFilter"
class="px-2 py-1.5 text-sm border border-coffee-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-coffee-400 text-coffee-700"
>
<option value="">All</option>
<option value="want">Want to go</option>
<option value="visited">Visited</option>
</select>
</div>
<!-- Main content -->
<div class="flex-1 overflow-hidden">
<MapView
v-show="activeTab === 'map'"
:shops="filteredShops"
@select="selectedShop = $event"
/>
<ListView
v-show="activeTab === 'list'"
:shops="filteredShops"
@select="selectedShop = $event"
/>
</div>
<!-- Shop detail drawer -->
<Transition name="slide-up">
<div
v-if="selectedShop"
class="fixed bottom-0 left-0 right-0 bg-white rounded-t-2xl shadow-2xl p-5 z-50 max-h-60vh overflow-y-auto"
@click.self="selectedShop = null"
>
<ShopCard :shop="selectedShop" @close="selectedShop = null" expanded />
</div>
</Transition>
<!-- Add shop modal -->
<AddShopModal v-if="showAddModal" @close="showAddModal = false" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useShopsStore } from '@/stores/shops'
import type { Shop } from '@/stores/shops'
import MapView from '@/components/MapView.vue'
import ListView from '@/components/ListView.vue'
import ShopCard from '@/components/ShopCard.vue'
import AddShopModal from '@/components/AddShopModal.vue'
const auth = useAuthStore()
const shopsStore = useShopsStore()
const activeTab = ref<'map' | 'list'>('map')
const search = ref('')
const statusFilter = ref<'' | 'want' | 'visited'>('')
const selectedShop = ref<Shop | null>(null)
const showAddModal = ref(false)
const tabs = [
{ id: 'map' as const, label: '🗺 Map' },
{ id: 'list' as const, label: '☰ List' },
]
const filteredShops = computed(() =>
shopsStore.shops.filter((s) => {
const q = search.value.toLowerCase()
const matchesSearch =
!q ||
s.record.name.toLowerCase().includes(q) ||
s.record.note?.toLowerCase().includes(q)
const matchesStatus = !statusFilter.value || s.record.status === statusFilter.value
return matchesSearch && matchesStatus
}),
)
onMounted(() => shopsStore.fetchAll())
</script>
<style scoped>
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.25s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
}
</style>