DEAD_CODE_CLEAN #7
15 changed files with 1790 additions and 346 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
252
apps/web-ui/src/components/me/MeOrderMenu.tsx
Normal file
252
apps/web-ui/src/components/me/MeOrderMenu.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
38
apps/web-ui/src/components/me/MeScreenHeader.tsx
Normal file
38
apps/web-ui/src/components/me/MeScreenHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
200
apps/web-ui/src/components/me/NextOrderGlimpse.tsx
Normal file
200
apps/web-ui/src/components/me/NextOrderGlimpse.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
{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="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">
|
||||
<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
|
||||
<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>
|
||||
|
||||
<div className="mt-3 flex flex-row justify-end">
|
||||
{!address.isDefault && (
|
||||
<button
|
||||
onClick={() => deleteMutation.mutate({ id: addr.id })}
|
||||
className="text-sm text-red-500 hover:text-red-700"
|
||||
onClick={() => onSetDefault(address.id)}
|
||||
className="mr-2 rounded bg-gray-500 px-3 py-2 transition-colors hover:bg-gray-600"
|
||||
>
|
||||
Delete
|
||||
<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>
|
||||
)}
|
||||
</AppContainer>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,27 +116,31 @@ 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="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">
|
||||
|
|
@ -174,6 +178,7 @@ function CouponsPage() {
|
|||
emptyMessage="No general coupons available"
|
||||
/>
|
||||
</div>
|
||||
</AppContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
{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"
|
||||
<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"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 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>
|
||||
<p className="font-semibold text-sm">
|
||||
Order #{order.id}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{order.createdAt
|
||||
? new Date(order.createdAt).toLocaleDateString()
|
||||
: ''}
|
||||
<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>
|
||||
<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'
|
||||
<p className="mt-0.5 text-base font-extrabold text-slate-900">#{item.orderId}</p>
|
||||
</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>
|
||||
)}
|
||||
{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'
|
||||
}`}
|
||||
>
|
||||
{order.status}
|
||||
</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
<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>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
Total: ₹{order.totalAmount || 0}
|
||||
)}
|
||||
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* Render child routes (order detail) when not on exact /me/orders path */
|
||||
<Outlet />
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</AppContainer>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{/* A. TERMS & CONDITIONS */}
|
||||
<p className="mb-2 text-base font-bold text-gray-900">A. TERMS & CONDITIONS</p>
|
||||
|
||||
<p className="mb-1 text-sm font-semibold text-gray-800">1. About FRESHYO</p>
|
||||
<p className="mb-4">
|
||||
By using Freshyo, you agree to these terms. If you do not agree, please
|
||||
do not use our service.
|
||||
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">
|
||||
2. Orders and Payments
|
||||
</p>
|
||||
<p className="mb-1 text-sm font-semibold text-gray-800">2. Eligibility</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).
|
||||
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">
|
||||
3. Delivery Policy
|
||||
</p>
|
||||
<p className="mb-1 text-sm font-semibold text-gray-800">3. Service Area</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.
|
||||
Services are limited strictly to Mahabubnagar Town. Orders outside the service area may be
|
||||
cancelled without notice.
|
||||
</p>
|
||||
|
||||
<p className="font-semibold mb-2 mt-4 text-gray-900">
|
||||
4. Returns and Refunds
|
||||
</p>
|
||||
<p className="mb-1 text-sm font-semibold text-gray-800">4. Account Registration</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.
|
||||
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="font-semibold mb-2 mt-4 text-gray-900">
|
||||
5. Privacy
|
||||
</p>
|
||||
<p className="mb-1 text-sm font-semibold text-gray-800">5. Orders</p>
|
||||
<p className="mb-4">
|
||||
We respect your privacy. Your personal information is used only for
|
||||
order processing and delivery purposes.
|
||||
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 & 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 & 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 & 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>
|
||||
</AppContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
],
|
||||
{
|
||||
title: 'Help & Complaints',
|
||||
icon: MessageSquare,
|
||||
color: '#EF4444',
|
||||
to: '/me/complaints',
|
||||
subtitle: 'Talk to our customer care',
|
||||
},
|
||||
{
|
||||
section: 'About',
|
||||
items: [
|
||||
{ icon: Info, label: 'About Us', to: '/me/about' },
|
||||
title: 'Terms & Conditions',
|
||||
icon: FileText,
|
||||
color: '#6B7280',
|
||||
to: '/me/terms',
|
||||
subtitle: 'Read our legal policies',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -74,62 +136,68 @@ 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 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-lg font-extrabold text-gray-900">
|
||||
{user.name || 'User'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{user.mobile}</p>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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}
|
||||
<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-xl border border-gray-200 bg-white">
|
||||
{section.items.map((item) => (
|
||||
<div className="overflow-hidden rounded-[32px] border border-gray-100 bg-white shadow-sm">
|
||||
{section.items.map((item, iIndex) => (
|
||||
<button
|
||||
key={item.label}
|
||||
key={iIndex}
|
||||
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"
|
||||
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' : ''
|
||||
}`}
|
||||
>
|
||||
<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" />
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-8 flex flex-col items-center gap-4">
|
||||
<MyButton
|
||||
{/* Logout row */}
|
||||
<button
|
||||
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
|
||||
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>
|
||||
) : (
|
||||
/* Render child routes when not on exact /me path */
|
||||
<Outlet />
|
||||
|
|
|
|||
|
|
@ -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 A–E) 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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue