refactor(frontend): declare props read-only, and drop the deprecated antd prop (#100) #157

Merged
bermudalamb merged 1 commits from feature/100-readonly-props into main 2026-08-24 12:01:54 -05:00
18 changed files with 31 additions and 31 deletions
Showing only changes of commit 8de261538b - Show all commits
+2 -2
View File
@@ -40,7 +40,7 @@ import DevThrow from './components/DevThrow';
const { Header, Content, Footer } = Layout; const { Header, Content, Footer } = Layout;
const { Title } = Typography; const { Title } = Typography;
interface CatalogueProps { type CatalogueProps = Readonly<{
failed: boolean; failed: boolean;
loading: boolean; loading: boolean;
items: Item[]; items: Item[];
@@ -50,7 +50,7 @@ interface CatalogueProps {
onSignIn: () => void; onSignIn: () => void;
onClearFilters: () => void; onClearFilters: () => void;
onChanged: () => void; onChanged: () => void;
} }>;
// The body of the catalogue: an outage, a sign-in prompt, an empty state, or // The body of the catalogue: an outage, a sign-in prompt, an empty state, or
// the grid. Extracted from App so the four cases read as early returns rather // the grid. Extracted from App so the four cases read as early returns rather
+2 -2
View File
@@ -21,7 +21,7 @@ function toTreeData(nodes: CategoryNode[]): CategoryTreeOption[] {
})); }));
} }
interface Props { type Props = Readonly<{
// Supplied by antd's Form.Item. `id` has to be forwarded or the field loses // Supplied by antd's Form.Item. `id` has to be forwarded or the field loses
// its association with the rendered <label>, which breaks both screen readers // its association with the rendered <label>, which breaks both screen readers
// and any lookup by label. // and any lookup by label.
@@ -30,7 +30,7 @@ interface Props {
id?: string; id?: string;
categories: Category[]; categories: Category[];
onCategoriesChanged: (categories: Category[]) => void; onCategoriesChanged: (categories: Category[]) => void;
} }>;
// Pulled out of the item form so that typing a new category name re-renders // Pulled out of the item form so that typing a new category name re-renders
// only this control. Left inline, every keystroke re-rendered the whole // only this control. Left inline, every keystroke re-rendered the whole
+2 -2
View File
@@ -36,13 +36,13 @@ function toTreeData(nodes: CategoryNode[]): CategoryTreeOption[] {
})); }));
} }
interface Props { type Props = Readonly<{
categories: Category[]; categories: Category[];
tags: Tag[]; tags: Tag[];
filters: ItemFilters; filters: ItemFilters;
onChange: (filters: ItemFilters) => void; onChange: (filters: ItemFilters) => void;
onClear: () => void; onClear: () => void;
} }>;
const centsToDollars = (cents: number | null): number | null => (cents === null ? null : cents / 100); const centsToDollars = (cents: number | null): number | null => (cents === null ? null : cents / 100);
const dollarsToCents = (dollars: number | null): number | null => const dollarsToCents = (dollars: number | null): number | null =>
+1 -1
View File
@@ -250,7 +250,7 @@ export default function Cart() {
open={addAddressOpen} open={addAddressOpen}
onOk={handleAddAddress} onOk={handleAddAddress}
onCancel={() => setAddAddressOpen(false)} onCancel={() => setAddAddressOpen(false)}
destroyOnClose destroyOnHidden
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item name="fullName" label="Full Name" rules={[{ required: true }]}> <Form.Item name="fullName" label="Full Name" rules={[{ required: true }]}>
+1 -1
View File
@@ -14,7 +14,7 @@ export function useCart() {
return useContext(CartContext); return useContext(CartContext);
} }
export function CartProvider({ children }: { children: React.ReactNode }) { export function CartProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const [items, setItems] = useState<CartItem[]>([]); const [items, setItems] = useState<CartItem[]>([]);
const { customer } = useCustomerAuth(); const { customer } = useCustomerAuth();
@@ -3,12 +3,12 @@ import Button from 'antd/es/button';
import type { FilterOptions } from '../api'; import type { FilterOptions } from '../api';
import { ItemFilters, categoryPath, formatPriceRange, hasActiveFilters } from '../filters'; import { ItemFilters, categoryPath, formatPriceRange, hasActiveFilters } from '../filters';
interface Props { type Props = Readonly<{
options: FilterOptions | null; options: FilterOptions | null;
filters: ItemFilters; filters: ItemFilters;
onChange: (filters: ItemFilters) => void; onChange: (filters: ItemFilters) => void;
onClear: () => void; onClear: () => void;
} }>;
export default function ActiveFilterChips({ options, filters, onChange, onClear }: Props) { export default function ActiveFilterChips({ options, filters, onChange, onClear }: Props) {
if (!hasActiveFilters(filters)) return null; if (!hasActiveFilters(filters)) return null;
+2 -2
View File
@@ -1,9 +1,9 @@
import { useId } from 'react'; import { useId } from 'react';
interface Props { type Props = Readonly<{
size?: number; size?: number;
className?: string; className?: string;
} }>;
/** /**
* The RD monogram, as an inline SVG rather than an `<img src="/favicon.svg">`. * The RD monogram, as an inline SVG rather than an `<img src="/favicon.svg">`.
+2 -2
View File
@@ -11,7 +11,7 @@ import type { DataNode } from 'antd/es/tree';
import type { FilterOptions } from '../api'; import type { FilterOptions } from '../api';
import { ItemFilters, buildCategoryTree, CategoryNode } from '../filters'; import { ItemFilters, buildCategoryTree, CategoryNode } from '../filters';
interface Props { type Props = Readonly<{
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
options: FilterOptions | null; options: FilterOptions | null;
@@ -19,7 +19,7 @@ interface Props {
onChange: (filters: ItemFilters) => void; onChange: (filters: ItemFilters) => void;
onClear: () => void; onClear: () => void;
resultCount: number; resultCount: number;
} }>;
function toTreeData(nodes: CategoryNode[]): DataNode[] { function toTreeData(nodes: CategoryNode[]): DataNode[] {
return nodes.map((node) => ({ return nodes.map((node) => ({
+2 -2
View File
@@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
interface Props { type Props = Readonly<{
content: string | null; content: string | null;
} }>;
export default function MarkdownView({ content }: Props) { export default function MarkdownView({ content }: Props) {
if (!content) return null; if (!content) return null;
+2 -2
View File
@@ -14,11 +14,11 @@ import AccountDetails from './AccountDetails';
const { Text } = Typography; const { Text } = Typography;
interface Props { type Props = Readonly<{
// Supplied by the route, which decides where closing lands: back to the page // 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. // the customer came from, or to the storefront when they arrived directly.
onClose: () => void; onClose: () => void;
} }>;
export default function Account({ onClose }: Props) { export default function Account({ onClose }: Props) {
const { customer, loading, refresh, logout } = useCustomerAuth(); const { customer, loading, refresh, logout } = useCustomerAuth();
+2 -2
View File
@@ -23,7 +23,7 @@ export type AuthMode = 'register' | 'login';
export const MARKETING_CONSENT_TEXT = export const MARKETING_CONSENT_TEXT =
'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.'; 'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.';
interface Props { type Props = Readonly<{
mode: AuthMode; mode: AuthMode;
onModeChange: (mode: AuthMode) => void; onModeChange: (mode: AuthMode) => void;
onForgotPassword: () => void; onForgotPassword: () => void;
@@ -31,7 +31,7 @@ interface Props {
// route closes back to the page behind it, while the cart and favorite // route closes back to the page behind it, while the cart and favorite
// prompts resume the action the customer was interrupted doing. // prompts resume the action the customer was interrupted doing.
onSuccess: () => void; onSuccess: () => void;
} }>;
// The one implementation of signing in and registering. It was previously // The one implementation of signing in and registering. It was previously
// written twice — once as the /login and /register pages, once inside the // written twice — once as the /login and /register pages, once inside the
+2 -2
View File
@@ -3,11 +3,11 @@ import Modal from 'antd/es/modal';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import AuthForm, { AuthMode } from './AuthForm'; import AuthForm, { AuthMode } from './AuthForm';
interface Props { type Props = Readonly<{
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
} }>;
// The prompt shown when a signed-out visitor does something that needs an // The prompt shown when a signed-out visitor does something that needs an
// account — adding to the cart, favoriting, or filtering by favorites. It is // account — adding to the cart, favoriting, or filtering by favorites. It is
+2 -2
View File
@@ -1,13 +1,13 @@
import Modal from 'antd/es/modal'; import Modal from 'antd/es/modal';
import AuthForm, { AuthMode } from './AuthForm'; import AuthForm, { AuthMode } from './AuthForm';
interface Props { type Props = Readonly<{
mode: AuthMode; mode: AuthMode;
onClose: () => void; onClose: () => void;
// Moving between the auth routes, supplied by the router so the rule about // Moving between the auth routes, supplied by the router so the rule about
// keeping the whole detour to one history entry lives in one place. // keeping the whole detour to one history entry lives in one place.
onNavigate: (path: string) => void; onNavigate: (path: string) => void;
} }>;
const TITLES: Record<AuthMode, string> = { const TITLES: Record<AuthMode, string> = {
register: 'Create an account', register: 'Create an account',
@@ -19,7 +19,7 @@ export function useCustomerAuth() {
return useContext(CustomerAuthContext); return useContext(CustomerAuthContext);
} }
export function CustomerAuthProvider({ children }: { children: React.ReactNode }) { export function CustomerAuthProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const [customer, setCustomer] = useState<Customer | null>(null); const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
+1 -1
View File
@@ -21,7 +21,7 @@ export function useFavorites() {
// Mirrors CartProvider: one fetch for the whole storefront rather than each // Mirrors CartProvider: one fetch for the whole storefront rather than each
// card asking whether it is favorited, and it clears on sign-out so one // card asking whether it is favorited, and it clears on sign-out so one
// customer's favorites never show to the next. // customer's favorites never show to the next.
export function FavoritesProvider({ children }: { children: React.ReactNode }) { export function FavoritesProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const [favorites, setFavorites] = useState<Favorite[]>([]); const [favorites, setFavorites] = useState<Favorite[]>([]);
const { customer } = useCustomerAuth(); const { customer } = useCustomerAuth();
+2 -2
View File
@@ -9,12 +9,12 @@ import { requestPasswordReset } from './customerApi';
const { Paragraph, Text } = Typography; const { Paragraph, Text } = Typography;
interface Props { type Props = Readonly<{
onClose: () => void; onClose: () => void;
// Steps back to sign-in without leaving a history entry behind, the same way // Steps back to sign-in without leaving a history entry behind, the same way
// the auth modal switches between its own tabs. // the auth modal switches between its own tabs.
onBackToSignIn: () => void; onBackToSignIn: () => void;
} }>;
export default function ForgotPassword({ onClose, onBackToSignIn }: Props) { export default function ForgotPassword({ onClose, onBackToSignIn }: Props) {
const [sent, setSent] = useState(false); const [sent, setSent] = useState(false);
+2 -2
View File
@@ -11,11 +11,11 @@ import { useCustomerAuth } from './CustomerAuthContext';
const { Paragraph } = Typography; const { Paragraph } = Typography;
interface Props { type Props = Readonly<{
onClose: () => void; onClose: () => void;
onRequestNewLink: () => void; onRequestNewLink: () => void;
onBackToSignIn: () => void; onBackToSignIn: () => void;
} }>;
export default function ResetPassword({ onClose, onRequestNewLink, onBackToSignIn }: Props) { export default function ResetPassword({ onClose, onRequestNewLink, onBackToSignIn }: Props) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
+1 -1
View File
@@ -23,7 +23,7 @@ function getInitialMode(): ThemeMode {
: 'light'; : 'light';
} }
export function ThemeModeProvider({ children }: { children: React.ReactNode }) { export function ThemeModeProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const [mode, setMode] = useState<ThemeMode>(getInitialMode); const [mode, setMode] = useState<ThemeMode>(getInitialMode);
useEffect(() => { useEffect(() => {