Frontend
Overview
CaterCow's frontend is composed of four distinct applications, all rooted under js/ in the monorepo. Three are Nuxt 2 applications sharing a common js/nuxt2/shared/ layer; the fourth is a modern Nuxt 4 + Vue 3 application. Each app serves a distinct user segment and is deployed as its own service.
| App | Framework | Base Path | Rendering | Deployment |
|---|---|---|---|---|
| Public / Orderer | Nuxt 2 + Vue 2 (Bridge) | / | SSR | nuxt-public |
| Caterer | Nuxt 2 + Vue 2 (Bridge) | /caterer | SPA | nuxt-caterer |
| Admin | Nuxt 2 + Vue 2 (Bridge) | /new_admin | SPA | nuxt-admin |
| Marketing | Nuxt 4 + Vue 3 + Pinia | / | SSR + ISR | nuxt4-marketing |
The Public and Marketing apps both serve / — they are differentiated by route ownership. Marketing handles SEO and CMS-driven pages; Public owns the authenticated orderer experience (search, checkout, customer dashboard).
Application Breakdown
Public / Orderer (js/nuxt2/public/)
The orderer-facing application. It owns the full customer lifecycle: search, restaurant discovery, individual package pages, checkout, and the post-order customer dashboard (orders, addresses, account settings). SSR is enabled in production; a demo mode flag (IS_DEMO=1) disables it.
Key pages:
pages/search.vue— restaurant and package searchpages/orders/_id/checkout.vue— multi-step checkout flowpages/customer/— customer dashboard (orders, addresses, profile)pages/organization/— team/organization views (mirrored to/customer/viaextendRoutes)
Runtime config exposes: Stripe keys, Google Maps API key, reCAPTCHA, PostHog, Customer.io, Zendesk, Segment, Transloadit, Ghost blog, and Google Tag Manager IDs.
Caterer (js/nuxt2/caterer/)
A single-page application (SSR disabled) for restaurant partners to manage their business on the platform. Authentication is required for all meaningful routes.
Key pages:
pages/_current_brand/orders/index.vue— order management dashboardpages/login.vue,pages/create-account.vue,pages/join.vue— caterer auth flows
Admin (js/nuxt2/admin/)
A SPA housing internal tooling for CaterCow operations staff. The root redirect (/) sends users to /admin. Font Awesome is loaded via CDN for icon support here.
Key pages:
pages/orders_by_date.vue— order pipeline viewpages/menus/— menu management and AI-assisted checkspages/brands/,pages/teams/— entity management
Marketing (js/nuxt4/)
The public-facing, SEO-optimized application built with Nuxt 4 and Vue 3. It integrates with Storyblok CMS for content blocks and Ghost for blog content. Static and ISR pages cover marketing content, explore/place pages, help articles, and restaurant profiles.
App shell (app.vue):
- Sets canonical URL and environment-appropriate favicon via
useHead - Adds
noindexin non-production environments - Fetches current user 300ms after mount; polls
/cpi/v2/user/statsevery 30 seconds when logged in
Key pages:
pages/index.vue— homepagepages/explore/[place]/catering.vue— geo-specific catering browsepages/catering/[kitchen].vue— individual restaurant/kitchen profilepages/help/[type]/[slug].vue— help center articles
Nuxt 4 modules: @storyblok/nuxt, @nuxt/content, @nuxt/image, @formkit/nuxt, @pinia/nuxt, @pinia-plugin-persistedstate/nuxt, @vueuse/nuxt, @nuxtjs/sitemap, @nuxtjs/robots, nuxt-jsonld, nuxt-site-config, nuxt-svgo, @nuxtjs/google-fonts.
Shared Layer (Nuxt 2)
js/nuxt2/shared/ is the common code pool for the three Nuxt 2 apps. Build aliases (~shared) make imports clean across app boundaries.
js/nuxt2/shared/
├── assets/stylesheets/ # Global SCSS: variables, mixins, responsive, typography
├── components/ # Domain-organized shared Vue components (~800 total across Nuxt 2)
│ ├── auth/ # Login modals, auth wrappers
│ ├── checkout/ # Checkout step components
│ ├── navigation/ # Nav bars, menus
│ ├── orders/ # Order cards, tables
│ └── ... # common/, inputs/, search/, ui/, etc.
├── composables/ # Vue composables (Nuxt 2 Composition API)
├── helpers/ # Pure utility functions (api.js, urls.js, utils.js)
├── plugins/ # api.js, axios.js — the shared API service layer
├── store/ # Shared Vuex modules: auth.js, front.js, ui.js
└── stores/ # Pinia stores used in Nuxt 2 hybrid modeRouting
Nuxt 2 — File-based + extendRoutes
All three Nuxt 2 apps use Nuxt's file-based router. The Public app extends routing further via extendRoutes in nuxt.config.js:
- Checkout → Modify cloning: The checkout flow's child routes are copied onto a parallel
/orders/:id/modifyroute, excluding poll-only steps (create-poll,extras). - Organization → Customer mirroring: All routes under
/organization/:organizationIdare copied to/customer/:customerId, keeping both URL structures in sync without duplicating page files. - Legacy redirects: A
redirectarray remaps legacy URLs (/customer/orders/*→/customer/me/orders/*,/poll/*→/attendee/group-order/*, etc.) and handles inactive brand 301 redirects loaded frominactive-brand-redirects.csv.
The Admin app redirects the root path to /admin.
Nuxt 4 — File-based + router.options.ts + routeRules
Custom scroll behavior is defined in router.options.ts:
- Hash links (
to.hash) scroll smoothly to the target element. - Same-path navigation (query-only changes) does not scroll.
- Forward/back navigation restores the saved position after a 500ms delay.
Production routeRules in nuxt.config.ts control caching and ISR:
- ISR pages:
/,/industries/**,/help/**,/dishes/**,/restaurant-profiles/** - Long-lived asset cache:
/_nuxt/**(30 days) - Proxy targets:
/catering/*/referral/**→ nuxt-public;/blog-api/**and/bloks/**cached at 60s - Static redirects are generated from
redirects.csvat build time
State Management
Nuxt 2 — Vuex (with Pinia hybrid)
Each Nuxt 2 app has a Vuex store rooted at store/index.js. The shared Vuex modules are:
| Module | Purpose |
|---|---|
shared/store/auth.js | Current user, session, CSRF token, brands, teams, stats, auth scopes |
shared/store/front.js | Front-end global state (banners, flash messages) |
shared/store/ui.js | UI state (modals, drawers, loading flags) |
The Admin and Public apps also use Pinia alongside Vuex for newer features. This hybrid approach was adopted to incrementally migrate toward Pinia without rewriting existing Vuex stores.
js/nuxt2/admin/plugins/pinia.js— registers Pinia in the Admin appjs/nuxt2/shared/stores/menuChecksStore.js— Pinia store for menu quality checks (accesses axios vianuxt2Context)js/nuxt2/public/stores/rosters.js— Pinia store for roster management
Nuxt 4 — Pure Pinia
The Marketing app uses Pinia exclusively, with persistence via @pinia-plugin-persistedstate/nuxt.
| Store | Purpose |
|---|---|
stores/auth.js | User session, CSRF token, brands, stats, action items |
stores/search.js | Search results, undeliverable address flag |
stores/front.js | Global front-end state |
stores/ui.js | UI state |
API Layer
Nuxt 2
js/nuxt2/shared/plugins/api.js is injected as $api into every Nuxt 2 component and store. It wraps $axios with a resource builder pattern:
// Plural resource example
$api.buildResources('order', 'v1/orderer', 'orders', extras)
// → $api.orders.list(filters), $api.orders.fetch(id), $api.orders.create(data), etc.All requests include context headers derived from route params:
X-CSRF-Token— from Vuex auth stateCC-As-Team— organization contextCC-As-User— customer contextCC-As-Brand— caterer brand context
Endpoints follow the pattern /cpi/v1/<namespace>/<resource> and /cpi/v2/<namespace>/<resource>.
Nuxt 4
js/nuxt4/plugins/api.js builds the same resource abstraction using Nuxt's $fetch.create() instead of axios. The plugin is accessed via the useApi() composable:
// composables/use-api.js
export function useApi() {
return useNuxtApp().$api;
}
// Fetch a list with auto-watched filters
const { records, count, loading } = await useApiList(
api => api.v1.public.packages.search,
filters
);useApiFetch and useApiList wrap useAsyncData for SSR-safe data fetching, with useApiList automatically re-fetching when reactive filters change.
The /fpi/* prefix is used for cached proxy variants served by the Nuxt 4 server layer in production.
Style Architecture
Both Nuxt 2 and Nuxt 4 share the same SCSS structural pattern:
assets/stylesheets/common/
├── all.scss # Entry point — ONLY variables and mixins (no output CSS)
├── variables.scss # Color, spacing, typography tokens
├── mixins.scss # Layout and component mixins
├── responsive.scss # Breakpoint mixins and utilities
└── typography.scss # Font definitions (imported at app level, not in all.scss)Important: all.scss is injected into every SFC via styleResources in Nuxt 2 (and imported globally in Nuxt 4's app.vue). It must contain only variables and mixins — any actual CSS output here would be duplicated into every component bundle.
Component styles use <style lang="scss" scoped> throughout. The Admin app has a separate style entry (admin/assets/stylesheets/all.scss) layered on top of the shared base.
Component Architecture
| Location | Scope | Approx. Count |
|---|---|---|
js/nuxt2/shared/components/ | Shared across all Nuxt 2 apps | ~800 (combined with app-specific) |
js/nuxt2/public/components/ | Public app only | — |
js/nuxt2/caterer/components/ | Caterer app only | — |
js/nuxt2/admin/components/ | Admin app only | — |
js/nuxt4/components/ | Marketing app | ~223 |
js/nuxt4/storyblok/ | CMS content blocks | 18 |
Nuxt 2 SFCs use the Options API; Nuxt 4 uses the Composition API (<script setup>). The shared Nuxt 2 components are organized by domain (auth/, checkout/, navigation/, orders/, search/, etc.) rather than by atomic design tier.
Storyblok blocks in js/nuxt4/storyblok/ are registered as globally available components by the @storyblok/nuxt module. Each file corresponds to a content block type defined in the Storyblok CMS (e.g., BrandsListBlok.vue, ReviewsBlok.vue, PackagesBlok.vue).
Key Data Flows
Auth + Stats (Nuxt 4)
app.vue onMounted
→ auth.fetchUser() (stores/auth.js → plugins/api.js → /cpi/v2/user)
→ useUtm() (captures UTM params from URL)
auth.loggedIn watch
→ fetchStatsWithTimer() (polls /cpi/v2/user/stats every 30s)Order Checkout (Nuxt 2 Public)
checkout page (pages/orders/_id/checkout.vue)
→ Vuex checkout module (store/checkout/actions.js)
→ $api (shared/plugins/api.js)
→ $axios (shared/plugins/axios.js)
→ /cpi/v1/orderer/* endpointsAdmin Menu Checks (Nuxt 2 Admin)
menu-checks pages (pages/menus/_menu_id/menu-checks/)
→ useMenuChecksStore() (shared/stores/menuChecksStore.js — Pinia)
→ $axios via nuxt2Context
→ /cpi/v1/admin/restaurant_menus/:id/* endpointsUtilities and Composables
Helper modules provide pure functions used across both frameworks:
| Path | Purpose |
|---|---|
js/nuxt2/shared/helpers/api.js | Request utilities (header builders, error parsing) |
js/nuxt2/shared/helpers/urls.js | URL building, parameterization, null removal |
js/nuxt4/helpers/urls.js | Same utilities, ESM-compatible for Nuxt 4 |
js/nuxt4/composables/use-api.js | useApi(), useApiFetch(), useApiList() |
js/nuxt2/shared/utils/search.js | Search parameter normalization |
Custom directives in Nuxt 4:
plugins/scrollSpy.client.js/plugins/scrollSpy.server.js— scroll-spy behavior with client/server split to avoid SSR hydration issues.
TypeScript is used selectively in Nuxt 4 for data contracts and build scripts (e.g., composables/use-cuisine-links.ts, scripts/build-cuisine-links.ts). The Nuxt 2 codebase is JavaScript throughout.