diff --git a/cmd/api/main.go b/cmd/api/main.go index 8d121b1..0bafae4 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -212,6 +212,8 @@ func run() error { SuppressionRepo: suppressions, UnsubscribeSigner: unsubSigner, StripeClient: stripeClient, + UploadsDir: cfg.UploadsDir, + UploadsPublicURL: cfg.UploadsPublicURL, }) if err != nil { return err diff --git a/docker-compose.yml b/docker-compose.yml index 8a0d440..e2e6df0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -86,6 +86,13 @@ services: GG_STRIPE_WEBHOOK_SECRET: ${GG_STRIPE_WEBHOOK_SECRET:-} GG_STRIPE_PRICE_PRO: ${GG_STRIPE_PRICE_PRO:-} GG_STRIPE_PRICE_BUSINESS: ${GG_STRIPE_PRICE_BUSINESS:-} + # Tier 2 Block D — branding image uploads. Stored on the named + # volume below so they survive container restarts; production + # would back this with S3 + CDN instead. + GG_UPLOADS_DIR: /var/lib/guestguard/uploads + GG_UPLOADS_PUBLIC_URL: http://localhost:8080/uploads + volumes: + - uploads-data:/var/lib/guestguard/uploads ports: - "8080:8080" depends_on: @@ -154,3 +161,4 @@ services: volumes: postgres-data: nats-data: + uploads-data: diff --git a/frontend/app.vue b/frontend/app.vue index db8f9a9..6f36d8d 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -6,7 +6,46 @@ const route = useRoute() // page. Inside the app it just clutters the chrome. const showGithub = computed(() => route.path === '/') +// Profile dropdown state. Closed by default; opens on click, closes on +// outside click / Escape / route change. Industry-standard pattern: the +// account-scoped actions (Account, Billing, Sign out) tuck under an avatar +// affordance instead of crowding the top-level nav. +const profileOpen = ref(false) +const profileRef = ref(null) + +function initials(name?: string | null) { + if (!name) return '·' + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase() || '') + .join('') || '·' +} + +function onDocClick(e: MouseEvent) { + if (!profileOpen.value) return + const root = profileRef.value + if (root && !root.contains(e.target as Node)) profileOpen.value = false +} +function onKeydown(e: KeyboardEvent) { + if (e.key === 'Escape') profileOpen.value = false +} + +if (import.meta.client) { + onMounted(() => { + window.addEventListener('click', onDocClick) + window.addEventListener('keydown', onKeydown) + }) + onUnmounted(() => { + window.removeEventListener('click', onDocClick) + window.removeEventListener('keydown', onKeydown) + }) +} +watch(() => route.fullPath, () => { profileOpen.value = false }) + async function signOut() { + profileOpen.value = false await auth.logout() navigateTo('/') } @@ -16,17 +55,22 @@ async function signOut() {
- + + GuestGuard -