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
+2 -2
View File
@@ -40,7 +40,7 @@ import DevThrow from './components/DevThrow';
const { Header, Content, Footer } = Layout;
const { Title } = Typography;
interface CatalogueProps {
type CatalogueProps = Readonly<{
failed: boolean;
loading: boolean;
items: Item[];
@@ -50,7 +50,7 @@ interface CatalogueProps {
onSignIn: () => void;
onClearFilters: () => void;
onChanged: () => void;
}
}>;
// 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
+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
// its association with the rendered <label>, which breaks both screen readers
// and any lookup by label.
@@ -30,7 +30,7 @@ interface Props {
id?: string;
categories: Category[];
onCategoriesChanged: (categories: Category[]) => void;
}
}>;
// 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
+2 -2
View File
@@ -36,13 +36,13 @@ function toTreeData(nodes: CategoryNode[]): CategoryTreeOption[] {
}));
}
interface Props {
type Props = Readonly<{
categories: Category[];
tags: Tag[];
filters: ItemFilters;
onChange: (filters: ItemFilters) => void;
onClear: () => void;
}
}>;
const centsToDollars = (cents: number | null): number | null => (cents === null ? null : cents / 100);
const dollarsToCents = (dollars: number | null): number | null =>
+1 -1
View File
@@ -250,7 +250,7 @@ export default function Cart() {
open={addAddressOpen}
onOk={handleAddAddress}
onCancel={() => setAddAddressOpen(false)}
destroyOnClose
destroyOnHidden
>
<Form form={form} layout="vertical">
<Form.Item name="fullName" label="Full Name" rules={[{ required: true }]}>
+1 -1
View File
@@ -14,7 +14,7 @@ export function useCart() {
return useContext(CartContext);
}
export function CartProvider({ children }: { children: React.ReactNode }) {
export function CartProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const [items, setItems] = useState<CartItem[]>([]);
const { customer } = useCustomerAuth();
@@ -3,12 +3,12 @@ import Button from 'antd/es/button';
import type { FilterOptions } from '../api';
import { ItemFilters, categoryPath, formatPriceRange, hasActiveFilters } from '../filters';
interface Props {
type Props = Readonly<{
options: FilterOptions | null;
filters: ItemFilters;
onChange: (filters: ItemFilters) => void;
onClear: () => void;
}
}>;
export default function ActiveFilterChips({ options, filters, onChange, onClear }: Props) {
if (!hasActiveFilters(filters)) return null;
+2 -2
View File
@@ -1,9 +1,9 @@
import { useId } from 'react';
interface Props {
type Props = Readonly<{
size?: number;
className?: string;
}
}>;
/**
* 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 { ItemFilters, buildCategoryTree, CategoryNode } from '../filters';
interface Props {
type Props = Readonly<{
open: boolean;
onClose: () => void;
options: FilterOptions | null;
@@ -19,7 +19,7 @@ interface Props {
onChange: (filters: ItemFilters) => void;
onClear: () => void;
resultCount: number;
}
}>;
function toTreeData(nodes: CategoryNode[]): DataNode[] {
return nodes.map((node) => ({
+2 -2
View File
@@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
interface Props {
type Props = Readonly<{
content: string | null;
}
}>;
export default function MarkdownView({ content }: Props) {
if (!content) return null;
+2 -2
View File
@@ -14,11 +14,11 @@ import AccountDetails from './AccountDetails';
const { Text } = Typography;
interface Props {
type Props = Readonly<{
// 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();
+2 -2
View File
@@ -23,7 +23,7 @@ export type AuthMode = 'register' | 'login';
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.';
interface Props {
type Props = Readonly<{
mode: AuthMode;
onModeChange: (mode: AuthMode) => void;
onForgotPassword: () => void;
@@ -31,7 +31,7 @@ interface Props {
// route closes back to the page behind it, while the cart and favorite
// prompts resume the action the customer was interrupted doing.
onSuccess: () => void;
}
}>;
// The one implementation of signing in and registering. It was previously
// 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 AuthForm, { AuthMode } from './AuthForm';
interface Props {
type Props = Readonly<{
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
}>;
// 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
+2 -2
View File
@@ -1,13 +1,13 @@
import Modal from 'antd/es/modal';
import AuthForm, { AuthMode } from './AuthForm';
interface Props {
type Props = Readonly<{
mode: AuthMode;
onClose: () => void;
// 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.
onNavigate: (path: string) => void;
}
}>;
const TITLES: Record<AuthMode, string> = {
register: 'Create an account',
@@ -19,7 +19,7 @@ export function useCustomerAuth() {
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 [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
// card asking whether it is favorited, and it clears on sign-out so one
// 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 { customer } = useCustomerAuth();
+2 -2
View File
@@ -9,12 +9,12 @@ import { requestPasswordReset } from './customerApi';
const { Paragraph, Text } = Typography;
interface Props {
type Props = Readonly<{
onClose: () => void;
// Steps back to sign-in without leaving a history entry behind, the same way
// the auth modal switches between its own tabs.
onBackToSignIn: () => void;
}
}>;
export default function ForgotPassword({ onClose, onBackToSignIn }: Props) {
const [sent, setSent] = useState(false);
+2 -2
View File
@@ -11,11 +11,11 @@ import { useCustomerAuth } from './CustomerAuthContext';
const { Paragraph } = Typography;
interface Props {
type Props = Readonly<{
onClose: () => void;
onRequestNewLink: () => void;
onBackToSignIn: () => void;
}
}>;
export default function ResetPassword({ onClose, onRequestNewLink, onBackToSignIn }: Props) {
const [searchParams] = useSearchParams();
+1 -1
View File
@@ -23,7 +23,7 @@ function getInitialMode(): ThemeMode {
: 'light';
}
export function ThemeModeProvider({ children }: { children: React.ReactNode }) {
export function ThemeModeProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const [mode, setMode] = useState<ThemeMode>(getInitialMode);
useEffect(() => {