Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,49 @@ After discovering the project's specific touchpoints, use a checklist like this
- [ ] No duplicate/alias env vars introduced
- [ ] Defaults are consistent across all touchpoints
```

## Data Fetching with React Query

This project uses **@tanstack/react-query** (v5) for client-side data fetching. The `QueryClientProvider` is set up in `pages/_app.tsx`.

### Guidelines

1. **Use `useQuery` for all GET requests in components.** Do not use raw `useEffect` + `fetch` for data loading. React Query provides caching, deduplication, background refetching, and proper loading/error states out of the box.

2. **Query keys** must be descriptive arrays: `["resource-name", ...params]`. Examples:
- `["pickup-slots", page]`
- `["order", orderId]`
- `["my-orders", { limit }]`

3. **Use `useMutation`** for POST/PUT/DELETE operations that modify server state. Invalidate related queries after mutations succeed:
```ts
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateOrder,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["orders"] }),
});
```

4. **Server-side pagination** — API routes should accept `limit` and `offset` query params and return `{ total, limit, offset, hasMore }` metadata alongside the data array. Use `placeholderData: (prev) => prev` in `useQuery` to keep previous data visible while the next page loads.

5. **Default options** are configured in `_app.tsx`:
- `staleTime: 30s` — data is considered fresh for 30 seconds before background refetch
- `retry: 1` — one automatic retry on failure

6. **Do not install `react-query`** (v3). The package is `@tanstack/react-query` (v5).

### Refactoring roadmap

The following components still use raw `useEffect` + `fetch` and should be migrated to React Query as they are touched:

- `components/ordercontainer/OrderContainer.tsx` — order resumption & pending orders fetch
- `components/admin/PendingVerificationView.tsx`
- `components/admin/OrdersByTimeslotView.tsx`
- `components/admin/ScheduleCalendarView.tsx`
- `components/admin/NotifyTimeslotsView.tsx`
- `components/payment/StripePaymentForm.tsx`
- `components/payment/BankTransferForm.tsx`
- `pages/my-orders.tsx`
- `pages/order_complete.tsx`

When refactoring these components, follow the pattern established in `components/pickup/TimeslotSelector.tsx`.
15 changes: 11 additions & 4 deletions app/api/pickup-slots/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { getPayloadClient } from "../../../lib/payload";
* slots before selecting one.
*
* Query params:
* - `limit` (number, default 50)
* - `limit` (number, default 50, max 100) — page size
* - `offset` (number, default 0) — number of slots to skip
*/
export async function GET(request: NextRequest) {
try {
Expand All @@ -21,6 +22,7 @@ export async function GET(request: NextRequest) {
100,
Math.max(1, Number(searchParams.get("limit")) || 50),
);
const offset = Math.max(0, Number(searchParams.get("offset")) || 0);

const payload = await getPayloadClient();

Expand Down Expand Up @@ -121,12 +123,17 @@ export async function GET(request: NextRequest) {
filtered.push(slot);
}

// Apply the requested limit
const limited = filtered.slice(0, limit);
// Apply offset-based pagination
const total = filtered.length;
const paginated = filtered.slice(offset, offset + limit);

return NextResponse.json({
success: true,
timeslots: limited.map((slot) => {
total,
limit,
offset,
hasMore: offset + limit < total,
timeslots: paginated.map((slot) => {
const maxCap =
typeof slot.maxCapacity === "number" ? slot.maxCapacity : null;
const booked =
Expand Down
31 changes: 13 additions & 18 deletions components/ordercontainer/OrderContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const OrderContainerInner = ({
case OrderStatus.AWAITING_PAYMENT:
return OrderStep.PAYMENT;
case OrderStatus.PAID:
return OrderStep.PICKUP;
case OrderStatus.AWAITING_PICKUP:
return OrderStep.PICKUP;
case OrderStatus.PRINTED:
case OrderStatus.PICKED_UP:
return OrderStep.COMPLETE;
Expand All @@ -93,31 +93,26 @@ const OrderContainerInner = ({
}
}, []);

// Resume an existing order or jump to pickup selection
// Resume an existing order or jump to pickup selection.
// Resets resumeChecked to false and re-fetches whenever the query
// params change (e.g. clicking "Change Pickup" while already on
// the /order page).
useEffect(() => {
const orderIdToResume = resumeOrderId || pickupForOrderId;
if (!orderIdToResume || resumeChecked) return;
if (!orderIdToResume) return;

setResumeChecked(false);

async function resumeOrder() {
try {
const res = await fetch(`/api/shop/${orderIdToResume}`);
if (!res.ok) {
setResumeChecked(true);
return;
}
if (!res.ok) return;

const data = await res.json();
const order = data.order;

if (!order?.status) {
setResumeChecked(true);
return;
}

if (order.status === OrderStatus.EXPIRED) {
// Can't resume expired orders
setResumeChecked(true);
return;
}
if (!order?.status) return;
if (order.status === OrderStatus.EXPIRED) return;

setActiveOrderId(order.id);
setActiveOrderNumber(order.orderNumber);
Expand All @@ -140,7 +135,7 @@ const OrderContainerInner = ({
}

resumeOrder();
}, [resumeOrderId, pickupForOrderId, resumeChecked, statusToStep]);
}, [resumeOrderId, pickupForOrderId, statusToStep]);

// Fetch any in-progress orders for authenticated users
useEffect(() => {
Expand Down
12 changes: 10 additions & 2 deletions components/ordercontainer/OrderSteps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,16 @@ export default function OrderSteps({
if (order.status === OrderStatus.PAID && step === OrderStep.PAYMENT) {
onPaymentSuccess();
}
// If already has a timeslot, jump to complete
// If already has a timeslot, jump to complete — but not
// when the user is on the PICKUP step to change their timeslot.
if (
[
OrderStatus.AWAITING_PICKUP,
OrderStatus.PRINTED,
OrderStatus.PICKED_UP,
].includes(order.status) &&
step !== OrderStep.COMPLETE
step !== OrderStep.COMPLETE &&
step !== OrderStep.PICKUP
) {
onPickupConfirmed();
}
Expand Down Expand Up @@ -593,6 +595,12 @@ export default function OrderSteps({
)}

<Box display="flex" gap={4} justifyContent="center" flexWrap="wrap">
<Button
colorScheme="green"
onClick={() => router.push(`/order?pickupFor=${orderId}`)}
>
Change Pickup
</Button>
<Button colorScheme="blue" onClick={() => router.push("/my-orders")}>
View My Orders
</Button>
Expand Down
Loading
Loading