feat: add WannPassts booking app with Go backend and Vue frontend
Initial project setup for a privacy-focused, self-hosted scheduling app: - Go backend with JWT auth, SQLite storage, and chi router - Calendar integrations via Google OAuth (FreeBusy), CalDAV, and ICS links - Public booking pages with configurable slots and booking requests - AES-256-GCM encryption for stored provider tokens - Vue 3 + TypeScript frontend with Vite, Pinia, and Vue Router - Docs, .gitignore, and .env.example for local development
This commit is contained in:
commit
cb30223fd4
47 changed files with 6599 additions and 0 deletions
149
frontend/src/views/DashboardView.vue
Normal file
149
frontend/src/views/DashboardView.vue
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuth } from '../stores/auth'
|
||||
import { useToast } from '../stores/toast'
|
||||
import LogoMark from '../components/LogoMark.vue'
|
||||
import ShareLinkCard from '../components/ShareLinkCard.vue'
|
||||
import SectionConnections from '../components/SectionConnections.vue'
|
||||
import SectionRequests from '../components/SectionRequests.vue'
|
||||
import SectionSettings from '../components/SectionSettings.vue'
|
||||
|
||||
const auth = useAuth()
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const tab = ref<'overview' | 'requests' | 'calendars' | 'settings'>('overview')
|
||||
const loading = ref(true)
|
||||
|
||||
const tabs = [
|
||||
{ value: 'overview', label: 'Übersicht' },
|
||||
{ value: 'requests', label: 'Anfragen' },
|
||||
{ value: 'calendars', label: 'Kalender' },
|
||||
{ value: 'settings', label: 'Einstellungen' },
|
||||
] as const
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.init()
|
||||
if (!auth.user) {
|
||||
auth.logout()
|
||||
router.push({ name: 'login' })
|
||||
return
|
||||
}
|
||||
loading.value = false
|
||||
|
||||
if (route.query.google === 'ok') {
|
||||
toast.ok('Google Kalender verbunden 🎉')
|
||||
tab.value = 'calendars'
|
||||
} else if (route.query.google === 'error') {
|
||||
toast.error('Google-Verbindung fehlgeschlagen: ' + (route.query.reason || 'unbekannter Fehler'))
|
||||
tab.value = 'calendars'
|
||||
} else if (typeof route.query.tab === 'string') {
|
||||
tab.value = route.query.tab as typeof tab.value
|
||||
}
|
||||
if (Object.keys(route.query).length > 0) {
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
})
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="page dash" v-if="!loading && auth.user">
|
||||
<header class="glass dash-header">
|
||||
<div class="row">
|
||||
<LogoMark />
|
||||
<strong class="brand">WannPassts</strong>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="muted small hide-sm">{{ auth.user.email }}</span>
|
||||
<button class="btn btn-sm" @click="logout">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tabs">
|
||||
<button
|
||||
v-for="t in tabs"
|
||||
:key="t.value"
|
||||
class="tab"
|
||||
:class="{ active: tab === t.value }"
|
||||
@click="tab = t.value"
|
||||
>
|
||||
{{ t.label }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div v-if="tab === 'overview'" class="col overview">
|
||||
<ShareLinkCard />
|
||||
<div class="overview-grid">
|
||||
<router-link :to="{ query: { tab: 'calendars' } }" class="glass tile" @click="tab = 'calendars'">
|
||||
<span class="tile-icon">📆</span>
|
||||
<strong>Kalender verbinden</strong>
|
||||
<span class="muted small">Google, iCloud/CalDAV oder ICS-Link hinzufügen</span>
|
||||
</router-link>
|
||||
<router-link :to="{ query: { tab: 'requests' } }" class="glass tile" @click="tab = 'requests'">
|
||||
<span class="tile-icon">📨</span>
|
||||
<strong>Anfragen prüfen</strong>
|
||||
<span class="muted small">Buchungsanfragen annehmen oder ablehnen</span>
|
||||
</router-link>
|
||||
<router-link :to="{ query: { tab: 'settings' } }" class="glass tile" @click="tab = 'settings'">
|
||||
<span class="tile-icon">⚙️</span>
|
||||
<strong>Buchungsregeln</strong>
|
||||
<span class="muted small">Zeitfenster, Dauern, Slot-Größe festlegen</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<section class="glass privacy">
|
||||
<strong>🔒 Datenschutz by Design</strong>
|
||||
<p class="muted small" style="margin: 6px 0 0">
|
||||
Besucher deiner Buchungsseite sehen ausschließlich <em>frei</em> oder <em>belegt</em> –
|
||||
niemals Titel, Ort oder Beschreibung deiner Termine. Google-Anbindungen nutzen die
|
||||
FreeBusy-API, die strukturell keine Termindetails liefert.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<SectionRequests v-else-if="tab === 'requests'" />
|
||||
<SectionConnections v-else-if="tab === 'calendars'" />
|
||||
<SectionSettings v-else />
|
||||
</main>
|
||||
|
||||
<main v-else class="page" style="display: flex; justify-content: center; padding-top: 20vh">
|
||||
<div class="skeleton" style="width: 320px; height: 120px"></div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dash { display: flex; flex-direction: column; gap: 18px; }
|
||||
.dash-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
.brand { font-size: 17px; letter-spacing: -0.01em; }
|
||||
.overview { gap: 18px; }
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 22px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tile:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.24); }
|
||||
.tile-icon { font-size: 26px; }
|
||||
.privacy { padding: 20px 24px; }
|
||||
@media (max-width: 520px) { .hide-sm { display: none; } }
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue