refactor(frontend): declare props read-only, and drop the deprecated antd prop (#100) #157
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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 }]}>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">`.
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user