22 KiB
Web User UI — TanStack Start App
Overview
Create apps/web-ui — a server-rendered web app using TanStack Start, Tailwind CSS, shadcn/ui, and tRPC. It mirrors apps/user-ui functionality (auth, products, stores, 1hr delivery, cart, checkout, orders, addresses, coupons, complaints, profile) excluding mobile-only features (notifications, location, haptics, hardware back, status bar) and payment (COD only).
Create packages/web-components — a shared web component library with shadcn primitives and domain components used by the web-ui app.
Phase 1: Initialize packages/web-components
1.1 Create the package
packages/web-components/
├── package.json # name: "web-components", private: true
├── tsconfig.json
├── tailwind.config.ts
├── postcss.config.js
├── globals.css # Tailwind base + CSS variables from shadcn
├── components.json # shadcn config
├── src/
│ ├── index.ts # barrel export
│ ├── lib/
│ │ ├── utils.ts # cn() helper (clsx + tailwind-merge)
│ │ └── constants.ts # shared constants
│ ├── components/
│ │ └── ui/ # shadcn primitives (added via CLI)
│ ├── hooks/
│ └── services/
1.2 Steps
-
Create
packages/web-components/package.json:- name:
web-components - main:
./src/index.ts - types:
./src/index.ts - private: true
- peerDependencies:
react,react-dom,tailwindcss,class-variance-authority,clsx,tailwind-merge,lucide-react - devDependencies:
typescript,@types/react,tailwindcss,postcss,autoprefixer
- name:
-
Create
tsconfig.json:- jsx:
react-jsx - strict: true
- paths:
@/*->./src/*
- jsx:
-
Initialize Tailwind:
npx tailwindcss initin the package- Configure
contentpaths to scan./src/**/*.{ts,tsx}
-
Initialize shadcn:
- Run
npx shadcn@latest initinsidepackages/web-components - Config values:
- style: default
- base color: neutral / slate
- CSS variables: yes
- globals.css path:
./globals.css - components path:
./src/components/ui - utils path:
./src/lib/utils.ts - tailwind config:
./tailwind.config.ts
- Run
-
Install shadcn UI primitives via CLI:
npx shadcn@latest add button -p .npx shadcn@latest add input -p .npx shadcn@latest add dialog -p .npx shadcn@latest add card -p .npx shadcn@latest add badge -p .npx shadcn@latest add checkbox -p .npx shadcn@latest add sheet -p .npx shadcn@latest add separator -p .npx shadcn@latest add scroll-area -p .npx shadcn@latest add select -p .npx shadcn@latest add tabs -p .npx shadcn@latest add alert-dialog -p .npx shadcn@latest add avatar -p .
-
Build domain components in
packages/web-components/src/components/:Component Web Implementation Mirrors from common-uimy-text.tsx<p>/<span>with Tailwind + variant propstext.tsx my-button.tsxWraps shadcn Buttonwith color variantsbutton.tsx my-text-input.tsxWraps shadcn Inputwith label + errortextinput.tsx my-touchable-opacity.tsx<button>with hover/active/rippletouchable-opacity.tsx loading-dialog.tsxshadcn Dialogwith spinnerloading-dialog.tsx bottom-dialog.tsxshadcn Sheetfrom bottomdialog.tsx confirmation-dialog.tsxshadcn AlertDialogdialog.tsx checkbox.tsxWraps shadcn Checkboxcheckbox.tsx search-bar.tsxInput with search icon + debounced onChange search-bar.tsx data-table.tsxHTML <table>with Tailwind stylingdata-table.tsx quantifier.tsxQuantity +/- stepper control quantifier.tsx mini-quantifier.tsxCompact quantity stepper mini-quantifier.tsx image-viewer.tsxFull-screen image modal image-viewer.tsx image-uploader.tsxFile input → preview grid ImageUploader.tsx image-uploader-neo.tsxFile input with usePickImage pattern ImageUploaderNeo.tsx image-carousel.tsxHorizontal scroll with dot indicators ImageCarousel.tsx profile-image.tsxAvatar with fallback initials profile-image.tsx app-container.tsxMax-width centered column layout (scrollable div, no KeyboardAwareScrollView) app-container.tsx flat-list.tsxCSS grid / flexbox layout flat-list.tsx dropdown.tsxshadcn Selectdropdown.tsx date-picker.tsxNative <input type="date">or shadcn popoverdate-picker.tsx google-sign-in.tsxOAuth PKCE via expo-auth-session equivalent (web flow) google-sign-in.tsx -
Copy hooks from
packages/ui/hooks/topackages/web-components/src/hooks/:theme-context.tsx— adapt to CSS variable-based theming (remove react-native dependency)usePagination.tsx— strip react-native imports, use pure JSuseIsDevMode.ts— usewindow.location.hostnameinstead of PlatformuseFocusCallback.ts— use IntersectionObserver or TanStack hooks
-
Copy services from
packages/ui/src/services/topackages/web-components/src/services/:StorageService.ts— localStorage only (remove SecureStore branch + Platform check)StorageServiceCasual.ts— adapt to localStorage only
-
Copy lib files from
packages/ui/src/lib/as needed:constants.ts— shared string constants (export same values)theme-colors.ts— adapt to CSS variable namesrefresh-context.tsx— strip react-native imports
Phase 2: Initialize apps/web-ui
2.1 Bootstrap with TanStack Start
cd apps
mkdir web-ui && cd web-ui
npm create tanstack-app@latest . -- --start --tailwind --typescript
This creates:
- File-based routing in
app/routes/ - SSR enabled via Vinxi
- Tailwind CSS configured
- TypeScript
- TanStack Router with file conventions
2.2 Add additional dependencies
npm install @trpc/client @trpc/react-query @trpc/server @tanstack/react-query zustand axios dayjs formik yup fuse.js jwt-decode clsx tailwind-merge class-variance-authority lucide-react
npm install -D @types/react
2.3 Configure path aliases in tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./app/*"],
"web-components": ["../../packages/web-components/src"],
"web-components/*": ["../../packages/web-components/src/*"],
"@packages/shared": ["../../packages/shared"],
"@packages/shared/*": ["../../packages/shared/*"],
"@backend/*": ["../../apps/backend/src/*"]
}
}
}
2.4 Configure workspace in root package.json
Add "apps/web-ui" to the workspaces array.
2.5 Add to turbo.json
"web-ui#dev": { "cache": false, "dependsOn": ["^build"] },
"web-ui#build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }
2.6 Add workspace script to root package.json
"web-ui": "bun run --filter web-ui",
"web-ui:dev": "bun run web-ui dev",
"web-ui:build": "bun run web-ui build"
Phase 3: Build Core Infrastructure
3.1 tRPC Client (app/lib/trpc-client.ts)
- Import
AppRoutertype from@backend/trpc/router(via path alias) createTRPCReact<AppRouter>()httpBatchLinkpointing to backend URL (same as user-ui:BASE_API_URL + '/api/trpc')- Auth token injection: read from localStorage via
StorageService.getAuthToken() - No superjson (match user-ui's transport)
3.2 Auth Context (app/lib/auth-context.tsx)
Same flow as user-ui/src/contexts/AuthContext.tsx:
- On mount: check localStorage for stored JWT token
- If found: set
isAuthenticated=true, fetch user data viatrpc.user.user.getSelfData - Provide:
login,loginWithToken,register,logout,updateUser,updateUserDetails - Post-login redirect via stored redirect URL in localStorage
- JWT token management:
getAuthToken(),saveAuthToken(),deleteAuthToken()using localStorage
3.3 Zustand Stores (app/lib/stores/)
Copy and adapt from user-ui/src/store/:
| Store | File | Adaptations for Web |
|---|---|---|
addressStore |
addressStore.ts |
None |
appStore |
appStore.ts |
None |
cartStore |
cartStore.ts |
None |
centralProductStore |
centralProductStore.ts |
Init via loader instead of effect hook |
centralSlotStore |
centralSlotStore.ts |
Init via loader instead of effect hook |
navigationStore |
navigationStore.ts |
None |
quickDeliveryStore |
quickDeliveryStore.ts |
None |
flashCartStore |
flashCartStore.ts |
None |
flashNavigationStore |
flashNavigationStore.ts |
None |
slotStore |
slotStore.ts |
None |
storeHeaderStore |
storeHeaderStore.ts |
None |
3.4 Query Client (app/lib/queryClient.ts)
Standard QueryClient from @tanstack/react-query with same defaults as user-ui.
3.5 API Hooks (app/lib/prominent-api-hooks.ts)
Copy from user-ui/src/hooks/prominent-api-hooks.ts:
useGetEssentialConsts()— tRPC query with 60s refetchuseAllProducts()— Axios get cached products JSONuseStores()— Axios get cached stores JSONuseSlots()— Axios get cached slots JSONuseBanners()— Axios get cached banners JSONuseStoreWithProducts(storeId)— Axios get store-specific JSON
The BASE_API_URL pointed to same backend URL.
3.6 Cart Query Hooks (app/lib/cart-query-hooks.ts)
Copy from user-ui/hooks/cart-query-hooks.tsx:
useGetCart(cartType)— localStorage-based cartuseAddToCart(cartType)— add item, invalidate queryuseUpdateCartItem(cartType)— update quantityuseRemoveFromCart(cartType)— remove itemclearLocalCart(cartType)— clear all
3.7 Other Hooks (app/lib/hooks/)
| Hook | Source | Notes |
|---|---|---|
useJWT.ts |
user-ui/hooks/useJWT.ts |
Adapt to localStorage instead of SecureStore |
useCurrentUserId.ts |
user-ui/hooks/useCurrentUserId.ts |
Decode JWT, same logic |
useUploadToObjectStore.ts |
user-ui/hooks/useUploadToObjectStore.ts |
Same presigned URL upload logic |
useHideTabNav.ts |
user-ui/src/hooks/useHideTabNav.ts |
Adapt to web scroll-based hiding |
3.8 Shared Types
Import types from @packages/shared directly (already in monorepo).
Phase 4: Build Routes
4.1 Root Layout (app/routes/__root.tsx)
Provider hierarchy:
<QueryClientProvider client={queryClient}>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<AuthProvider>
<RefreshProvider queryClient={queryClient}>
<CentralStoreInitializer>
<Outlet />
</CentralStoreInitializer>
</RefreshProvider>
</AuthProvider>
<Toast /> ← react-hot-toast or sonner (toast library)
</trpc.Provider>
</QueryClientProvider>
Removed compared to user-ui's root layout:
- ❌ MyStatusBar — not needed on web
- ❌ SafeAreaProvider/SafeAreaView — use CSS safe area env()
- ❌ UpdateChecker — not relevant for web
- ❌ HealthTestWrapper — version enforcement not needed
- ❌ WebViewWrapper — no webview overlay
- ❌ FirstUserWrapper — no first-time user flow
- ❌ NotificationProvider + NotifChecker — no push notifications
- ❌ PaperProvider — using shadcn instead
- ❌ LocationTestWrapper — no location checks
- ❌ BackHandlerWrapper — no hardware back button
- ❌ ThemeProvider (react-navigation) — use Tailwind dark mode instead
4.2 Route Map (Flattened)
| Route | Source (user-ui) | Description |
|---|---|---|
/ |
app/index.tsx |
Redirect to /home |
/login |
app/(auth)/login.tsx |
3-step OTP/password login |
/register |
app/(auth)/register.tsx |
Registration form |
/home |
app/(drawer)/(tabs)/home/index.tsx |
Dashboard: stores carousel, banners, products grid, search bar, floating cart bar |
/home/cart |
app/(drawer)/(tabs)/home/cart.tsx |
Cart page |
/home/checkout |
app/(drawer)/(tabs)/home/checkout.tsx |
COD-only checkout with address selector, slot picker, order summary |
/home/order-success |
app/(drawer)/(tabs)/home/order-success.tsx |
Order placed success screen |
/home/product/$id |
app/(drawer)/(tabs)/home/product-detail/[id].tsx |
Product detail with reviews |
/home/search |
app/(drawer)/(tabs)/home/search-results/index.tsx |
Live search with Fuse.js |
/flash |
app/(drawer)/(tabs)/flash-delivery/(products)/index.tsx |
1hr delivery product listing |
/flash/cart |
app/(drawer)/(tabs)/flash-delivery/(cart)/cart.tsx |
Flash delivery cart |
/flash/checkout |
app/(drawer)/(tabs)/flash-delivery/checkout.tsx |
Flash delivery checkout (COD only) |
/flash/order-success |
app/(drawer)/(tabs)/flash-delivery/order-success.tsx |
Flash delivery success |
/flash/product/$id |
app/(drawer)/(tabs)/flash-delivery/product-detail/[id].tsx |
Flash product detail |
/stores |
app/(drawer)/(tabs)/stores/index.tsx |
Store listing |
/stores/$storeId |
app/(drawer)/(tabs)/stores/store-detail/[id].tsx |
Store products with tag filters |
/stores/$storeId/product/$productId |
app/(drawer)/(tabs)/stores/store-detail/product-detail/[id].tsx |
Store product detail |
/me |
app/(drawer)/(tabs)/me/index.tsx |
Account menu (orders, addresses, coupons, complaints, profile, about) |
/me/orders |
app/(drawer)/(tabs)/me/my-orders/index.tsx |
Order list with infinite scroll |
/me/orders/$id |
app/(drawer)/(tabs)/me/my-orders/[id].tsx |
Order detail with cancel/notes/complaint |
/me/addresses |
app/(drawer)/(tabs)/me/addresses/index.tsx |
Address CRUD |
/me/coupons |
app/(drawer)/(tabs)/me/coupons/index.tsx |
Coupon list + redeem |
/me/complaints |
app/(drawer)/(tabs)/me/complaints/index.tsx |
Complaint list + raise |
/me/edit-profile |
app/(drawer)/(tabs)/me/edit-profile/index.tsx |
Edit profile |
/me/about |
app/(drawer)/(tabs)/me/about/index.tsx |
About page |
/me/terms |
app/(drawer)/(tabs)/me/terms/index.tsx |
Terms & Conditions |
4.3 Navigation Layout (app/components/nav-layout.tsx)
Bottom navigation bar:
- Home, Stores, Me tabs
- 1hr Delivery as a prominent center button (matching mobile's FAB style)
- Active state highlighting
- Responsive: bottom bar on mobile, sidebar on desktop (optional enhancement)
Wraps all /home, /flash, /stores, /me routes.
4.4 Page Components (common-ui → web-components migration)
Rebuild in app/components/ using web-components:
| Component | Replaces from user-ui |
|---|---|
ProductCard.tsx |
Card with image, name, price, add-to-cart |
CartPage.tsx |
Cart items with quantifiers, totals, proceed to checkout |
CheckoutPage.tsx |
Address selector, slot picker, order summary, place order button (COD only) |
CheckoutAddressSelector.tsx |
Address selection in checkout |
AddressForm.tsx |
Formik + yup address form |
ProductDetail.tsx |
Full product detail with reviews |
SlotProducts.tsx |
Slot-specific product grid |
StoreProducts.tsx |
Store product grid with tag filters |
OrderMenu.tsx |
Order actions (cancel, notes, complaint) |
BannerCarousel.tsx |
Image banners carousel |
FloatingCartBar.tsx |
Sticky bottom cart summary bar |
TermsAndConditionsContent.tsx |
Static terms content |
RegistrationForm.tsx |
Registration form fields |
NextOrderGlimpse.tsx |
Next upcoming order summary |
TabLayoutWrapper.tsx |
Wrapper for screen with optional tabs |
FlashDeliveryProducts.tsx |
Flash-eligible products list |
UserAddressHeader.tsx |
Collapsed address display |
4.5 Payment — Omitted Entirely
- No Razorpay integration
- No PhonePe integration
- Checkout defaults to COD without a payment method selector
- Order status: placed directly without payment flow
placeOrdermutation sendspaymentMethod: 'cod'always
Phase 5: Wrappers Removed (vs user-ui)
| user-ui Wrapper | Reason Removed |
|---|---|
| ThemeProvider (react-navigation) | Using Tailwind CSS variables + dark mode |
| SafeAreaProvider / SafeAreaView | Handled by CSS env(safe-area-inset-*) |
| MyStatusBar | No status bar on web |
| UpdateChecker (expo-updates) | Not applicable to web |
| HealthTestWrapper | Version enforcement not needed |
| WebViewWrapper | No webview overlay needed |
| FirstUserWrapper | No first-time user flow needed |
| NotificationProvider | No push notifications on web |
| NotifChecker | No push token registration |
| PaperProvider | Using shadcn/ui instead |
| LocationTestWrapper | No location checks |
| BackHandlerWrapper | No hardware back button on web |
| CentralStoreInitializer | KEPT — needed for product/slot store hydration |
Phase 6: tRPC Integration
6.1 Client Setup
Direct httpBatchLink to the existing backend URL. No proxy needed — the backend CORS config already allows web origins (currently allows localhost:5174 and ui.freshyo.in; add web-ui dev URL).
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: `${BASE_API_URL}/api/trpc`,
headers: async () => {
const token = await getAuthToken()
return token ? { Authorization: `Bearer ${token}` } : {}
},
}),
],
})
6.2 Used Procedures (same as user-ui)
| Category | Procedures |
|---|---|
| Auth | trpc.user.auth.login, .sendOtp, .verifyOtp, .register, .updateProfile, .deleteAccount |
| User | trpc.user.user.getSelfData, .checkProfileComplete |
| Address | trpc.user.address.getUserAddresses, .getDefaultAddress, .createAddress, .updateAddress, .deleteAddress |
| Order | trpc.user.order.getOrders, .getOrderById, .getRecentlyOrderedProducts, .placeOrder, .cancelOrder, .updateUserNotes |
| Product | trpc.user.product.getProductDetails, .getProductReviews, .createReview |
| Coupon | trpc.user.coupon.getEligible, .getMyCoupons, .redeemReservedCoupon |
| Complaint | trpc.user.complaint.getAll, .raise |
| Cart | trpc.user.cart.getCartSlots |
| File | trpc.user.fileUpload.generateUploadUrls, trpc.common.generateUploadUrls |
| Common | trpc.common.healthCheck, .essentialConsts, .getStoresSummary |
6.3 Caching Strategy
Same as user-ui:
- Products, stores, slots, banners: fetched via Axios from cached JSON files on CDN/assets domain
- tRPC used for dynamic data (auth, orders, addresses, etc.)
Phase 7: Implementation Order
| Step | Task | Depends On |
|---|---|---|
| 1 | Create packages/web-components/ structure, package.json, tsconfig |
— |
| 2 | Initialize Tailwind + shadcn CLI in web-components | 1 |
| 3 | Install shadcn primitives (button, input, dialog, etc.) | 2 |
| 4 | Build domain components in web-components | 3 |
| 5 | Copy hooks + services to web-components | 1 |
| 6 | Bootstrap apps/web-ui with TanStack Start |
— |
| 7 | Add dependencies, configure tsconfig, workspace, turbo | 6 |
| 8 | Build tRPC client, auth context, zustand stores | 7 |
| 9 | Build query client, API hooks, cart hooks | 7 |
| 10 | Build root layout with all providers | 8 |
| 11 | Build login + register routes | 8, 9 |
| 12 | Build home dashboard route | 9, 4 |
| 13 | Build 1hr delivery (flash) routes | 9, 4 |
| 14 | Build stores routes | 9, 4 |
| 15 | Build cart + checkout (COD only) | 9, 4 |
| 16 | Build me section (orders, addresses, coupons, complaints, profile) | 8, 9 |
| 17 | Build about + terms routes | 8 |
| 18 | Add navigation layout (bottom bar) | 10 |
| 19 | Test end-to-end with existing backend | all |
Files That Don't Need Migration
| user-ui File | Reason Not Needed |
|---|---|
services/notif-service/ |
Push notifications not supported on web |
services/toaster.ts |
Replace with sonner/react-hot-toast |
src/components/LocationAttacher.tsx |
No location attach on web |
components/LocationTestWrapper.tsx |
No location checks |
components/HealthTestWrapper.tsx |
No version enforcement |
components/WebViewWrapper.tsx |
No webview overlay |
components/UpdateChecker.tsx |
No expo-updates |
components/BackHandler.tsx |
No hardware back |
components/FirstUserWrapper.tsx |
No first-time user splash |
app/api/auth/ |
OAuth handled differently on web (no expo-router API routes needed) |
src/components/MyStatusBar.tsx |
No status bar concepts on web |
eas.json, app.json, expo-env.d.ts |
Expo-specific config files |
metro.config.js |
Metro not used; TanStack uses Vite |
assets/ |
Static assets handled differently in web builds |
google-services.json |
Android-specific |
State Flow Notes
Auth Flow (same as user-ui)
- App mounts →
AuthProvider.init()checks localStorage for JWT - If token found: fetch user data via
trpc.user.user.getSelfData - Login: OTP or password → receive token → store in localStorage → set auth state
- Registration: collect details → register via tRPC → auto-login
- Logout: clear localStorage → reset auth state → redirect to
/login
Cart Flow (same as user-ui)
- Cart stored in localStorage (
StorageServiceCasual) - Uses TanStack Query with key
local-cart-{cartType} - Add/update/remove operations invalidate query to trigger re-render
- No server-side cart — purely local
Checkout Flow (COD only, no payment)
- User selects address (from saved addresses or adds new)
- User selects delivery slot per store
- User reviews order summary (items, total, delivery charge)
- User clicks "Place Order" →
trpc.user.order.placeOrderwithpaymentMethod: 'cod' - On success → clear cart → navigate to order-success page with order ID
Product/Slot Caching (same as user-ui)
- Products, stores, slots cached JSON fetched via Axios from CDN
centralProductStorekeeps all products in memory withproductsByIdmapcentralSlotStorekeeps slots with per-product availability- Refetch functions stored in stores, triggered by pull-to-refresh