DEAD_CODE_CLEAN #7

Merged
shafi merged 30 commits from DEAD_CODE_CLEAN into master 2026-09-14 04:32:52 +00:00
15 changed files with 1790 additions and 346 deletions
Showing only changes of commit 4aa7d8a1d4 - Show all commits

View file

@ -45,7 +45,12 @@
"Shell(done:*)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && echo \"=== product.ts trpc import line & usage count ===\" && sed -n '1,40p' apps/backend/src/trpc/apis/admin-apis/apis/product.ts && echo \"=== count occurrences of each beyond import ===\" && for fn in checkUnitExists getProductImagesById replaceProductTags; do echo \"$fn: total=$(grep -c \"$fn\" apps/backend/src/trpc/apis/admin-apis/apis/product.ts)\"; done)",
"Shell(node -e const fs = require(\"fs\"); const parse = (p) => { const src = fs.readFileSync(p,\"utf8\"); const names = new Set(); // export { a, b as c, ... } from / re-export lists const re = /\\bexport\\s*\\{([^}]*)\\}/g; let m; while ((m = re.exec(src))) { const body = m[1]; for (let line of body.split(\",\")) { line = line.trim().replace(/\\/\\/.*$/,\"\").trim(); if (!line) continue; const asMatch = line.match(/^(.+?)\\s+as\\s+(.+)$/); if (asMatch) names.add(asMatch[2].trim()); else if (line.match(/^[A-Za-z_$][\\w$]*$/)) names.add(line); } } return names; }; const sqlite = parse(\"packages/db_helper_sqlite/index.ts\"); const pg = parse(\"packages/db_helper_postgres/index.ts\"); const onlySqlite = [...sqlite].filter(x=>!pg.has(x)).sort(); const onlyPg = [...pg].filter(x=>!sqlite.has(x)).sort(); console.log(\"ONLY IN SQLITE INDEX:\", onlySqlite.join(\", \")); console.log(\"ONLY IN POSTGRES INDEX:\", onlyPg.join(\", \")); )",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/packages/db_helper_postgres npx tsc --noEmit > /tmp/pg_tsc.log 2>&1 echo \"tsc exit: $?\" echo \"=== error count ===\"; grep -c \"error TS\" /tmp/pg_tsc.log echo \"=== errors touching shared types / banner / coupon / store / const / complaint / staff-user ===\" grep -E \"admin-apis/(banner|coupon|store|const|complaint|staff-user|slots|vendor-snippets|order)\\.ts\" /tmp/pg_tsc.log | head -20 echo \"=== total error files ===\" grep \"error TS\" /tmp/pg_tsc.log | grep -oE \"^[^(]+\" | sort -u)"
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/packages/db_helper_postgres npx tsc --noEmit > /tmp/pg_tsc.log 2>&1 echo \"tsc exit: $?\" echo \"=== error count ===\"; grep -c \"error TS\" /tmp/pg_tsc.log echo \"=== errors touching shared types / banner / coupon / store / const / complaint / staff-user ===\" grep -E \"admin-apis/(banner|coupon|store|const|complaint|staff-user|slots|vendor-snippets|order)\\.ts\" /tmp/pg_tsc.log | head -20 echo \"=== total error files ===\" grep \"error TS\" /tmp/pg_tsc.log | grep -oE \"^[^(]+\" | sort -u)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/web-ui && npx tsc --noEmit > /tmp/me_tsc.txt 2>&1; echo \"exit=$?\"; wc -l /tmp/me_tsc.txt; head -20 /tmp/me_tsc.txt)",
"Shell(sleep:*)",
"Shell(for p in /me /me/orders /me/addresses /me/coupons /me/complaints /me/edit-profile /me/terms; do code=$(curl -s -o /dev/null -w \"%{http_code}\" \"http://localhost:4175$p\"); echo \"$p -> $code\"; done)",
"Shell(curl -s http://localhost:4175/me)",
"Shell(pkill -f \"vite dev\" 2>/dev/null; pkill -f \"web-ui\" 2>/dev/null; echo \"stopped\")"
],
"deny": [],
"defaultMode": "default"

View file

@ -30,7 +30,7 @@
- Prefers detailed technical documentation of system architecture, data models, flows, and integrations in markdown format. Confidence: 0.8
- Wants edge cases explicitly enumerated when documenting or analyzing existing code/systems. Confidence: 0.8
- Values end-to-end analysis of features (architecture, data model, flows, integrations, and edge cases) when asked to explain how something works. Confidence: 0.7
- When creating a clone/mirror of an app on a different platform (e.g., a web version of a React Native app), expects EXACT replication — functionality and looks must match the reference app with zero changes ("not even a slight change is acceptable"). The tech stack is inherited from the existing clone, not the original. Confidence: 0.9
- When creating a clone/mirror of an app on a different platform (e.g., a web version of a React Native app), expects EXACT replication — functionality and looks must match the reference app with zero changes ("not even a slight change is acceptable"). The tech stack is inherited from the existing clone, not the original. The exactness requirement applies per section and down to every sub-page: naming a section of the RN app (e.g., "the 'me' section and all its sub pages") means the entire subtree must be an exact replica of the mobile screens, and he will flag remaining gaps ("I see a lot of gaps") if any sub-screen diverges — so the agent should audit every sub-page, not just the section hub, against the RN source of truth. Confidence: 0.95
- Prefers unifying parallel data structures into a single instance rather than maintaining separate ones (e.g., one cart for flash and regular items instead of separate flash/regular carts). Confidence: 0.9
- Prefers using sentinel values in existing fields (e.g., slotId = 0) to distinguish special-case items rather than creating separate fields or structures. Confidence: 0.9
- Prefers handling special-case logic locally in the relevant component/file rather than globally or via parallel flows. Confidence: 0.85

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'
import type { FlashDeliveryProps } from '@packages/shared'
import { useNavigate } from '@tanstack/react-router'
import { useNavigate, useLocation } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks'
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks'
@ -27,6 +27,8 @@ const formatTimeRange = (deliveryTime: string | Date) => {
export function FloatingCartBar({ isFlashDelivery = false }: FlashDeliveryProps) {
const navigate = useNavigate()
const location = useLocation()
const isCartPage = location.pathname === '/cart' || location.pathname === '/flash/cart' || location.pathname.startsWith('/me')
const [isExpanded, setIsExpanded] = useState(false)
const [quantities, setQuantities] = useState<Record<number, number>>({})
const cartType = isFlashDelivery ? 'flash' : 'regular'
@ -78,7 +80,7 @@ export function FloatingCartBar({ isFlashDelivery = false }: FlashDeliveryProps)
/* ---------- Desktop: slide-over panel ---------- */
const slideOver = (
<div className="pointer-events-auto flex h-full w-full max-w-md flex-col border-l border-gray-200 bg-white">
<div className="pointer-events-auto flex h-full w-full max-w-[85vw] flex-col border-l border-gray-200 bg-white">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
<div>
@ -147,7 +149,7 @@ export function FloatingCartBar({ isFlashDelivery = false }: FlashDeliveryProps)
className="h-14 w-14 shrink-0 rounded-lg border border-gray-100 bg-gray-50 object-cover"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold text-gray-900">
<p className="text-sm font-bold text-gray-900 break-words">
{productsById[item.skuId]?.name || ''}
</p>
<p className="text-xs font-medium text-gray-500">
@ -222,7 +224,7 @@ export function FloatingCartBar({ isFlashDelivery = false }: FlashDeliveryProps)
return (
<>
{/* Mobile: compact bottom bar */}
<div className="fixed bottom-18 left-3 right-3 z-40 md:hidden">
<div className={`fixed bottom-18 left-3 right-3 z-[60] md:hidden ${isExpanded || isCartPage ? 'hidden' : ''}`}>
<button
onClick={() => itemCount > 0 && setIsExpanded(true)}
className="flex w-full items-center justify-between rounded-xl px-4 py-3 shadow-lg"
@ -250,7 +252,7 @@ export function FloatingCartBar({ isFlashDelivery = false }: FlashDeliveryProps)
{/* Desktop: slide-over */}
<div
className={`fixed inset-0 z-50 transition-opacity duration-300 ${
className={`fixed inset-0 z-[70] transition-opacity duration-300 ${
isExpanded ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
}`}
>

View file

@ -0,0 +1,252 @@
import React, { useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { BottomDialog } from 'web-components'
import { trpc } from '../../lib/trpc-client'
import ComplaintForm from '../ComplaintForm'
import { MoreVertical, ChevronRight, Eye, StickyNote, AlertTriangle, XCircle } from 'lucide-react'
interface MeOrderMenuProps {
orderId: number
postActionHandler?: () => void
}
/**
* Port of user-ui's OrderMenu (the 3-dot menu on each order card):
* View Details / Edit Notes / Raise Complaint / Cancel Order.
* Uses web BottomDialog for the menu and nested actions.
*/
function ActionRow({
icon,
iconBg,
iconColor,
title,
subtitle,
onClick,
}: {
icon: React.ReactNode
iconBg: string
iconColor: string
title: string
subtitle: string
onClick: () => void
}) {
return (
<button
onClick={onClick}
className="mb-3 flex w-full flex-row items-center rounded-xl border border-gray-100 bg-white p-4 text-left shadow-sm transition-colors hover:bg-gray-50"
>
<div className={`mr-4 flex h-10 w-10 items-center justify-center rounded-full ${iconBg}`}>
<span className={iconColor}>{icon}</span>
</div>
<div className="flex-1">
<p className="text-base font-semibold text-gray-900">{title}</p>
<p className="text-xs text-gray-500">{subtitle}</p>
</div>
<ChevronRight className="h-6 w-6 text-gray-400" />
</button>
)
}
export function MeOrderMenu({ orderId, postActionHandler }: MeOrderMenuProps) {
const navigate = useNavigate()
const [open, setOpen] = useState(false)
const [editNotesDialogOpen, setEditNotesDialogOpen] = useState(false)
const [editNotes, setEditNotes] = useState('')
const [complaintDialogOpen, setComplaintDialogOpen] = useState(false)
const [cancelDialogOpen, setCancelDialogOpen] = useState(false)
const [cancelReason, setCancelReason] = useState('')
const cancelOrderMutation = trpc.user.order.cancelOrder.useMutation()
const updateNotesMutation = trpc.user.order.updateUserNotes.useMutation({
onSuccess: () => {
alert('Notes updated successfully')
setEditNotesDialogOpen(false)
setEditNotes('')
postActionHandler?.()
},
onError: (error: any) => {
alert(error.message || 'Failed to update notes')
},
})
const handleEditNotes = async () => {
try {
await updateNotesMutation.mutateAsync({
id: orderId,
userNotes: editNotes.trim(),
})
} catch (error) {
// handled in mutation
}
}
const handleCancelOrder = async () => {
if (!cancelReason.trim()) {
alert('Please enter a reason for cancellation')
return
}
try {
await cancelOrderMutation.mutateAsync({ id: orderId, reason: cancelReason })
alert('Order cancelled successfully')
setCancelDialogOpen(false)
setCancelReason('')
postActionHandler?.()
} catch (error: any) {
alert(error.message || 'Failed to cancel order')
}
}
const handleComplaintClose = () => {
setComplaintDialogOpen(false)
postActionHandler?.()
}
return (
<>
<button
onClick={() => setOpen(true)}
aria-label="Order options"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition-colors hover:bg-gray-50"
>
<MoreVertical className="h-[18px] w-[18px] text-slate-600" />
</button>
{/* Menu Dialog */}
<BottomDialog open={open} onClose={() => setOpen(false)}>
<div className="px-4 pb-8 pt-2">
<div className="mb-6 flex items-center justify-center flex-col">
<div className="mb-4 h-1.5 w-12 rounded-full bg-gray-200" />
<p className="text-lg font-bold text-gray-900">Order Options</p>
<p className="text-sm text-gray-500">Select an action for Order #{orderId}</p>
</div>
<ActionRow
icon={<Eye className="h-5 w-5" />}
iconBg="bg-gray-50"
iconColor="text-gray-600"
title="View Details"
subtitle="View complete order information"
onClick={() => {
setOpen(false)
navigate({ to: '/me/orders/$id', params: { id: String(orderId) } })
}}
/>
<ActionRow
icon={<StickyNote className="h-5 w-5" />}
iconBg="bg-blue-50"
iconColor="text-blue-600"
title="Edit Notes"
subtitle="Add special instructions"
onClick={() => {
setOpen(false)
setEditNotes('')
setEditNotesDialogOpen(true)
}}
/>
<ActionRow
icon={<AlertTriangle className="h-5 w-5" />}
iconBg="bg-yellow-50"
iconColor="text-yellow-600"
title="Raise Complaint"
subtitle="Report an issue with this order"
onClick={() => {
setOpen(false)
setComplaintDialogOpen(true)
}}
/>
<ActionRow
icon={<XCircle className="h-5 w-5" />}
iconBg="bg-red-50"
iconColor="text-red-600"
title="Cancel Order"
subtitle="Request order cancellation"
onClick={() => {
setOpen(false)
setCancelDialogOpen(true)
}}
/>
</div>
</BottomDialog>
{/* Edit Notes Dialog */}
<BottomDialog open={editNotesDialogOpen} onClose={() => setEditNotesDialogOpen(false)}>
<div className="p-6">
<div className="mb-4 flex flex-row items-center justify-between">
<p className="text-xl font-bold text-gray-900">Edit Instructions</p>
<button onClick={() => setEditNotesDialogOpen(false)}>
<span className="text-gray-400"></span>
</button>
</div>
<textarea
className="mb-6 min-h-32 w-full rounded-xl border border-gray-200 bg-gray-50 p-4 text-base text-gray-800"
value={editNotes}
onChange={(e) => setEditNotes(e.target.value)}
placeholder="Add special delivery instructions here..."
rows={4}
/>
<button
className={`w-full rounded-xl bg-blue-600 py-4 text-center font-bold text-white shadow-sm ${
updateNotesMutation.isPending ? 'opacity-70' : ''
}`}
onClick={handleEditNotes}
disabled={updateNotesMutation.isPending}
>
{updateNotesMutation.isPending ? 'Saving...' : 'Save Instructions'}
</button>
</div>
</BottomDialog>
{/* Cancel Order Dialog */}
<BottomDialog open={cancelDialogOpen} onClose={() => setCancelDialogOpen(false)}>
<div className="p-6">
<div className="mb-4 flex flex-row items-center justify-between">
<p className="text-xl font-bold text-gray-900">Cancel Order</p>
<button onClick={() => setCancelDialogOpen(false)}>
<span className="text-gray-400"></span>
</button>
</div>
<div className="mb-4 rounded-xl border border-red-100 bg-red-50 p-4">
<p className="text-sm text-red-800">
Are you sure you want to cancel this order? This action cannot be undone.
</p>
</div>
<p className="mb-2 font-medium text-gray-700">Reason for cancellation</p>
<textarea
className="mb-6 min-h-24 w-full rounded-xl border border-gray-200 bg-gray-50 p-4 text-base text-gray-800"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
placeholder="Please tell us why you are cancelling..."
rows={3}
/>
<button
className={`flex w-full items-center justify-center rounded-xl bg-red-600 py-4 text-center font-bold text-white shadow-sm ${
cancelOrderMutation.isPending ? 'opacity-70' : ''
}`}
onClick={handleCancelOrder}
disabled={cancelOrderMutation.isPending}
>
{cancelOrderMutation.isPending ? 'Processing...' : 'Confirm Cancellation'}
</button>
</div>
</BottomDialog>
{/* Raise Complaint Dialog */}
<BottomDialog open={complaintDialogOpen} onClose={handleComplaintClose}>
<ComplaintForm
open={complaintDialogOpen}
onClose={handleComplaintClose}
orderId={orderId}
/>
</BottomDialog>
</>
)
}

View file

@ -0,0 +1,38 @@
import React from 'react'
import { useNavigate } from '@tanstack/react-router'
import { ChevronLeft } from 'lucide-react'
interface MeScreenHeaderProps {
title: string
subtitle?: string
right?: React.ReactNode
}
/**
* Replicates the RN user-ui stack headers for /me sub pages:
* white bar, back chevron, bold title + optional subtitle, optional right slot.
* Mobile-only (`md:hidden`) on desktop the global AppLayout topbar already
* supplies the bar, so this prevents stacked duplicate headers.
*/
export function MeScreenHeader({ title, subtitle, right }: MeScreenHeaderProps) {
const navigate = useNavigate()
return (
<div className="flex flex-row items-center justify-between border-b border-gray-100 bg-white px-4 py-4 md:hidden">
<div className="flex min-w-0 flex-row items-center">
<button
onClick={() => navigate({ to: '/me' })}
aria-label="Go back"
className="-ml-2 mr-1 p-2"
>
<ChevronLeft className="h-6 w-6 text-gray-800" />
</button>
<div className="min-w-0">
<p className="truncate text-lg font-bold text-gray-900">{title}</p>
{subtitle && <p className="text-xs text-gray-400">{subtitle}</p>}
</div>
</div>
{right && <div className="flex shrink-0 items-center">{right}</div>}
</div>
)
}

View file

@ -0,0 +1,200 @@
import React, { useState, useRef, useCallback } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { trpc } from '../../lib/trpc-client'
import { useGetEssentialConsts } from '../../hooks/prominent-api-hooks'
import { useAuth } from '../../lib/auth-context'
import { orderStatusManipulator } from '../../lib/string-manipulators'
import dayjs from 'dayjs'
import { Loader2, ChevronRight } from 'lucide-react'
import type { UserOrderSummary } from '@packages/shared'
/**
* Port of user-ui's NextOrderGlimpse: a horizontal snap-scroll of amber
* "Upcoming Order" cards shown on the /me hub below the header.
*/
interface OrderCardProps {
order: UserOrderSummary
supportMobile?: string | null
onPress: () => void
}
function formatRange(startIso: string | undefined) {
const startTime = dayjs(startIso)
const endTime = startTime.add(1, 'hour')
return `${startTime.format('DD MMM, hh:mm A')} - ${endTime.format('hh:mm A')}`
}
function OrderCard({ order, supportMobile, onPress }: OrderCardProps) {
return (
<button
onClick={onPress}
className="mr-6 w-[88vw] max-w-[95%] shrink-0 snap-start text-left md:w-[380px] md:max-w-none"
>
<div className="rounded-2xl border-2 border-amber-300 bg-gradient-to-r from-amber-50 to-orange-50 p-4">
<div className="mb-3 flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<div className="mr-2 h-2 w-2 rounded-full bg-amber-500" />
<p className="text-[10px] font-bold uppercase tracking-wider text-amber-600">
Upcoming Order
</p>
</div>
{order.isFlashDelivery && (
<div className="flex flex-row items-center rounded-full bg-amber-200 px-2 py-0.5">
<span className="mr-0.5 text-[10px]"></span>
<p className="text-[8px] font-black uppercase text-amber-700">Flash</p>
</div>
)}
</div>
<div className="mb-3 flex flex-row items-start justify-between">
<div>
<p className="text-lg font-extrabold text-gray-900">#{order.orderId}</p>
<p className="mt-0.5 text-xs text-gray-500">
{order.items.length} {order.items.length === 1 ? 'item' : 'items'} {order.totalAmount}
</p>
</div>
<div className="rounded-lg border border-amber-200 bg-white px-2 py-1">
<p className="text-[10px] font-bold uppercase text-amber-700">
{orderStatusManipulator(order.deliveryStatus)}
</p>
</div>
</div>
<div className="flex flex-row items-center">
<span className="mr-1.5 text-amber-600">{order.isFlashDelivery ? '⚡' : '🕐'}</span>
<p className="text-xs font-medium text-amber-800">
{order.isFlashDelivery
? '30-Min Delivery'
: formatRange(order.deliveryDate || order.orderDate)}
</p>
</div>
{order.items.length > 0 && (
<div className="mt-3 flex flex-row items-center border-t border-amber-200 pt-3">
<div className="flex flex-row">
{order.items.slice(0, 3).map((item, idx) => (
<div
key={idx}
className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-lg border border-amber-200 bg-white"
style={{ marginLeft: idx > 0 ? -8 : 0 }}
>
{item.image ? (
<img src={item.image} alt="" className="h-full w-full rounded-lg object-cover" />
) : null}
</div>
))}
{order.items.length > 3 && (
<div className="ml-2 flex h-8 w-8 items-center justify-center rounded-lg border border-amber-200 bg-amber-100">
<p className="text-[9px] font-bold text-amber-700">+{order.items.length - 3}</p>
</div>
)}
</div>
<p className="ml-2 text-xs font-medium text-amber-700">Track Order</p>
<ChevronRight className="ml-auto h-3.5 w-3.5 text-amber-600" />
</div>
)}
{supportMobile && (
<a
href={`tel:${supportMobile}`}
className="mt-3 flex flex-row items-center border-t border-amber-200 pt-3"
>
<span className="mr-1.5 text-amber-600">📞</span>
<p className="text-xs font-medium text-amber-700">
Need help? Call {supportMobile}
</p>
</a>
)}
</div>
</button>
)
}
export function NextOrderGlimpse() {
const navigate = useNavigate()
const { user } = useAuth()
const [currentIndex, setCurrentIndex] = useState(0)
const scrollerRef = useRef<HTMLDivElement>(null)
const { data: ordersData, isLoading } = trpc.user.order.getOrders.useQuery(
{ page: 1, pageSize: 50 },
{ enabled: !!user }
)
const { data: essentialConsts } = useGetEssentialConsts()
const upcomingOrders = (ordersData?.data || [])
.filter((order) => {
if (order.orderStatus.toLowerCase() === 'cancelled') return false
if (order.deliveryStatus.toLowerCase() === 'success') return false
return true
})
.sort((a, b) => {
if (a.isFlashDelivery && !b.isFlashDelivery) return -1
if (!a.isFlashDelivery && b.isFlashDelivery) return 1
if (a.isFlashDelivery && b.isFlashDelivery) {
return dayjs(b.createdAt).diff(dayjs(a.createdAt))
}
if (a.deliveryDate && b.deliveryDate) {
return dayjs(a.deliveryDate).diff(dayjs(b.deliveryDate))
}
return 0
})
const handleScroll = useCallback(() => {
const el = scrollerRef.current
if (!el) return
const index = Math.round(el.scrollLeft / (el.clientWidth || 1))
setCurrentIndex(Math.min(Math.max(index, 0), Math.max(upcomingOrders.length - 1, 0)))
}, [upcomingOrders.length])
if (!user) return null
if (isLoading) {
return (
<div className="mb-4 px-6">
<div className="flex items-center justify-center rounded-2xl border border-gray-100 bg-white p-4">
<Loader2 className="h-5 w-5 animate-spin text-blue-500" />
</div>
</div>
)
}
if (upcomingOrders.length === 0) return null
const goToOrder = (order: UserOrderSummary) => {
navigate({ to: '/me/orders/$id', params: { id: String(order.id) } })
}
return (
<div className="mb-4">
<div
ref={scrollerRef}
onScroll={handleScroll}
className="flex snap-x snap-mandatory flex-row overflow-x-auto scrollbar-hide px-6"
>
{upcomingOrders.map((order) => (
<OrderCard
key={order.id}
order={order}
supportMobile={essentialConsts?.supportMobile}
onPress={() => goToOrder(order)}
/>
))}
</div>
{upcomingOrders.length > 1 && (
<div className="mt-2 flex flex-row justify-center">
{upcomingOrders.map((_, index) => (
<div
key={index}
className={`mx-1 h-2 w-2 rounded-full ${
index === currentIndex ? 'bg-amber-500' : 'bg-amber-200'
}`}
/>
))}
</div>
)}
</div>
)
}

View file

@ -1,65 +1,257 @@
import { createFileRoute } from '@tanstack/react-router'
import { trpc } from '../lib/trpc-client'
import { p, MyButton, AppContainer } from 'web-components'
import { MapPin, Plus } from 'lucide-react'
import { useState } from 'react'
import { MeScreenHeader } from '../components/me/MeScreenHeader'
import { AddressForm } from '../components/AddressForm'
import { BottomDialog } from 'web-components'
import type { UserAddress } from '@packages/shared'
import { MapPin, Plus, AlertTriangle } from 'lucide-react'
export const Route = createFileRoute('/me/addresses')({ component: AddressesPage })
function AddressesPage() {
const utils = trpc.useUtils()
const { data } = trpc.user.address.getUserAddresses.useQuery()
const deleteMutation = trpc.user.address.deleteAddress.useMutation({
onSuccess: () => utils.user.address.getUserAddresses.invalidate(),
})
const addresses = data?.data || []
// Address view — required core of the shared UserAddress + optional geo fields
type Address = Pick<
UserAddress,
'id' | 'name' | 'phone' | 'addressLine1' | 'addressLine2' | 'city' | 'state' | 'pincode' | 'isDefault'
> & {
latitude?: number | null
longitude?: number | null
googleMapsUrl?: string | null
}
const formatAddress = (addr: Address) => {
return `${addr.addressLine1}${addr.addressLine2 ? `, ${addr.addressLine2}` : ''}, ${addr.city}, ${addr.state} - ${addr.pincode}`
}
function AddressCard({
address,
onEdit,
onDelete,
onSetDefault,
isDeleting,
}: {
address: Address
onEdit: (address: Address) => void
onDelete: (id: number) => void
onSetDefault: (id: number) => void
isDeleting?: boolean
}) {
return (
<AppContainer>
<p className="font-bold mb-4 text-xl">
My Addresses
</p>
<div className="mb-3 rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
<div className="mb-2 flex flex-row items-start justify-between">
<div className="flex-1">
<div className="mb-1 flex flex-row items-center">
<p className="mr-2 text-lg font-semibold text-gray-800">{address.name}</p>
{address.isDefault && (
<span className="rounded-full bg-green-100 px-2 py-1">
<p className="text-xs font-medium text-green-700">Default</p>
</span>
)}
</div>
<p className="mb-1 text-sm text-gray-600">📞 {address.phone}</p>
<p className="text-sm text-gray-700">📍 {formatAddress(address)}</p>
</div>
</div>
{addresses.length === 0 ? (
<div className="flex flex-col items-center gap-4 py-20">
<MapPin className="h-12 w-12 text-gray-300" />
<p className="text-gray-500">No addresses saved</p>
</div>
) : (
<div className="flex flex-col gap-3">
{addresses.map((addr: any) => (
<div
key={addr.id}
className="rounded-xl border border-gray-100 bg-white p-4 shadow-sm"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="font-semibold">{addr.name}</p>
<p className="text-sm text-gray-600">
{addr.addressLine1}
{addr.addressLine2 ? `, ${addr.addressLine2}` : ''}
</p>
<p className="text-sm text-gray-600">
{addr.city}, {addr.state} - {addr.pincode}
</p>
<p className="text-sm text-gray-500">{addr.phone}</p>
{addr.isDefault && (
<span className="mt-1 inline-block rounded-full bg-brand-100 px-2 py-0.5 text-xs text-brand-700">
Default
</span>
)}
</div>
<button
onClick={() => deleteMutation.mutate({ id: addr.id })}
className="text-sm text-red-500 hover:text-red-700"
>
Delete
</button>
</div>
</div>
))}
</div>
)}
</AppContainer>
<div className="mt-3 flex flex-row justify-end">
{!address.isDefault && (
<button
onClick={() => onSetDefault(address.id)}
className="mr-2 rounded bg-gray-500 px-3 py-2 transition-colors hover:bg-gray-600"
>
<p className="text-xs font-medium text-white">Set Default</p>
</button>
)}
<button
onClick={() => onEdit(address)}
className="mr-2 rounded bg-blue-500 px-3 py-2 transition-colors hover:bg-blue-600"
>
<p className="text-xs font-medium text-white">Edit</p>
</button>
<button
onClick={() => onDelete(address.id)}
disabled={isDeleting}
className={`rounded bg-red-500 px-3 py-2 transition-colors hover:bg-red-600 ${
isDeleting ? 'opacity-50' : ''
}`}
>
<p className="text-xs font-medium text-white">
{isDeleting ? 'Deleting...' : 'Delete'}
</p>
</button>
</div>
</div>
)
}
function AddressesPage() {
const [modalVisible, setModalVisible] = useState(false)
const [editingAddress, setEditingAddress] = useState<Address | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(null)
const { data, isLoading, error, refetch } = trpc.user.address.getUserAddresses.useQuery()
const addresses = (data?.data || []) as Address[]
const updateAddressMutation = trpc.user.address.updateAddress.useMutation()
const deleteAddressMutation = trpc.user.address.deleteAddress.useMutation()
const handleAddAddress = () => {
setEditingAddress(null)
setModalVisible(true)
}
const handleEditAddress = (address: Address) => {
setEditingAddress(address)
setModalVisible(true)
}
const handleDeleteAddress = (id: number) => {
const ok = window.confirm('Are you sure you want to delete this address?')
if (!ok) return
setDeletingId(id)
deleteAddressMutation.mutate(
{ id },
{
onSuccess: () => {
setDeletingId(null)
refetch()
alert('Address deleted successfully')
},
onError: (error: any) => {
setDeletingId(null)
alert(error.message || 'Failed to delete address')
},
}
)
}
const handleSetDefault = (id: number) => {
const addressToUpdate = addresses.find((addr) => addr.id === id)
if (!addressToUpdate) return
updateAddressMutation.mutate(
{
id: addressToUpdate.id,
name: addressToUpdate.name,
phone: addressToUpdate.phone,
addressLine1: addressToUpdate.addressLine1,
addressLine2: addressToUpdate.addressLine2 || undefined,
city: addressToUpdate.city,
state: addressToUpdate.state,
pincode: addressToUpdate.pincode,
isDefault: true,
},
{
onSuccess: () => {
refetch()
alert('Default address updated')
},
onError: (error: any) => {
alert(error.message || 'Failed to update default address')
},
}
)
}
const handleAddressSubmit = () => {
setModalVisible(false)
setEditingAddress(null)
refetch()
}
return (
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="My Addresses" />
{isLoading ? (
<div className="flex min-h-[60vh] flex-1 items-center justify-center">
<p className="text-gray-600">Loading addresses...</p>
</div>
) : error ? (
<div className="flex min-h-[60vh] flex-1 flex-col items-center justify-center p-4">
<AlertTriangle className="h-12 w-12 text-red-500" />
<p className="mt-2 mb-4 text-center text-red-600">Failed to load addresses. Please try again.</p>
<button
onClick={() => refetch()}
className="rounded bg-blue-500 px-4 py-2 text-white"
>
Retry
</button>
</div>
) : (
<div className="mx-auto w-full max-w-2xl">
{/* Header row */}
<div className="flex flex-row items-center justify-between p-4 pb-2">
<p className="text-2xl font-bold text-gray-800">My Addresses</p>
<button
onClick={handleAddAddress}
aria-label="Add address"
className="rounded-full bg-blue-500 p-2 transition-colors hover:bg-blue-600"
>
<Plus className="h-6 w-6 text-white" />
</button>
</div>
{addresses.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<MapPin className="h-16 w-16 text-gray-300" />
<p className="mb-2 mt-4 text-center text-gray-500">No addresses found</p>
<p className="mb-6 text-center text-gray-400">Add your first delivery address</p>
<button
onClick={handleAddAddress}
className="rounded-lg bg-blue-500 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-600"
>
Add Address
</button>
</div>
) : (
<div className="p-4">
{addresses.map((address) => (
<AddressCard
key={address.id}
address={address}
onEdit={handleEditAddress}
onDelete={handleDeleteAddress}
onSetDefault={handleSetDefault}
isDeleting={deletingId === address.id}
/>
))}
</div>
)}
</div>
)}
{/* Add/Edit bottom sheet */}
<BottomDialog open={modalVisible} onClose={() => setModalVisible(false)}>
<div className="w-full">
<AddressForm
onSuccess={handleAddressSubmit}
initialValues={
editingAddress
? {
id: editingAddress.id,
name: editingAddress.name,
phone: editingAddress.phone,
addressLine1: editingAddress.addressLine1,
addressLine2: editingAddress.addressLine2 || '',
city: editingAddress.city,
state: editingAddress.state,
pincode: editingAddress.pincode,
isDefault: editingAddress.isDefault,
latitude: editingAddress.googleMapsUrl
? undefined
: editingAddress.latitude ?? undefined,
longitude: editingAddress.googleMapsUrl
? undefined
: editingAddress.longitude ?? undefined,
googleMapsUrl: editingAddress.googleMapsUrl ?? undefined,
}
: undefined
}
isEdit={!!editingAddress}
/>
</div>
</BottomDialog>
</div>
)
}

View file

@ -1,11 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { trpc } from '../lib/trpc-client'
import { p, AppContainer, div } from 'web-components'
import { Phone, Mail, Headphones, CheckCircle, Clock, AlertCircle, ThumbsUp, MessageSquare } from 'lucide-react'
import { MeScreenHeader } from '../components/me/MeScreenHeader'
import { Phone, Mail, Headphones, Clock, AlertCircle, ThumbsUp } from 'lucide-react'
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks'
import dayjs from 'dayjs'
import { Dialog } from '../components/Dialog'
import { useState } from 'react'
import type { ComplaintItemProps } from '@packages/shared'
export const Route = createFileRoute('/me/complaints')({ component: ComplaintsPage })
@ -75,33 +73,35 @@ function ComplaintsPage() {
const { data, isLoading, error, refetch } = trpc.user.complaint.getAll.useQuery()
const complaints = data?.complaints || []
const { data: constsData } = useGetEssentialConsts()
const [showContactDialog, setShowContactDialog] = useState(false)
if (isLoading) {
return (
<AppContainer>
<div className="flex min-h-full flex-1 items-center justify-center bg-gray-50">
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="Complaints" />
<div className="flex min-h-[60vh] flex-1 items-center justify-center bg-gray-50">
<p className="font-medium text-gray-500">Loading complaints...</p>
</div>
</AppContainer>
</div>
)
}
if (error) {
return (
<AppContainer>
<div className="flex min-h-full flex-1 flex-col items-center justify-center bg-gray-50">
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="Complaints" />
<div className="flex min-h-[60vh] flex-1 flex-col items-center justify-center bg-gray-50">
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
<p className="text-lg font-bold text-gray-900">Oops!</p>
<p className="mt-2 text-gray-500">Failed to load complaints</p>
</div>
</AppContainer>
</div>
)
}
return (
<AppContainer>
<div className="min-h-full flex-1 bg-gray-50">
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="Complaints" />
<div className="flex flex-col bg-gray-50">
{/* Support Header */}
<div className="bg-brand-600 px-4 py-5">
<div className="mb-4 flex flex-row items-center">
@ -171,6 +171,6 @@ function ComplaintsPage() {
)}
</div>
</div>
</AppContainer>
</div>
)
}

View file

@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { trpc } from '../lib/trpc-client'
import { useState } from 'react'
import { p, MyButton, AppContainer, div } from 'web-components'
import { MeScreenHeader } from '../components/me/MeScreenHeader'
import { Ticket, AlertCircle } from 'lucide-react'
import dayjs from 'dayjs'
import type { UserCouponDisplay } from '@packages/shared'
@ -116,64 +116,69 @@ function CouponsPage() {
if (isLoading) {
return (
<AppContainer>
<div className="flex flex-1 items-center justify-center">
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="My Coupons" />
<div className="flex min-h-[60vh] flex-1 items-center justify-center">
<p className="text-gray-600">Loading coupons...</p>
</div>
</AppContainer>
</div>
)
}
if (error) {
return (
<AppContainer>
<div className="flex flex-1 flex-col items-center justify-center p-4">
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="My Coupons" />
<div className="flex min-h-[60vh] flex-1 flex-col items-center justify-center p-4">
<AlertCircle className="mb-2 h-12 w-12 text-red-500" />
<p className="text-center text-red-600">Failed to load coupons. Please try again.</p>
</div>
</AppContainer>
</div>
)
}
return (
<AppContainer>
<div className="flex-1 p-4">
{/* Add Coupon Card */}
<div className="mb-6 rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
<p className="mb-3 text-lg font-semibold text-gray-800">Add a Coupon</p>
<input
type="text"
placeholder="Enter coupon code"
value={code}
onChange={(e) => setCode(e.target.value)}
className="mb-4 w-full rounded-lg border border-gray-200 px-4 py-2 text-sm focus:border-blue-500 focus:outline-none"
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="My Coupons" />
<div className="mx-auto w-full max-w-2xl">
<div className="flex-1 p-4">
{/* Add Coupon Card */}
<div className="mb-6 rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
<p className="mb-3 text-lg font-semibold text-gray-800">Add a Coupon</p>
<input
type="text"
placeholder="Enter coupon code"
value={code}
onChange={(e) => setCode(e.target.value)}
className="mb-4 w-full rounded-lg border border-gray-200 px-4 py-2 text-sm focus:border-blue-500 focus:outline-none"
/>
<button
onClick={handleRedeem}
disabled={redeemMutation.isPending || code.trim().length < 4}
className={`w-full rounded-lg px-4 py-3 text-center font-semibold text-white ${
redeemMutation.isPending || code.trim().length < 4
? 'cursor-not-allowed bg-gray-400 opacity-50'
: 'bg-blue-500 hover:bg-blue-600'
}`}
>
{redeemMutation.isPending ? 'Adding...' : 'Add Coupon'}
</button>
</div>
{/* Coupon Sections */}
<CouponSection
title="Only for Me"
coupons={personalCoupons}
emptyMessage="No personal coupons available"
/>
<CouponSection
title="Apply to All"
coupons={generalCoupons}
emptyMessage="No general coupons available"
/>
<button
onClick={handleRedeem}
disabled={redeemMutation.isPending || code.trim().length < 4}
className={`w-full rounded-lg px-4 py-3 text-center font-semibold text-white ${
redeemMutation.isPending || code.trim().length < 4
? 'cursor-not-allowed bg-gray-400 opacity-50'
: 'bg-blue-500 hover:bg-blue-600'
}`}
>
{redeemMutation.isPending ? 'Adding...' : 'Add Coupon'}
</button>
</div>
{/* Coupon Sections */}
<CouponSection
title="Only for Me"
coupons={personalCoupons}
emptyMessage="No personal coupons available"
/>
<CouponSection
title="Apply to All"
coupons={generalCoupons}
emptyMessage="No general coupons available"
/>
</div>
</AppContainer>
</div>
)
}

View file

@ -1,10 +1,11 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { useAuth } from '../lib/auth-context'
import { trpc } from '../lib/trpc-client'
import { p, AppContainer, div } from 'web-components'
import { AlertCircle, LogOut, Trash2, User, Mail, Phone, X } from 'lucide-react'
import { Dialog } from '../components/Dialog'
import { MeScreenHeader } from '../components/me/MeScreenHeader'
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage'
import { ProfileImage, BottomDialog } from 'web-components'
import { AlertCircle, LogOut, Trash2, Camera, X, Loader2 } from 'lucide-react'
export const Route = createFileRoute('/me/edit-profile')({ component: EditProfilePage })
@ -13,8 +14,18 @@ function EditProfilePage() {
const { user, logout, loginWithToken } = useAuth()
const [name, setName] = useState(user?.name || '')
const [email, setEmail] = useState(user?.email || '')
const [mobile, setMobile] = useState(user?.mobile || '')
const [profileImageUri, setProfileImageUri] = useState<string | null>(user?.profileImage || null)
const [profileImageFile, setProfileImageFile] = useState<File | null>(null)
const [errors, setErrors] = useState<Record<string, string>>({})
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [showPasswordDialog, setShowPasswordDialog] = useState(false)
const [enteredMobile, setEnteredMobile] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const fileInputRef = useRef<HTMLInputElement>(null)
const { uploadSingle, isUploading } = useUploadToObjectStorage()
const updateMutation = trpc.user.auth.updateProfile.useMutation({
onSuccess: (data) => {
@ -25,16 +36,85 @@ function EditProfilePage() {
},
})
const updatePasswordMutation = trpc.user.auth.updatePassword.useMutation({
onSuccess: () => {
alert('Password updated successfully')
setShowPasswordDialog(false)
setPassword('')
setConfirmPassword('')
},
onError: (error: any) => {
alert(error.message || 'Failed to update password')
},
})
const deleteMutation = trpc.user.auth.deleteAccount.useMutation({
onSuccess: () => {
setShowDeleteModal(false)
logout()
},
onError: (error: any) => {
alert(error.message || 'Failed to delete account')
},
})
const handleSubmit = (e: React.FormEvent) => {
const validate = (): boolean => {
const next: Record<string, string> = {}
if (!name.trim()) next.name = 'Name is required'
else if (name.trim().length < 2) next.name = 'Name must be at least 2 characters'
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email.trim()) next.email = 'Email is required'
else if (!emailRegex.test(email.trim())) next.email = 'Please enter a valid email address'
const cleanMobile = mobile.replace(/\D/g, '')
if (!mobile.trim()) next.mobile = 'Mobile number is required'
else if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile))
next.mobile = 'Please enter a valid 10-digit mobile number'
setErrors(next)
return Object.keys(next).length === 0
}
const handleImageSelect = (file: File | undefined) => {
if (!file) return
if (!file.type.startsWith('image/')) return
setProfileImageFile(file)
setProfileImageUri(URL.createObjectURL(file))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
updateMutation.mutate({ name, email })
if (!validate()) return
try {
let profileImageUrl: string | undefined
if (profileImageFile) {
const { key } = await uploadSingle(profileImageFile, profileImageFile.type || 'image/jpeg', 'profile')
profileImageUrl = key
}
updateMutation.mutate({
name: name.trim(),
email: email.trim().toLowerCase(),
mobile: mobile.replace(/\D/g, ''),
...(profileImageUrl ? { profileImageUrl } : {}),
})
} catch (error: any) {
alert(error.message || 'Failed to upload profile image')
}
}
const handleUpdatePassword = () => {
if (password !== confirmPassword) {
alert('Passwords do not match')
return
}
if (password.length < 6) {
alert('Password must be at least 6 characters')
return
}
updatePasswordMutation.mutate({ password })
}
const handleDeleteAccount = () => {
@ -49,8 +129,14 @@ function EditProfilePage() {
deleteMutation.mutate({ mobile: enteredMobile.trim() })
}
const fieldClass = (hasError?: string) =>
`w-full rounded-lg border bg-gray-50 px-4 py-3 text-sm focus:outline-none ${
hasError ? 'border-red-500' : 'border-gray-200 focus:border-blue-500'
}`
return (
<AppContainer>
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="Edit Profile" />
<div className="flex min-h-full flex-col px-4 py-8">
{/* Header */}
<div className="mb-8 mt-4 text-center">
@ -60,62 +146,93 @@ function EditProfilePage() {
{/* Profile Form */}
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="mb-2 flex flex-col items-center">
<div className="relative">
<ProfileImage uri={profileImageUri} size={100} />
<button
type="button"
onClick={() => fileInputRef.current?.click()}
aria-label="Change profile picture"
className="absolute -bottom-1 -right-1 flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-brand-500 text-white shadow-sm transition-colors hover:bg-brand-600"
>
<Camera className="h-4 w-4" />
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleImageSelect(e.target.files?.[0])}
/>
</div>
</div>
{/* Name Field */}
<div className="rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
<label className="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700">
<User className="h-4 w-4" />
Full Name
</label>
<label className="mb-2 block text-sm font-medium text-gray-700">Full Name</label>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-lg border border-gray-200 px-4 py-3 text-sm focus:border-blue-500 focus:outline-none"
className={fieldClass(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
</div>
{/* Email Field */}
<div className="rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
<label className="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700">
<Mail className="h-4 w-4" />
Email Address
</label>
<label className="mb-2 block text-sm font-medium text-gray-700">Email Address</label>
<input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-lg border border-gray-200 px-4 py-3 text-sm focus:border-blue-500 focus:outline-none"
className={fieldClass(errors.email)}
/>
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email}</p>}
</div>
{/* Mobile Field (Disabled) */}
{/* Mobile Field */}
<div className="rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
<label className="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700">
<Phone className="h-4 w-4" />
Mobile Number
</label>
<label className="mb-2 block text-sm font-medium text-gray-700">Mobile Number</label>
<input
type="tel"
value={user?.mobile || ''}
disabled
className="w-full cursor-not-allowed rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-500"
value={mobile}
maxLength={10}
onChange={(e) => {
const clean = e.target.value.replace(/\D/g, '')
if (clean.length <= 10) setMobile(clean)
}}
className={fieldClass(errors.mobile)}
/>
<p className="mt-1 text-xs text-gray-400">Mobile number cannot be changed</p>
{errors.mobile && <p className="mt-1 text-sm text-red-500">{errors.mobile}</p>}
</div>
{/* Save Button */}
<button
type="submit"
disabled={updateMutation.isPending}
disabled={updateMutation.isPending || isUploading}
className={`mt-2 w-full rounded-lg px-4 py-3 font-semibold text-white transition-colors ${
updateMutation.isPending
updateMutation.isPending || isUploading
? 'cursor-not-allowed bg-gray-400'
: 'bg-blue-600 hover:bg-blue-700'
: 'bg-brand-600 hover:bg-brand-700'
}`}
>
{updateMutation.isPending ? 'Saving...' : 'Save Changes'}
{isUploading
? 'Uploading...'
: updateMutation.isPending
? 'Updating...'
: 'Update Profile'}
</button>
{/* Update Password */}
<button
type="button"
onClick={() => setShowPasswordDialog(true)}
className="w-full rounded-lg bg-brand-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-brand-700"
>
Update Password
</button>
</form>
@ -124,7 +241,7 @@ function EditProfilePage() {
{/* Logout Button */}
<button
onClick={logout}
className="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-3 font-medium text-red-600 transition-colors hover:bg-red-50"
className="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-200 bg-gray-100 px-4 py-3 font-medium text-red-600 transition-colors hover:bg-gray-200"
>
<LogOut className="h-4 w-4" />
Logout
@ -141,8 +258,50 @@ function EditProfilePage() {
</div>
</div>
{/* Update Password Dialog */}
<BottomDialog open={showPasswordDialog} onClose={() => setShowPasswordDialog(false)}>
<div className="p-4">
<p className="mb-4 text-lg font-bold text-gray-900">Update Password</p>
<input
type="password"
placeholder="New Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mb-3 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-blue-500 focus:outline-none"
/>
<input
type="password"
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="mb-6 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-blue-500 focus:outline-none"
/>
<div className="flex gap-3">
<button
onClick={() => setShowPasswordDialog(false)}
className="flex-1 rounded-lg border border-gray-200 bg-gray-100 px-4 py-3 font-medium text-gray-700 transition-colors hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={handleUpdatePassword}
disabled={updatePasswordMutation.isPending}
className={`flex flex-1 items-center justify-center rounded-lg bg-brand-600 px-4 py-3 font-medium text-white transition-colors hover:bg-brand-700 ${
updatePasswordMutation.isPending ? 'cursor-not-allowed opacity-70' : ''
}`}
>
{updatePasswordMutation.isPending ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
'Update'
)}
</button>
</div>
</div>
</BottomDialog>
{/* Delete Account Modal */}
<Dialog open={showDeleteModal} onClose={() => setShowDeleteModal(false)}>
<BottomDialog open={showDeleteModal} onClose={() => setShowDeleteModal(false)}>
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-red-600">
@ -186,17 +345,21 @@ function EditProfilePage() {
<button
onClick={confirmDeleteAccount}
disabled={deleteMutation.isPending || !enteredMobile.trim()}
className={`flex-1 rounded-lg px-4 py-3 font-medium text-white transition-colors ${
className={`flex flex-1 items-center justify-center rounded-lg px-4 py-3 font-medium text-white transition-colors ${
deleteMutation.isPending || !enteredMobile.trim()
? 'cursor-not-allowed bg-red-400'
: 'bg-red-600 hover:bg-red-700'
}`}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete Forever'}
{deleteMutation.isPending ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
'Delete Forever'
)}
</button>
</div>
</div>
</Dialog>
</AppContainer>
</BottomDialog>
</div>
)
}

View file

@ -2,7 +2,7 @@ import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { trpc } from '../lib/trpc-client'
import { useState, useEffect } from 'react'
import { ChevronLeft, CreditCard, CheckCircle, Zap, Edit2, Tag, XCircle, X, AlertCircle, Loader2 } from 'lucide-react'
import { Dialog } from '../components/Dialog'
import { BottomDialog } from 'web-components'
import ComplaintForm from '../components/ComplaintForm'
import { orderStatusManipulator } from '../lib/string-manipulators'
import dayjs from 'dayjs'
@ -164,7 +164,7 @@ function OrderDetailPage() {
<div className="flex-1 overflow-y-auto p-4 pb-12">
{/* 1 Hr Delivery Banner */}
{order.isFlashDelivery && (
<div className="mb-4 rounded-2xl border border-amber-200 bg-linear-to-r from-amber-50 to-yellow-50 p-4">
<div className="mb-4 rounded-2xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4">
<div className="flex flex-row items-center">
<Zap className="h-6 w-6 text-amber-600" />
<div className="ml-3 flex-1">
@ -391,7 +391,7 @@ function OrderDetailPage() {
</div>
{/* Cancel Order Dialog */}
<Dialog open={cancelDialogOpen} onClose={() => setCancelDialogOpen(false)}>
<BottomDialog open={cancelDialogOpen} onClose={() => setCancelDialogOpen(false)}>
<div className="p-6">
<div className="mb-4 flex flex-row items-center justify-between">
<p className="text-xl font-bold text-gray-900">Cancel Order</p>
@ -429,10 +429,10 @@ function OrderDetailPage() {
)}
</button>
</div>
</Dialog>
</BottomDialog>
{/* Raise Complaint Dialog */}
<Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
<BottomDialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
<ComplaintForm
open={complaintDialogOpen}
onClose={() => {
@ -441,7 +441,7 @@ function OrderDetailPage() {
}}
orderId={order.id}
/>
</Dialog>
</BottomDialog>
</div>
)
}

View file

@ -1,80 +1,456 @@
import { createFileRoute, useNavigate, Outlet, useLocation } from '@tanstack/react-router'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { trpc } from '../lib/trpc-client'
import { p, AppContainer, div } from 'web-components'
import { Package, ChevronRight } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { MeOrderMenu } from '../components/me/MeOrderMenu'
import { MeScreenHeader } from '../components/me/MeScreenHeader'
import { orderStatusManipulator } from '../lib/string-manipulators'
import dayjs from 'dayjs'
import { AlertCircle, Package } from 'lucide-react'
// Matches packages/ui (RN common-ui) REFUND_STATUS values used by user-ui.
const REFUND_STATUS = {
PENDING: 'pending',
NOT_APPLICABLE: 'na',
PROCESSING: 'initiated',
SUCCESS: 'success',
} as const
export const Route = createFileRoute('/me/orders')({ component: OrdersPage })
function OrdersPage() {
const navigate = useNavigate()
const location = useLocation()
const { data } = trpc.user.order.getOrders.useQuery({ page: 1 })
const orders = data?.data || []
const PAGE_SIZE = 10
// Check if we're on the exact /me/orders path (not a child route like /me/orders/123)
const isExactOrdersPath = location.pathname === '/me/orders'
type StatusStyle = {
bg: string
text: string
iconColor: string
icon: React.ReactNode
label: string
}
function getStatusStyle(status: string): StatusStyle {
const s = status.toLowerCase()
switch (s) {
case 'delivered':
case 'success':
case 'completed':
return { bg: 'bg-emerald-50', text: 'text-emerald-700', iconColor: '#059669', icon: <span></span>, label: 'Delivered' }
case 'cancelled':
case 'failed':
return { bg: 'bg-rose-50', text: 'text-rose-700', iconColor: '#E11D48', icon: <span></span>, label: 'Cancelled' }
case 'pending':
case 'payment_pending':
return { bg: 'bg-amber-50', text: 'text-amber-700', iconColor: '#D97706', icon: <span>🕐</span>, label: 'Pending' }
case 'packaged':
return { bg: 'bg-brand-50', text: 'text-brand-700', iconColor: '#1570EF', icon: <span>📦</span>, label: 'Packaged' }
case 'processing':
case 'confirmed':
case 'shipped':
return { bg: 'bg-brand-50', text: 'text-brand-700', iconColor: '#1570EF', icon: <span>🚚</span>, label: s.charAt(0).toUpperCase() + s.slice(1) }
default:
return { bg: 'bg-slate-50', text: 'text-slate-700', iconColor: '#64748B', icon: <span>i</span>, label: status }
}
}
function getRefundStyle(status: string): { bg: string; text: string; icon: React.ReactNode; color: string } {
switch (status) {
case REFUND_STATUS.SUCCESS:
return { bg: 'bg-emerald-50', text: 'text-emerald-700', icon: <span></span>, color: '#059669' }
case REFUND_STATUS.PROCESSING:
return { bg: 'bg-brand-50', text: 'text-brand-700', icon: <span></span>, color: '#1570EF' }
case REFUND_STATUS.PENDING:
return { bg: 'bg-amber-50', text: 'text-amber-700', icon: <span>🕐</span>, color: '#D97706' }
default:
return { bg: 'bg-slate-50', text: 'text-slate-700', icon: <span>i</span>, color: '#64748B' }
}
}
function OrderCard({
item,
getStatusStyle,
getRefundStyle,
onPress,
onViewMoreItems,
refetch,
}: {
item: any
getStatusStyle: (s: string) => StatusStyle
getRefundStyle: (s: string) => { bg: string; text: string; icon: React.ReactNode; color: string }
onPress: (orderId: number) => void
onViewMoreItems: (orderId: number) => void
refetch: () => void
}) {
const mainStatus = getStatusStyle(item.orderStatus)
const deliveryStatus = getStatusStyle(item.deliveryStatus)
const totalAmount = item.totalAmount
return (
<AppContainer>
{isExactOrdersPath ? (
<>
<p className="font-bold mb-4 text-xl">
My Orders
</p>
<button
onClick={() => onPress(item.id)}
className="mb-6 w-full overflow-hidden rounded-[28px] border border-slate-100 bg-white text-left shadow-sm transition-colors hover:bg-slate-50/50"
>
{/* Top Header: Order ID and Status */}
<div className="flex flex-row items-center justify-between border-b border-slate-100 bg-slate-50/50 px-5 py-4">
<div>
<div className="flex flex-row items-center">
<span className="mr-2 h-2 w-2 rounded-full bg-brand-500" />
<p className="text-[10px] font-bold uppercase tracking-tighter text-slate-400">
Order Reference
</p>
</div>
<p className="mt-0.5 text-base font-extrabold text-slate-900">#{item.orderId}</p>
</div>
{orders.length === 0 ? (
<div className="flex flex-col items-center gap-4 py-20">
<Package className="h-12 w-12 text-gray-300" />
<p className="text-gray-500">No orders yet</p>
</div>
) : (
<div className="flex flex-col gap-3">
{orders.map((order: any) => (
<div
key={order.id}
onClick={() =>
navigate({ to: '/me/orders/$id', params: { id: String(order.id) } })
}
className="rounded-xl border border-gray-100 bg-white p-4 shadow-sm"
>
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-sm">
Order #{order.id}
</p>
<p className="text-xs text-gray-500">
{order.createdAt
? new Date(order.createdAt).toLocaleDateString()
: ''}
</p>
</div>
<div className="flex items-center gap-2">
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
order.status === 'delivered'
? 'bg-green-100 text-green-700'
: order.status === 'cancelled'
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
}`}
>
{order.status}
</span>
<ChevronRight className="h-4 w-4 text-gray-400" />
</div>
</div>
<p className="mt-1 text-xs text-gray-400">
Total: {order.totalAmount || 0}
</p>
</div>
))}
</div>
<div className="flex flex-row items-center gap-2">
{item.orderStatus.toLowerCase() === 'cancelled' && (
<span className={`flex flex-row items-center rounded-full border px-3 py-1.5 ${mainStatus.bg} border-rose-100`}>
<span style={{ color: mainStatus.iconColor }} className="mr-1.5 text-[11px]">
{mainStatus.icon}
</span>
<span className={`text-[11px] font-bold uppercase tracking-wide ${mainStatus.text}`}>
{orderStatusManipulator(mainStatus.label)}
</span>
</span>
)}
</>
) : (
/* Render child routes (order detail) when not on exact /me/orders path */
<Outlet />
)}
</AppContainer>
{item.isFlashDelivery && (
<span className="flex flex-row items-center rounded-full border border-amber-200 bg-amber-100 px-2 py-1">
<span className="mr-1 text-[10px]"></span>
<span className="text-[10px] font-black uppercase text-amber-700">Flash</span>
</span>
)}
</div>
</div>
<div className="p-5">
{/* Order Date & Quick Stats */}
<div className="mb-5 flex flex-row items-start justify-between">
<div className="flex flex-1 flex-col gap-3">
{(item.deliveryDate || item.isFlashDelivery) && (
<div className="flex flex-row items-center">
<div
className={`mr-3 flex h-9 w-9 items-center justify-center rounded-xl ${
item.isFlashDelivery ? 'bg-amber-50' : 'bg-brand-50'
}`}
>
<span className="text-[16px]">{item.isFlashDelivery ? '⚡' : '🕐'}</span>
</div>
<div>
<p
className={`text-[10px] font-bold uppercase ${
item.isFlashDelivery ? 'text-amber-700' : 'text-brand-700'
}`}
>
{item.isFlashDelivery ? '1 Hr Delivery' : 'Delivery Time'}
</p>
<p
className={`text-sm font-extrabold ${
item.isFlashDelivery ? 'text-amber-900' : 'text-brand-900'
}`}
>
{item.isFlashDelivery
? dayjs(item.createdAt || item.orderDate).add(30, 'minutes').format('DD MMM, hh:mm A')
: (() => {
const startTime = dayjs(item.deliveryDate)
const endTime = startTime.add(1, 'hour')
return `${startTime.format('DD MMM, hh:mm A')} - ${endTime.format('hh:mm A')}`
})()}
</p>
{item.isFlashDelivery && (
<p className="mt-1 text-[9px] font-bold uppercase text-amber-600"> 30-Min Delivery</p>
)}
</div>
</div>
)}
<div className="flex flex-row items-center">
<div className="mr-3 flex h-9 w-9 items-center justify-center rounded-xl bg-slate-100">
<span className="text-[16px]">📅</span>
</div>
<div>
<p className="text-[10px] font-bold uppercase text-slate-400">Placed On</p>
<p className="text-sm font-semibold text-slate-800">
{dayjs(item.orderDate).format('DD MMM YYYY, hh:mm A')}
</p>
</div>
</div>
</div>
<MeOrderMenu orderId={item.id} postActionHandler={refetch} />
</div>
{/* Items Section */}
<div className="mb-5 rounded-2xl border border-slate-100 bg-slate-50/50 p-3">
<div className="mb-3 flex flex-row items-center justify-between px-1">
<p className="text-[11px] font-bold uppercase tracking-wider text-slate-500">Items Summary</p>
<p className="text-[11px] font-bold text-brand-600">
{item.items.length} {item.items.length === 1 ? 'Item' : 'Items'}
</p>
</div>
{item.items.slice(0, 2).map((product: any, pIndex: number) => (
<div key={pIndex} className="mb-3 flex flex-row items-center last:mb-0">
<div className="relative">
<img
src={product.image || undefined}
alt=""
className="h-12 w-12 rounded-xl border border-slate-200 bg-white object-cover"
/>
<div className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full border-2 border-white bg-brand-500">
<p className="text-[10px] font-bold text-white">{product.quantity}</p>
</div>
</div>
<div className="ml-4 min-w-0 flex-1">
<p className="truncate text-sm font-bold text-slate-800">{product.productName}</p>
<p className="mt-0.5 text-xs font-medium text-slate-500">
Unit Price: {product.price}
</p>
</div>
<p className="ml-2 text-sm font-extrabold text-slate-900">{product.amount}</p>
</div>
))}
{item.items.length > 2 && (
<button
onClick={(e) => {
e.stopPropagation()
onViewMoreItems(item.id)
}}
className="mt-2 w-full border-t border-slate-100 pt-2 text-center"
>
<p className="text-xs font-bold text-brand-600">
+ {item.items.length - 2} more {item.items.length - 2 === 1 ? 'item' : 'items'} in this order
</p>
</button>
)}
</div>
{/* Delivery Status - Single Line */}
<div className="mb-5 flex flex-row items-center justify-between">
<div className="flex min-w-0 flex-1 flex-row items-center">
<span
className="mr-2 rounded-lg p-1.5"
style={{ backgroundColor: deliveryStatus.iconColor + '15' }}
>
<span className="text-[14px]">{item.isFlashDelivery ? '⚡' : '🚚'}</span>
</span>
<p className="mr-2 shrink-0 text-[10px] font-bold uppercase text-slate-400">
{item.isFlashDelivery ? '1 Hr Delivery:' : 'Shipping Status:'}
</p>
<p className={`truncate text-xs font-bold ${deliveryStatus.text}`}>
{orderStatusManipulator(item.deliveryStatus)}
</p>
</div>
{item.isFlashDelivery && (
<span className="ml-2 shrink-0 rounded-full border border-amber-200 bg-amber-100 px-2 py-0.5">
<p className="text-[8px] font-black uppercase text-amber-700"> Fast</p>
</span>
)}
</div>
{/* User Notes */}
{item.userNotes && (
<div className="mb-5">
<div className="flex flex-row items-start rounded-2xl border border-amber-100 bg-amber-50/50 p-3">
<span className="mt-0.5 text-[16px]">📝</span>
<div className="ml-3 flex-1">
<p className="text-[10px] font-bold uppercase text-amber-700">Your Instructions</p>
<p className="mt-0.5 line-clamp-2 text-xs font-medium text-amber-900">
{item.userNotes}
</p>
</div>
</div>
</div>
)}
{/* Cancellation Info */}
{item.cancelReason && (
<div className="mb-5 rounded-2xl border border-rose-100 bg-rose-50 p-4">
<div className="mb-2 flex flex-row items-center">
<span className="text-[16px]"></span>
<p className="ml-2 text-xs font-bold text-rose-800">Cancellation Details</p>
</div>
<p className="text-xs font-medium leading-relaxed text-rose-600">{item.cancelReason}</p>
{item.refundStatus && item.refundStatus !== REFUND_STATUS.NOT_APPLICABLE && (
<div className="mt-3 flex flex-row items-center justify-between border-t border-rose-100 pt-3">
<p className="text-[10px] font-bold uppercase text-rose-700">Refund Status</p>
<span className={`flex flex-row items-center rounded-lg border border-rose-200 bg-white px-2 py-1 ${getRefundStyle(item.refundStatus).bg}`}>
<span className="mr-1.5 text-[12px]" style={{ color: getRefundStyle(item.refundStatus).color }}>
{getRefundStyle(item.refundStatus).icon}
</span>
<span
className={`text-[10px] font-bold uppercase ${getRefundStyle(item.refundStatus).text}`}
>
{item.refundStatus}
</span>
</span>
</div>
)}
</div>
)}
{/* Footer: Price and CTA */}
<div className="flex flex-row items-center justify-between border-t border-slate-100 pt-5">
<div>
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Amount to Pay</p>
<p className="text-2xl font-black text-slate-900">{totalAmount}</p>
{item.discountAmount ? (
<div className="mt-1 flex flex-row items-center self-start rounded-full bg-emerald-50 px-2 py-0.5">
<span className="mr-1 text-[10px]">🏷</span>
<p className="text-[10px] font-bold text-emerald-700">Saved {item.discountAmount}</p>
</div>
) : null}
{item.deliveryCharge > 0 && (
<div className="mt-1 flex flex-row items-center">
<span className="mr-1 text-[12px]">🚚</span>
<p className="text-[10px] font-medium text-slate-600">Delivery: {item.deliveryCharge}</p>
</div>
)}
</div>
<div className="flex flex-row items-center rounded-2xl bg-brand-600 px-5 py-3 shadow-lg">
<p className="mr-1 text-sm font-bold text-white">View Details</p>
<span className="text-[16px] text-white"></span>
</div>
</div>
</div>
</button>
)
}
function OrdersPage() {
const navigate = useNavigate()
// Infinite scroll state
const [allOrders, setAllOrders] = useState<any[]>([])
const [currentPage, setCurrentPage] = useState<number>(1)
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false)
const [hasNextPage, setHasNextPage] = useState<boolean>(true)
const [loadMoreError, setLoadMoreError] = useState<string | null>(null)
const { data: ordersData, isLoading, error, refetch } = trpc.user.order.getOrders.useQuery({
page: currentPage,
pageSize: PAGE_SIZE,
})
// Handle data accumulation for infinite scroll
useEffect(() => {
if (ordersData?.data) {
if (currentPage === 1) {
setAllOrders(ordersData.data)
} else {
setAllOrders((prev) => [...prev, ...ordersData.data])
}
const totalPages = ordersData.pagination?.totalPages || 1
setHasNextPage(currentPage < totalPages)
setIsLoadingMore(false)
setLoadMoreError(null)
}
}, [ordersData, currentPage])
// Handle errors during infinite scroll loading
useEffect(() => {
if (error && currentPage > 1) {
setIsLoadingMore(false)
setLoadMoreError('Failed to load more orders. Please try again.')
}
}, [error, currentPage])
const loadMoreOrders = useCallback(() => {
if (!isLoadingMore && hasNextPage && !isLoading) {
setIsLoadingMore(true)
setCurrentPage((prev) => prev + 1)
}
}, [isLoadingMore, hasNextPage, isLoading])
return (
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="My Orders" />
{isLoading && currentPage === 1 && (
<div className="flex min-h-[60vh] flex-1 items-center justify-center bg-gray-50">
<p className="font-medium text-gray-500">Loading your orders...</p>
</div>
)}
{error && currentPage === 1 && !isLoading && (
<div className="flex min-h-[60vh] flex-1 flex-col items-center justify-center bg-gray-50 p-6">
<div className="mb-4 rounded-full bg-white p-6 shadow-sm">
<AlertCircle className="h-12 w-12 text-red-500" />
</div>
<p className="mt-2 text-xl font-bold text-gray-900">Unable to load orders</p>
<p className="mt-2 mb-6 text-center text-gray-500">Please check your connection and try again</p>
<button
onClick={() => refetch()}
className="rounded-full bg-blue-600 px-8 py-3 font-bold text-white shadow-md transition-colors hover:bg-blue-700"
>
Retry
</button>
</div>
)}
{!isLoading && !error && (
<div className="mx-auto w-full max-w-2xl px-4 py-6">
{allOrders.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20">
<div className="mb-4 rounded-full bg-white p-6 shadow-sm">
<Package className="h-16 w-16 text-gray-200" />
</div>
<p className="mt-2 text-lg font-bold text-gray-900">No orders yet</p>
<p className="mt-2 text-center text-gray-500">Your order history will appear here</p>
<button
onClick={() => navigate({ to: '/home' })}
className="mt-6 rounded-full bg-blue-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-blue-700"
>
Start Shopping
</button>
</div>
) : (
<>
{allOrders.map((order) => (
<OrderCard
key={order.id}
item={order}
getStatusStyle={getStatusStyle}
getRefundStyle={getRefundStyle}
onPress={(orderId) => navigate({ to: '/me/orders/$id', params: { id: String(orderId) } })}
onViewMoreItems={(orderId) =>
navigate({ to: '/me/orders/$id', params: { id: String(orderId) } })
}
refetch={refetch}
/>
))}
{/* Footer / infinite scroll sentinel */}
<div className="flex flex-col items-center py-4">
{isLoadingMore && <p className="text-sm text-gray-500">Loading more orders...</p>}
{loadMoreError && (
<div className="flex flex-col items-center gap-2">
<p className="mb-2 text-sm text-red-500">{loadMoreError}</p>
<button
onClick={() => {
setLoadMoreError(null)
loadMoreOrders()
}}
className="rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white"
>
Retry
</button>
</div>
)}
{!hasNextPage && allOrders.length > 0 && (
<p className="text-xs text-gray-400">End of list</p>
)}
{hasNextPage && !isLoadingMore && !loadMoreError && (
<button onClick={loadMoreOrders} className="rounded-lg bg-gray-200 px-5 py-2 text-sm font-medium text-gray-700">
Load more
</button>
)}
</div>
</>
)}
</div>
)}
</div>
)
}

View file

@ -1,57 +1,183 @@
import { createFileRoute } from '@tanstack/react-router'
import { p, AppContainer } from 'web-components'
import { MeScreenHeader } from '../components/me/MeScreenHeader'
export const Route = createFileRoute('/me/terms')({ component: TermsPage })
// Verbatim content ported from apps/user-ui/components/TermsAndConditions.tsx
// (source of truth for the me/terms page).
function TermsPage() {
return (
<AppContainer>
<p className="font-bold mb-6 text-2xl">
Terms & Conditions
</p>
<div className="min-h-screen bg-gray-50">
<MeScreenHeader title="Terms & Conditions" />
<div className="mx-auto w-full max-w-2xl p-4 pb-8">
<div className="rounded-2xl bg-white p-5 text-sm leading-relaxed text-gray-600 shadow-sm">
{/* Header */}
<p className="text-base font-bold text-gray-900">FRESHYO Legal Policies</p>
<p className="mb-4 text-xs text-gray-500">Last Updated: [Add Date]</p>
<div className="prose prose-sm max-w-none text-gray-600">
<p className="font-semibold mb-2 mt-4 text-gray-900">
1. Acceptance of Terms
</p>
<p className="mb-4">
By using Freshyo, you agree to these terms. If you do not agree, please
do not use our service.
</p>
<p className="mb-4">
By accessing or using the FRESHYO mobile application ("App"), you agree to the following
policies. If you do not agree, please do not use the App.
</p>
<p className="font-semibold mb-2 mt-4 text-gray-900">
2. Orders and Payments
</p>
<p className="mb-4">
All orders are subject to availability. We reserve the right to cancel
any order. Payments are collected at the time of delivery (COD).
</p>
{/* A. TERMS & CONDITIONS */}
<p className="mb-2 text-base font-bold text-gray-900">A. TERMS &amp; CONDITIONS</p>
<p className="font-semibold mb-2 mt-4 text-gray-900">
3. Delivery Policy
</p>
<p className="mb-4">
Delivery times are estimates. We strive to deliver within the promised
time window but delays may occur due to unforeseen circumstances.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">1. About FRESHYO</p>
<p className="mb-4">
FRESHYO is a grocery and fresh meat delivery application serving customers only within
Mahabubnagar Town, Telangana, India.
</p>
<p className="font-semibold mb-2 mt-4 text-gray-900">
4. Returns and Refunds
</p>
<p className="mb-4">
If you are not satisfied with the quality of your order, please contact
us within 24 hours of delivery. Refunds will be processed after quality
assessment.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">2. Eligibility</p>
<p className="mb-4">
Users must be 18 years or older to place orders. By registering, you confirm the accuracy
of the information provided.
</p>
<p className="font-semibold mb-2 mt-4 text-gray-900">
5. Privacy
</p>
<p className="mb-4">
We respect your privacy. Your personal information is used only for
order processing and delivery purposes.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">3. Service Area</p>
<p className="mb-4">
Services are limited strictly to Mahabubnagar Town. Orders outside the service area may be
cancelled without notice.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">4. Account Registration</p>
<p className="mb-4">
Registration requires a valid mobile number and OTP verification. Users are responsible for
all activity under their account. FRESHYO reserves the right to suspend or terminate
accounts for misuse.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">5. Orders</p>
<p className="mb-4">
Placing an order constitutes an offer to purchase. FRESHYO may accept or reject orders
based on availability, pricing errors, or operational reasons.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">6. Pricing &amp; Payment</p>
<p className="mb-4">
Prices are displayed in INR and include applicable taxes unless stated otherwise. Only Cash
on Delivery (COD) is accepted. Full payment must be made at the time of delivery. Failure to
pay may result in order cancellation and account restrictions.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">7. Delivery</p>
<p className="mb-4">
Delivery timelines are estimates and may vary. Users must ensure availability at the
delivery address. Incorrect address or unavailability may lead to cancellation.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">8. Fresh Meat &amp; Perishables</p>
<p className="mb-4">
Fresh meat and perishable items are non-returnable and non-refundable except for: wrong item
delivered, spoiled or damaged item at delivery. Issues must be reported within 2 hours with
photos.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">9. User Conduct</p>
<p className="mb-4">
Users must not: provide false information, abuse staff, misuse the App, or engage in illegal
activity. Violations may result in permanent account termination.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">10. Intellectual Property</p>
<p className="mb-4">All content, branding, and app design belong exclusively to FRESHYO.</p>
<p className="mb-1 text-sm font-semibold text-gray-800">11. Limitation of Liability</p>
<p className="mb-4">
Liability is limited to the value of the order placed. Product images are indicative only.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">12. Force Majeure</p>
<p className="mb-4">
FRESHYO is not liable for delays caused by events beyond reasonable control.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">13. Governing Law</p>
<p className="mb-4">
Governed by Indian law. Jurisdiction: Courts of Telangana.
</p>
{/* B. PRIVACY POLICY */}
<p className="mb-2 text-base font-bold text-gray-900">B. PRIVACY POLICY</p>
<p className="mb-4">FRESHYO respects your privacy and is committed to protecting your data.</p>
<p className="mb-1 text-sm font-semibold text-gray-800">1. Information We Collect</p>
<p className="mb-4">
Name, mobile number, delivery address, order history, app usage data (basic analytics).
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">2. How We Use Information</p>
<p className="mb-4">
To process and deliver orders, to communicate order updates, to improve app experience, for
customer support.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">3. Data Sharing</p>
<p className="mb-4">
Data is shared only with delivery partners for order fulfillment. We do not sell or rent
personal data to third parties.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">4. Data Security</p>
<p className="mb-4">
We use reasonable security measures to protect your data. However, no system is 100%
secure.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">5. Data Retention</p>
<p className="mb-4">
Data is retained only as long as necessary for business and legal purposes.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">6. User Rights</p>
<p className="mb-4">
Users may request: data correction, account deletion (subject to legal and operational
requirements).
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">7. Children's Privacy</p>
<p className="mb-4">FRESHYO does not knowingly collect data from users under 18.</p>
{/* C. REFUND & CANCELLATION POLICY */}
<p className="mb-2 text-base font-bold text-gray-900">C. REFUND &amp; CANCELLATION POLICY</p>
<p className="mb-1 text-sm font-semibold text-gray-800">1. Order Cancellation</p>
<p className="mb-4">
Orders can be cancelled before dispatch. Once dispatched, cancellation may not be allowed,
especially for perishables.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">2. Refund Policy</p>
<p className="mb-4">
Since payments are Cash on Delivery, refunds (if applicable) will be handled via: cash
adjustment, replacement, or store credit (at FRESHYO's discretion).
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">3. Non-Refundable Items</p>
<p className="mb-4">
Fresh meat and perishable groceries except in cases of damage, spoilage, or wrong delivery.
</p>
<p className="mb-1 text-sm font-semibold text-gray-800">4. Reporting Issues</p>
<p className="mb-4">
Issues must be reported within 2 hours of delivery. Supporting photos are mandatory.
</p>
{/* D. CONTACT INFORMATION */}
<p className="mb-2 text-base font-bold text-gray-900">D. CONTACT INFORMATION</p>
<p className="mb-1">📧 Email: support@freshyo.in</p>
<p className="mb-4">📞 Customer Support: 8688182552</p>
{/* E. ACCEPTANCE */}
<p className="mb-2 text-base font-bold text-gray-900">E. ACCEPTANCE</p>
<p className="mb-4">
By registering or placing an order, you confirm that you have read, understood, and agreed
to all policies above.
</p>
</div>
</div>
</AppContainer>
</div>
)
}

View file

@ -1,22 +1,48 @@
import { createFileRoute, useNavigate, Outlet, useLocation } from '@tanstack/react-router'
import { useAuth } from '../lib/auth-context'
import { MyButton, ProfileImage } from 'web-components'
import { MyButton } from 'web-components'
import { AppLayout } from '../components/AppLayout'
import { NextOrderGlimpse } from '../components/me/NextOrderGlimpse'
import {
Package,
MapPin,
ShoppingCart,
Ticket,
MessageSquare,
MapPin,
User,
Info,
MessageSquare,
FileText,
LogOut,
ShoppingCart,
ChevronRight,
Settings,
} from 'lucide-react'
import React from 'react'
export const Route = createFileRoute('/me')({ component: MePage })
interface MenuItem {
title: string
icon: React.ComponentType<{ size?: number | string; color?: string }>
color: string
to: string
subtitle: string
}
interface MenuSection {
title: string
items: MenuItem[]
}
function MenuIcon({ icon: Icon, color }: { icon: MenuItem['icon']; color: string }) {
return (
<div
className="mr-4 flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl"
style={{ backgroundColor: `${color}15` }}
>
<Icon size={24} color={color} />
</div>
)
}
function MePage() {
const navigate = useNavigate()
const location = useLocation()
@ -40,33 +66,69 @@ function MePage() {
)
}
const menuItems = [
const menuSections: MenuSection[] = [
{
section: 'Shopping & Activity',
title: 'Shopping & Activity',
items: [
{ icon: Package, label: 'My Orders', to: '/me/orders' },
{ icon: ShoppingCart, label: 'My Cart', to: '/cart' },
{ icon: Ticket, label: 'Coupons', to: '/me/coupons' },
{
title: 'My Orders',
icon: Package,
color: '#1570EF',
to: '/me/orders',
subtitle: 'Orders history & tracking',
},
{
title: 'My Cart',
icon: ShoppingCart,
color: '#10B981',
to: '/cart',
subtitle: 'Items ready for checkout',
},
{
title: 'Coupons',
icon: Ticket,
color: '#8B5CF6',
to: '/me/coupons',
subtitle: 'View active offers & rewards',
},
],
},
{
section: 'Saved Information',
title: 'Saved Information',
items: [
{ icon: MapPin, label: 'Addresses', to: '/me/addresses' },
{ icon: User, label: 'Profile Settings', to: '/me/edit-profile' },
{
title: 'Addresses',
icon: MapPin,
color: '#F59E0B',
to: '/me/addresses',
subtitle: 'Manage delivery locations',
},
{
title: 'Profile Settings',
icon: User,
color: '#3B82F6',
to: '/me/edit-profile',
subtitle: 'Update your personal details',
},
],
},
{
section: 'Support',
title: 'Support',
items: [
{ icon: MessageSquare, label: 'Help & Complaints', to: '/me/complaints' },
{ icon: FileText, label: 'Terms & Conditions', to: '/me/terms' },
],
},
{
section: 'About',
items: [
{ icon: Info, label: 'About Us', to: '/me/about' },
{
title: 'Help & Complaints',
icon: MessageSquare,
color: '#EF4444',
to: '/me/complaints',
subtitle: 'Talk to our customer care',
},
{
title: 'Terms & Conditions',
icon: FileText,
color: '#6B7280',
to: '/me/terms',
subtitle: 'Read our legal policies',
},
],
},
]
@ -74,60 +136,66 @@ function MePage() {
return (
<AppLayout>
{isExactMePath ? (
<div className="mx-auto w-full max-w-5xl px-4 py-6 pb-24 md:px-8 md:pb-12">
{/* Profile Header */}
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Your Account
</p>
<h1 className="display-2 mt-1 text-gray-900">My Freshyo</h1>
</div>
<div className="mb-8 flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-6">
<ProfileImage uri={user.profileImage} size={64} />
<div>
<p className="text-lg font-extrabold text-gray-900">
{user.name || 'User'}
</p>
<p className="text-sm text-gray-500">{user.mobile}</p>
</div>
</div>
{/* Menu */}
<div className="grid gap-6 md:grid-cols-2">
{menuItems.map((section) => (
<div key={section.section}>
<p className="mb-2 text-[11px] font-black uppercase tracking-[0.18em] text-gray-400">
{section.section}
</p>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white">
{section.items.map((item) => (
<button
key={item.label}
onClick={() => navigate({ to: item.to as any })}
className="flex w-full items-center gap-3 border-b border-gray-100 px-5 py-4 text-left transition-colors last:border-b-0 hover:bg-gray-50"
>
<item.icon className="h-5 w-5 text-brand-600" />
<p className="flex-1 text-sm font-semibold text-gray-800">{item.label}</p>
<ChevronRight className="h-4 w-4 text-gray-300" />
</button>
))}
</div>
<div className="min-h-screen bg-gray-50">
<div className="mx-auto w-full max-w-2xl">
<div className="flex flex-row items-center justify-between px-6 pb-10 pt-8">
<div>
<p className="text-3xl font-extrabold tracking-tight text-gray-900">My Account</p>
<p className="mt-1 text-sm text-gray-400">Manage your freshyo experience</p>
</div>
))}
</div>
<button
onClick={() => navigate({ to: '/me/edit-profile' })}
aria-label="Settings"
className="rounded-2xl bg-gray-100 p-3 transition-colors hover:bg-gray-200"
>
<Settings size={22} color="#1F2937" />
</button>
</div>
{/* Logout */}
<div className="mt-8 flex flex-col items-center gap-4">
<MyButton
onClick={logout}
variant="red"
className="bg-red-600 text-white hover:bg-red-700"
textContent="Logout"
/>
<p className="text-xs text-gray-400">
Version 1.0.0
</p>
<NextOrderGlimpse />
<div className="px-6">
{menuSections.map((section, sIndex) => (
<div key={sIndex} className="mb-8">
<p className="mb-4 px-2 text-sm font-bold uppercase tracking-[2px] text-gray-400">
{section.title}
</p>
<div className="overflow-hidden rounded-[32px] border border-gray-100 bg-white shadow-sm">
{section.items.map((item, iIndex) => (
<button
key={iIndex}
onClick={() => navigate({ to: item.to as any })}
className={`flex w-full flex-row items-center p-4 text-left transition-colors hover:bg-gray-50 ${
iIndex !== section.items.length - 1 ? 'border-b border-gray-50' : ''
}`}
>
<MenuIcon icon={item.icon} color={item.color} />
<div className="min-w-0 flex-1">
<p className="text-base font-semibold text-gray-800">{item.title}</p>
<p className="mt-0.5 text-xs text-gray-400">{item.subtitle}</p>
</div>
<div className="ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-50">
<ChevronRight size={16} color="#94A3B8" />
</div>
</button>
))}
</div>
</div>
))}
{/* Logout row */}
<button
onClick={logout}
className="mt-2 flex w-full flex-row items-center justify-center rounded-[24px] border border-red-50 bg-white p-5 shadow-sm transition-colors hover:bg-red-50"
>
<LogOut size={22} color="#F43F5E" className="mr-2" />
<p className="text-base font-bold text-rose-500">Log Out from Freshyo</p>
</button>
<p className="mt-12 text-center text-[10px] uppercase tracking-[3px] text-gray-300">
Version 1.2.0 Freshyo App
</p>
</div>
</div>
</div>
) : (

View file

@ -1466,3 +1466,20 @@ Planned edits: package.json name web-ui->admin-web + add sonner, @dnd-kit/core,
[2026-09-06 01:00:00] backend admin-apis image-URL pattern: replicate user-ui scaffoldAssetUrl in admin-apis/product.ts (9 sites) + admin-apis/complaint.ts (1 site), replacing generateSignedUrlsFromS3Urls. Collapse redundant await/Promise.all wrappers, drop unused import, rename signed* locals. Response field names unchanged (no frontend impact). Write/upload paths untouched. No migration.
[2026-09-06 01:05:00] VERIFIED admin image-URL unification: backend tsc 0 errors (= baseline); only remaining generateSignedUrlsFromS3Urls refs in admin-apis are inside commented-out blocks (product.ts old getProductById + old reviews query), left untouched as history. Live admin product/complaint image fields now return permanent CDN scaffoldAssetUrl URLs like user-apis; response field names unchanged.
[2026-09-06 09:15:00] WEB-UI ME SECTION — EXACT REPLICA of user-ui me screens. Ported RN layouts/cards/dialogs/colors; added mobile-only stack headers to /me subpages (desktop keeps global shell); removed invented "About" menu entry from hub.
NEW FILES:
- apps/web-ui/src/components/me/MeScreenHeader.tsx — mobile-only (md:hidden) back-chevron + title bar replicating RN stack headers.
- apps/web-ui/src/components/me/NextOrderGlimpse.tsx — port of RN amber "Upcoming Order" horizontal snap carousel (filters active orders, flash-first sort, pagination dots, tel: support).
- apps/web-ui/src/components/me/MeOrderMenu.tsx — port of RN OrderMenu 3-dot action sheet (View Details / Edit Notes / Raise Complaint / Cancel Order) using web BottomDialog.
REWRITES (route → RN parity):
- me.tsx — hub: gray-50 bg; "My Account" 3xl header + subtitle + gear → edit-profile; NextOrderGlimpse below; three rounded-[32px] section cards w/ per-item colored 15%-alpha icon tiles (orders brand600, cart #10B981, coupons #8B5CF6, addresses #F59E0B, profile #3B82F6, complaints #EF4444, terms #6B7280) + chevron circles; white rounded-[24px] "Log Out from Freshyo" card; "Version 1.2.0 • Freshyo App" footer. Removed profile-card + About section.
- me.orders.tsx — full RN order card (rounded-[28px], ORDER REFERENCE header w/ blue dot, cancelled/FLASH pills, delivery + placed-on rows, 3-dot menu, item thumbnails w/ qty bubbles, +N more, shipping status, notes/cancel panels, total + Saved ₹ + delivery, View Details); page-1/10 infinite scroll with Load-more + loading/retry/end-of-list footer; empty/error states; local REFUND_STATUS const matching packages/ui (na/initiated/success/pending); mobile header "My Orders".
- me.orders.$id.tsx — Dialog → BottomDialog for cancel + complaint sheets; bg-linear → bg-gradient fix; header/colors aligned to RN detail.
- me.addresses.tsx — RN address cards w/ Default badge + 📞/📍, Set Default(Edit/Delete actions; + FAB header; empty state w/ MapPin + Add; loading/error+retry; add/edit via web BottomDialog + existing web AddressForm; window.confirm + alert parity for delete/set-default.
- me.coupons.tsx — wrapped w/ gray-50 + mobile header "My Coupons"; loading/error headers; content identical.
- me.complaints.tsx — full-bleed gray-50 page + mobile header "Complaints"; loading/error aligned; removed AppContainer confinement so brand "Need Help?" header is full-width.
- me.edit-profile.tsx — RN parity: centered avatar + camera upload (FileReader → useUploadToObjectStorage 'profile' context, ProfileImage), labeled Full Name/Email/Mobile fields w/ gray-50 bg + inline validation, brand "Update Profile", brand "Update Password" BottomDialog (match+length validation), gray Logout row, red "Delete Me and My Data" + BottomDialog confirm (⚠️ title + mobile entry + Cancel/Delete Forever). updateProfile now sends mobile + optional profileImageUrl (was name/email only).
- me.terms.tsx — replaced invented stub with verbatim FRESHYO legal content (sections AE) from apps/user-ui/components/TermsAndConditions.tsx inside a white card + mobile header.
NO LONGER LINKED: /me/about (hub About entry removed; RN has no About screen). Route file me.about.tsx left in place but no nav entry points to it.
NOTES: no backend/migration changes; colors/tokens reused from web-ui theme.