Tavotto
AI drew your research figure. Tavotto makes it publication-ready: edit matplotlib figures on a canvas, lay out in mm, check journal rules. Free, open source.
Upload panoramas, connect rooms and publish stunning virtual tours in minutes. Free to start. No credit card required.
Translation pending
DesignVue · TypeScriptGitHub @peter2validOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗The auth-protected dashboard SPA for Viewora. Built with Nuxt 3, Tailwind CSS, Pinia, and Supabase auth. Deployed to app.viewora.software.
Standalone repo — separate GitHub repository, deployed independently to Vercel. No shared code with
viewora-backendorviewora-marketing.
/p/[slug] and embed spaces via /embed/[slug].| Layer | Tech |
|---|---|
| Framework | Nuxt 3 (CSR/SPA mode for auth-protected routes) |
| Styling | Tailwind CSS v3 (via @nuxtjs/tailwindcss) |
| State | Pinia (stores/auth.ts, stores/plan.ts) |
| Auth | @nuxtjs/supabase — JWT stored in session, forwarded as Bearer token |
| 360° Viewer | @egjs/view360 v3 (via View360Viewer.vue + TourViewer.vue) |
| Fonts | Inter 400–700, Outfit 600–700 via Google Fonts |
| HTTP | useApiFetch composable wraps $fetch with the Supabase JWT |
viewora-app/
├── app.vue # Root layout shell, page transitions
├── nuxt.config.ts # Modules, route rules, runtime config
├── tailwind.config.ts # Design tokens (zinc palette, etc.)
├── .env.example # Required environment variables
│
├── pages/
│ ├── index.vue # Root redirect (→ /app or /login)
│ ├── login.vue # Auth: email/password login
│ ├── register.vue # Auth: email/password registration
│ ├── confirm.vue # Auth: email confirmation callback
│ ├── reset-password.vue # Auth: password reset
│ │
│ ├── app/ # Auth-protected dashboard (all CSR, no-cache)
│ │ ├── index.vue # Dashboard home — summary cards
│ │ ├── analytics.vue # Analytics: views breakdown per space
│ │ ├── billing.vue # Billing: plan selection, Paystack flow, usage
│ │ ├── settings.vue # Profile/account settings
│ │ │
│ │ ├── spaces/
│ │ │ ├── index.vue # Spaces list + create space modal
│ │ │ └── [id]/
│ │ │ └── index.vue # Space detail: upload media, 360 settings, publish
│ │ │
│ │ ├── leads/
│ │ │ └── index.vue # Leads inbox — all inquiries across spaces
│ │ │
│ │ └── capture/
│ │ └── index.vue # 360 capture tool (plan-gated)
│ │
│ ├── p/
│ │ └── [slug].vue # Public space viewer (no auth required)
│ │
│ └── embed/
│ └── [slug].vue # Minimal embed-optimized viewer (iframe safe)
│
├── components/
│ ├── SidebarLink.vue # Dashboard nav link with active state
│ ├── app/
│ │ └── ConfirmationModal.vue # Reusable delete-confirm dialog
│ └── viewer/
│ ├── View360Viewer.vue # @egjs/view360 panorama renderer (client-only)
│ ├── TourViewer.vue # Full tour UI: scene strip, hotspot popups, share sheet
│ └── Hotspot.vue # Individual hotspot overlay marker
│
├── composables/
│ ├── useApiFetch.ts # $fetch wrapper that injects Supabase Bearer token
│ └── useSpaces.ts # CRUD + publish operations for spaces
│
├── stores/
│ ├── auth.ts # User identity, profile, displayName, initials
│ └── plan.ts # Subscription status, plan limits, can() entitlement checks
│
├── middleware/
│ ├── auth.ts # Redirects unauthenticated users to /login
│ ├── guest.ts # Redirects authenticated users away from /login, /register
│ └── redirects.global.ts # Global redirect rules
│
├── layouts/ # Nuxt layouts (dashboard shell with sidebar)
├── plugins/ # Nuxt plugins
├── server/ # Nitro server routes (proxy if NUXT_PUBLIC_API_BASE_URL not set)
├── types/
│ └── database.types.ts # Supabase-generated TypeScript types
└── assets/
└── css/main.css # Global CSS, Tailwind base
composables/useApiFetch.tsThe single HTTP abstraction for the entire app. Reads the Supabase session access token and injects it as Authorization: Bearer <token> on every call. Respects NUXT_PUBLIC_API_BASE_URL — if unset, calls go to Nitro proxy routes at /api/*; if set, calls go directly to the Fastify backend. This is the only way the app should make API calls.
composables/useSpaces.tsEncapsulates all space CRUD operations: fetchSpaces, fetchSpace, createSpace, updateSpace, deleteSpace, publishSpace. All calls go through useApiFetch. Spaces are called "spaces" in the frontend but stored as properties in the database — the backend normalizes this at the API boundary.
stores/auth.tsWraps useSupabaseUser() into a Pinia store. Exposes: user (JWT payload), profile (from /profile API), isLoggedIn, displayName, avatarInitials. fetchProfile() loads the profile row from the backend on login.
stores/plan.tsLoaded once on login via fetchSubscriptionStatus(). Exposes the active plan's feature flags. Use planStore.can('lead_capture_enabled') to gate plan-specific UI. Falls back to a hardcoded Free plan object if the API fails. Every premium UI element must check this store.
pages/p/[slug].vuePublic tour viewer — SSR with Vercel edge caching (s-maxage=60). Fetches via GET /p/:slug which calls the get_tour_data() Supabase RPC (returns space + scenes + hotspots). Renders TourViewer → View360Viewer for 360° panoramas. Includes lead capture form (gated on lead_form_enabled). View analytics are recorded server-side — no client-side ping needed.
pages/app/billing.vueHandles the entire billing lifecycle: plan listing, Paystack checkout initialization, success redirect handling, and subscription status display with usage meters.
nuxt.config.ts)| Route | Mode | Notes |
|---|---|---|
/login, /register, /confirm, /reset-password | CSR | Auth pages |
/p/** | SSR | public, s-maxage=60 — Vercel edge cache, fast first paint |
/embed/** | CSR | Iframe-safe, no auth |
/app/** | CSR, no-store | Auth-protected dashboard |
/api/** | — | private, no-store caching headers |
/login.middleware/auth.ts guards all /app/** routes — redirects to /login if no session.useApiFetch reads useSupabaseSession().access_token and forwards it on every backend call.middleware/auth.ts is bypassed (if (process.dev) return).cp .env.example .env
# Required:
# SUPABASE_URL=https://<ref>.supabase.co
# SUPABASE_KEY=<anon-key>
# Optional (defaults to Nitro proxy if unset):
# NUXT_PUBLIC_API_BASE_URL=http://localhost:3000
npm install
npm run dev # http://localhost:3001
| Variable | Required | Default | Description |
|---|---|---|---|
SUPABASE_URL | ✅ | — | Supabase project URL |
SUPABASE_KEY | ✅ | — | Supabase anon/public key |
NUXT_PUBLIC_APP_URL | — | https://app.viewora.software | App's own public URL |
NUXT_PUBLIC_API_BASE_URL | — | '' (same-origin Nitro) | Set to Fastify URL for external backend |
viewora-backend via useApiFetch. The app does not query Supabase tables directly (except auth session management via @nuxtjs/supabase).viewora-marketing links to app.viewora.software for login/register and for any "Get Started" CTAs. The marketing site's /login and /register pages are thin redirects into this app.viewora.software (marketing) can link to app.viewora.software/p/[slug] for any published space.