freshyo/web-user-ui.md
2026-05-10 11:06:57 +05:30

511 lines
22 KiB
Markdown

# 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
1. 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`
2. Create `tsconfig.json`:
- jsx: `react-jsx`
- strict: true
- paths: `@/*` -> `./src/*`
3. Initialize Tailwind:
- `npx tailwindcss init` in the package
- Configure `content` paths to scan `./src/**/*.{ts,tsx}`
4. Initialize shadcn:
- Run `npx shadcn@latest init` inside `packages/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`
5. 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 .`
6. Build domain components in `packages/web-components/src/components/`:
| Component | Web Implementation | Mirrors from `common-ui` |
|-----------|-------------------|--------------------------|
| `my-text.tsx` | `<p>`/`<span>` with Tailwind + variant props | text.tsx |
| `my-button.tsx` | Wraps shadcn `Button` with color variants | button.tsx |
| `my-text-input.tsx` | Wraps shadcn `Input` with label + error | textinput.tsx |
| `my-touchable-opacity.tsx` | `<button>` with hover/active/ripple | touchable-opacity.tsx |
| `loading-dialog.tsx` | shadcn `Dialog` with spinner | loading-dialog.tsx |
| `bottom-dialog.tsx` | shadcn `Sheet` from bottom | dialog.tsx |
| `confirmation-dialog.tsx` | shadcn `AlertDialog` | dialog.tsx |
| `checkbox.tsx` | Wraps shadcn `Checkbox` | checkbox.tsx |
| `search-bar.tsx` | Input with search icon + debounced onChange | search-bar.tsx |
| `data-table.tsx` | HTML `<table>` with Tailwind styling | data-table.tsx |
| `quantifier.tsx` | Quantity +/- stepper control | quantifier.tsx |
| `mini-quantifier.tsx` | Compact quantity stepper | mini-quantifier.tsx |
| `image-viewer.tsx` | Full-screen image modal | image-viewer.tsx |
| `image-uploader.tsx` | File input → preview grid | ImageUploader.tsx |
| `image-uploader-neo.tsx` | File input with usePickImage pattern | ImageUploaderNeo.tsx |
| `image-carousel.tsx` | Horizontal scroll with dot indicators | ImageCarousel.tsx |
| `profile-image.tsx` | Avatar with fallback initials | profile-image.tsx |
| `app-container.tsx` | Max-width centered column layout (scrollable div, no KeyboardAwareScrollView) | app-container.tsx |
| `flat-list.tsx` | CSS grid / flexbox layout | flat-list.tsx |
| `dropdown.tsx` | shadcn `Select` | dropdown.tsx |
| `date-picker.tsx` | Native `<input type="date">` or shadcn popover | date-picker.tsx |
| `google-sign-in.tsx` | OAuth PKCE via expo-auth-session equivalent (web flow) | google-sign-in.tsx |
7. Copy hooks from `packages/ui/hooks/` to `packages/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 JS
- `useIsDevMode.ts` — use `window.location.hostname` instead of Platform
- `useFocusCallback.ts` — use IntersectionObserver or TanStack hooks
8. Copy services from `packages/ui/src/services/` to `packages/web-components/src/services/`:
- `StorageService.ts` — localStorage only (remove SecureStore branch + Platform check)
- `StorageServiceCasual.ts` — adapt to localStorage only
9. 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 names
- `refresh-context.tsx` — strip react-native imports
---
## Phase 2: Initialize `apps/web-ui`
### 2.1 Bootstrap with TanStack Start
```bash
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
```bash
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`
```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`
```json
"web-ui#dev": { "cache": false, "dependsOn": ["^build"] },
"web-ui#build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }
```
### 2.6 Add workspace script to root `package.json`
```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 `AppRouter` type from `@backend/trpc/router` (via path alias)
- `createTRPCReact<AppRouter>()`
- `httpBatchLink` pointing 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 via `trpc.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 refetch
- `useAllProducts()` — Axios get cached products JSON
- `useStores()` — Axios get cached stores JSON
- `useSlots()` — Axios get cached slots JSON
- `useBanners()` — Axios get cached banners JSON
- `useStoreWithProducts(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 cart
- `useAddToCart(cartType)` — add item, invalidate query
- `useUpdateCartItem(cartType)` — update quantity
- `useRemoveFromCart(cartType)` — remove item
- `clearLocalCart(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
- `placeOrder` mutation sends `paymentMethod: '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).
```typescript
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)
1. App mounts → `AuthProvider.init()` checks localStorage for JWT
2. If token found: fetch user data via `trpc.user.user.getSelfData`
3. Login: OTP or password → receive token → store in localStorage → set auth state
4. Registration: collect details → register via tRPC → auto-login
5. 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)
1. User selects address (from saved addresses or adds new)
2. User selects delivery slot per store
3. User reviews order summary (items, total, delivery charge)
4. User clicks "Place Order" → `trpc.user.order.placeOrder` with `paymentMethod: 'cod'`
5. 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
- `centralProductStore` keeps all products in memory with `productsById` map
- `centralSlotStore` keeps slots with per-product availability
- Refetch functions stored in stores, triggered by pull-to-refresh