feat(account): open My Account as a modal over the page behind it (#51)
/account had no header and no links of any kind, so once a customer opened it the only way back to the storefront was the browser's back button or editing the URL. It is now a modal rendered over whatever the customer was looking at, while staying a real route. Opening it from the header pushes /account and names the current page as the backdrop, so closing returns there with filters intact, and the browser's Back button does the same thing as the close control. Entering /account directly — a bookmark, the link in a verification email, or the redirect after registering — has no page behind it and falls back to the storefront, so closing always lands somewhere real. Keeping it a route means the URL still works: bookmarkable, shareable, and refreshable with the view still open, which the header link and the four post-authentication redirects already depend on. Deleting an account now clears the session as well. Previously it removed the account server-side and navigated home without touching the auth context, so the header went on offering "My Account" for an account that no longer existed until the next reload. That was always wrong, but the modal makes it visible rather than merely stale, because the storefront is rendered behind and the wrong header is on screen throughout. The modal body is capped and scrolls, and the order history table scrolls within itself, so the view survives a phone without pushing its own title and close control off-screen. Also scopes the account switch locator in the favorites spec to the modal, since the storefront now renders behind the account view and has a theme switch of its own, and gives the post-registration wait a realistic timeout — it waits on a bcrypt round-trip rather than a render, and the 5s default was surfacing as a flake on whichever test lost the race under parallel load. Closes #51
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Typography, Switch, Button, Table, Modal, message, Space, Divider } from 'antd';
|
||||
import { Typography, Switch, Button, Table, Modal, message, Space, Divider } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchMyOrders, OrderHistoryItem, updateConsent, exportMyData, deleteMyAccount } from './customerApi';
|
||||
import { setFavoriteAlerts } from './favoritesApi';
|
||||
@@ -7,7 +7,13 @@ import { useCustomerAuth } from './CustomerAuthContext';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Account() {
|
||||
interface Props {
|
||||
// Supplied by the route, which decides where closing lands: back to the page
|
||||
// the customer came from, or to the storefront when they arrived directly.
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function Account({ onClose }: Props) {
|
||||
const { customer, loading, refresh, logout } = useCustomerAuth();
|
||||
const [orders, setOrders] = useState<OrderHistoryItem[]>([]);
|
||||
const navigate = useNavigate();
|
||||
@@ -59,16 +65,37 @@ export default function Account() {
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteMyAccount();
|
||||
// The storefront is rendered behind this modal, so without clearing the
|
||||
// session it goes on showing "My Account" and hiding Sign up for an
|
||||
// account that no longer exists — visibly stale, not merely stale in
|
||||
// state. replace, so Back cannot return to /account and bounce to
|
||||
// /login.
|
||||
refresh();
|
||||
message.success('Account deleted');
|
||||
navigate('/');
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 700, margin: '48px auto', padding: '0 16px' }}>
|
||||
<Card>
|
||||
<Title level={3}>My Account</Title>
|
||||
<Modal
|
||||
open
|
||||
// The account view is a place a customer can be sent by an email link or
|
||||
// a bookmark, so it is titled and closable rather than relying on the
|
||||
// page behind it to say where they are.
|
||||
title="My Account"
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={700}
|
||||
// The view holds profile, two consents, order history, and the account
|
||||
// controls, which is taller than a phone. Capping the body and letting it
|
||||
// scroll keeps the title and close control in reach instead of pushing
|
||||
// them off-screen.
|
||||
style={{ maxWidth: 'calc(100vw - 32px)', top: 24 }}
|
||||
styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}
|
||||
destroyOnHidden
|
||||
>
|
||||
<div>
|
||||
<Text>{customer.email}</Text>
|
||||
{!customer.email_verified && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
@@ -98,6 +125,9 @@ export default function Account() {
|
||||
size="small"
|
||||
dataSource={orders}
|
||||
pagination={false}
|
||||
// Scrolls within itself rather than widening the modal past the
|
||||
// viewport on a phone.
|
||||
scroll={{ x: 'max-content' }}
|
||||
columns={[
|
||||
{ title: 'Item', dataIndex: 'item_name' },
|
||||
{ title: 'Amount', dataIndex: 'amount_cents', render: (v: number) => `$${(v / 100).toFixed(2)}` },
|
||||
@@ -112,7 +142,7 @@ export default function Account() {
|
||||
<Button onClick={handleLogout}>Log out</Button>
|
||||
<Button danger onClick={handleDelete}>Delete my account</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user