Seven reported items, of which the first four had two root causes. The active tab was invisible in dark mode because colorPrimary was hardcoded to #1a1a1a in both themes. The accent now inverts with the theme, and colorTextLightSolid inverts with it, or a near-white accent would get antd's default white label and disappear. The Category tab, Tag tab, and item-form category selector ignored the theme entirely. antd declares main: lib/index.js and module: es/index.js, so importing from 'antd' resolves to the ES build while 'antd/lib/...' loads the CommonJS one — two copies, two React contexts, and no ConfigProvider for anything deep-imported. Switching those files to antd/es/* keeps the deep-import convention and shares the instance. This was introduced by my own use of the lib path; es is correct under Vite. Two storefront components had the same latent bug. "Colour" is now "Color". The Customers tab shows how many items each customer is holding, as a link opening the item list with a Release button. Release mirrors the customer's own cart removal — drop the cart row, return the item to available, guarded on 'reserved' so it can never resurrect a sold item — and deliberately sends no email about an action the customer did not take. The count is a subquery rather than another join, which would have multiplied rows and inflated order_count and total_spent_cents. The Inventory tab filters by category, tags, price, and status, reusing the storefront's parser and query builder so the two cannot drift. Reserved is one option in a Status filter rather than a standalone toggle. Also fixes two defects the screenshots exposed: the reserved-count link bubbled to the row handler and opened the customer drawer behind the dialog, and .admin-category-node had no CSS at all, so the tree node name, item count, and actions ran together as one string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { Link, useSearchParams } from 'react-router-dom';
|
|
import Card from 'antd/es/card';
|
|
import Typography from 'antd/es/typography';
|
|
import Alert from 'antd/es/alert';
|
|
import Button from 'antd/es/button';
|
|
import Spin from 'antd/es/spin';
|
|
import { verifyEmail } from './customerApi';
|
|
import { useCustomerAuth } from './CustomerAuthContext';
|
|
|
|
const { Title, Paragraph } = Typography;
|
|
|
|
type Status = 'verifying' | 'verified' | 'failed';
|
|
|
|
export default function VerifyEmail() {
|
|
const [searchParams] = useSearchParams();
|
|
const token = searchParams.get('token');
|
|
const [status, setStatus] = useState<Status>('verifying');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { refresh } = useCustomerAuth();
|
|
|
|
// The backend deletes the token once it succeeds, so a second request for the
|
|
// same token comes back as "invalid or expired" and would overwrite the success
|
|
// state with an error. StrictMode runs effects twice in dev, which is exactly
|
|
// that scenario, so pin the request to a single firing.
|
|
const requested = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (requested.current) return;
|
|
requested.current = true;
|
|
|
|
if (!token) {
|
|
setStatus('failed');
|
|
setError('This link is missing its verification token.');
|
|
return;
|
|
}
|
|
|
|
verifyEmail(token)
|
|
.then(() => {
|
|
setStatus('verified');
|
|
refresh();
|
|
})
|
|
.catch((err: Error) => {
|
|
setStatus('failed');
|
|
setError(err.message);
|
|
});
|
|
}, [token, refresh]);
|
|
|
|
return (
|
|
<div style={{ maxWidth: 420, margin: '48px auto', padding: '0 16px' }}>
|
|
<Card>
|
|
<Title level={3}>Email verification</Title>
|
|
|
|
{status === 'verifying' && (
|
|
<Paragraph>
|
|
<Spin style={{ marginRight: 12 }} />
|
|
Verifying your email address…
|
|
</Paragraph>
|
|
)}
|
|
|
|
{status === 'verified' && (
|
|
<>
|
|
<Alert type="success" message="Your email address is verified." style={{ marginBottom: 16 }} />
|
|
<Link to="/account">
|
|
<Button type="primary" block>Go to my account</Button>
|
|
</Link>
|
|
</>
|
|
)}
|
|
|
|
{status === 'failed' && (
|
|
<>
|
|
<Alert type="error" message={error || 'Verification failed.'} style={{ marginBottom: 16 }} />
|
|
<Paragraph type="secondary">
|
|
Verification links expire 24 hours after the account is created. If yours has
|
|
expired, the link can no longer be used.
|
|
</Paragraph>
|
|
<Link to="/account">
|
|
<Button block>Go to my account</Button>
|
|
</Link>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|