Prerequisite
Kahraman ve kötü adam bileşen grupları boyunca kullanılacak olan yeni arayüzler ve türler oluşturun.
// src/models/Boy.ts
export interface Boy {
id: string;
name: string;
description: string;
}
/* istanbul ignore file */
// src/models/types.ts
import { Hero } from "./Hero";
import { Villain } from "./Villain";
import { Boy } from "./Boy";
export type HeroProperty = Hero["name"] | Hero["description"] | Hero["id"];
export type VillainProperty =
| Villain["name"]
| Villain["description"]
| Villain["id"];
export type BoyProperty = Boy["name"] | Boy["description"] | Boy["id"];
export type EntityRoute = "heroes" | "villains" | "boys";
export type EntityType = "hero" | "villain" | "boy";
/** Returns the corresponding route for the entity;
*
* `hero` -> `/heroes`, `villain` -> `/villains`, `boy` -> `/boys` */
export const entityRoute = (entityType: EntityType) =>
entityType === "hero"
? "heroes"
: entityType === "villain"
? "villains"
: "boys";
/* istanbul ignore file */
api.ts
'yi kendi klasörüne taşıyın
api.ts
'yi kendi klasörüne taşıyın./src/hooks/api.ts
adresinden ./src/api/api.ts
adresine. IDE'niz bağımlılıkları otomatik olarak güncellemelidir.
Hook'ları güncelleyin
useDeleteEntity
, Boy
için güncellenir.
// src/hooks/useDeleteEntity.ts
import { Boy } from "models/Boy";
import { Hero } from "models/Hero";
import { EntityType, entityRoute } from "models/types";
import { Villain } from "models/Villain";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { deleteItem } from "../api/api";
/**
* Helper for DELETE to `/heroes`, `/villains` or 'boys' routes.
* @returns {object} {deleteEntity, isDeleting, isDeleteError, deleteError}
*/
export function useDeleteEntity(entityType: EntityType) {
const route = entityRoute(entityType);
const navigate = useNavigate();
const queryClient = useQueryClient();
const mutation = useMutation(
(item: Hero | Villain | Boy) => deleteItem(`${route}/${item.id}`),
{
// on success receives the original item as a second argument
// if you recall, the first argument is the created item
onSuccess: (_, deletedEntity: Hero | Villain | Boy) => {
// get all the entities from the cache
const entities: Hero[] | Villain[] | Boy[] =
queryClient.getQueryData([`${route}`]) || [];
// set the entities cache without the delete one
queryClient.setQueryData(
[`${route}`],
entities.filter((h) => h.id !== deletedEntity.id)
);
navigate(`/${route}`);
},
}
);
return {
deleteEntity: mutation.mutate,
isDeleting: mutation.isLoading,
isDeleteError: mutation.isError,
deleteError: mutation.error,
};
}
useGetEntities
yalnızca yorumun güncellenmesi gereklidir
/heroes
veya/villains
yollarına GET için yardımcı/heroes
,/villains
veya/boys
yollarına GET için yardımcı.
usePostEntity
Boy
için güncellenir.
// src/hooks/usePostEntity.ts
import { Boy } from "models/Boy";
import { Hero } from "models/Hero";
import { EntityType, entityRoute } from "models/types";
import { Villain } from "models/Villain";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { createItem } from "../api/api";
/**
* Helper for simple POST to `/heroes`, `/villains`, `/boys` routes
* @returns {object} {mutate, status, error}
*/
export function usePostEntity(entityType: EntityType) {
const route = entityRoute(entityType);
const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation((item: Hero | Villain | Boy) => createItem(route, item), {
onSuccess: (newData: Hero | Villain | Boy) => {
// use queryClient's setQueryData to set the cache
// takes a key as the first arg, the 2nd arg is a cb that takes the old query cache and returns the new one
queryClient.setQueryData(
[route],
(oldData: Hero[] | Villain[] | Boy[] | undefined) => [
...(oldData || []),
newData,
]
);
return navigate(`/${route}`);
},
});
}
usePutEntity
Boys
için güncellenir.
// src/hooks/usePutEntity.ts
import { Hero } from "models/Hero";
import { Boy } from "models/Boy";
import { Villain } from "models/Villain";
import { EntityType, entityRoute } from "models/types";
import { useMutation, useQueryClient } from "react-query";
import type { QueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { editItem } from "../api/api";
/**
* Helper for PUT to `/heroes` route
* @returns {object} {updateHero, isUpdating, isUpdateError, updateError}
*/
export function usePutEntity(entityType: EntityType) {
const route = entityRoute(entityType);
const queryClient = useQueryClient();
const navigate = useNavigate();
const mutation = useMutation(
(item: Hero | Villain | Boy) => editItem(`${route}/${item.id}`, item),
{
onSuccess: (updatedEntity: Hero | Villain | Boy) => {
updateEntityCache(entityType, updatedEntity, queryClient);
navigate(`/${route}`);
},
}
);
return {
updateEntity: mutation.mutate,
isUpdating: mutation.isLoading,
isUpdateError: mutation.isError,
updateError: mutation.error,
};
}
/** Replace a hero in the cache with the updated version. */
function updateEntityCache(
entityType: EntityType,
updatedEntity: Hero | Villain | Boy,
queryClient: QueryClient
) {
const route = entityRoute(entityType);
// get all the heroes from the cache
let entityCache: Hero[] | Villain[] | Boy[] =
queryClient.getQueryData(route) || [];
// find the index in the cache of the hero that's been edited
const entityIndex = entityCache.findIndex((h) => h.id === updatedEntity.id);
if (entityIndex !== -1) {
// if the entity is found, replace the pre-edited entity with the updated one
// this is just replacing an array item in place,
// while not mutating the original array
entityCache = entityCache.map((preEditedEntity) =>
preEditedEntity.id === updatedEntity.id ? updatedEntity : preEditedEntity
);
console.log("entityCache is", entityCache);
// use queryClient's setQueryData to set the cache
// takes a key as the first arg, the 2nd arg is the new cache
return queryClient.setQueryData([route], entityCache);
}
}
E2E komutlarını değiştirin
./cypress/support
altındaki 3 dosya arasında, alakalılık lehine içe aktarmaları bölmek mümkündür
commands.ts
: tüm eklenti içe aktarmaları ve e2e & CT ile ortak komutlar (ör:cy.getByCY
).component.ts
: bileşen testlerine özgü komutlar (ör:cy.mount
,cy.wrappedMount
).e2e.ts
: e2e'ye özgü komutlar (ör:cy.crud
).
// cypress/support/commands.ts
import "@testing-library/cypress/add-commands";
import "@bahmutov/cypress-code-coverage/support";
Cypress.Commands.add("getByCy", (selector, ...args) =>
cy.get(`[data-cy="${selector}"]`, ...args)
);
Cypress.Commands.add("getByCyLike", (selector, ...args) =>
cy.get(`[data-cy*=${selector}]`, ...args)
);
Cypress.Commands.add("getByClassLike", (selector, ...args) =>
cy.get(`[class*=${selector}]`, ...args)
);
component.tsx
ve e2e.ts
, CT ve e2e dosyalarının ortak komutlara ve eklentilere erişimi olması için commands.ts
'yi içe aktaracaktır.
// cypress/support/component.tsx
import "./commands";
import { mount } from "cypress/react18";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { ErrorBoundary } from "react-error-boundary";
import ErrorComp from "../../src/components/ErrorComp";
import PageSpinner from "../../src/components/PageSpinner";
import { Suspense } from "react";
Cypress.Commands.add("mount", mount);
Cypress.Commands.add(
"wrappedMount",
(WrappedComponent: React.ReactNode, options = {}) => {
const wrapped = (
<QueryClientProvider client={new QueryClient()}>
<ErrorBoundary fallback={<ErrorComp />}>
<Suspense fallback={<PageSpinner />}>
<BrowserRouter>{WrappedComponent}</BrowserRouter>
</Suspense>
</ErrorBoundary>
</QueryClientProvider>
);
return cy.mount(wrapped, options);
}
);
// cypress/support/e2e.ts
import "./commands";
import { Villain } from "./../../src/models/Villain";
import { Hero } from "../../src/models/Hero";
import { Boy } from "../../src/models/Boy";
import {
entityRoute,
EntityRoute,
EntityType,
HeroProperty,
VillainProperty,
BoyProperty,
} from "../../src/models/types";
import data from "../fixtures/db.json";
Cypress.Commands.add(
"crud",
(
method: "GET" | "POST" | "PUT" | "DELETE",
route: string,
{
body,
allowedToFail = false,
}: { body?: Hero | Villain | Boy | object; allowedToFail?: boolean } = {}
) =>
cy.request<(Hero[] & Hero) | (Villain[] & Villain) | (Boy[] & Boy)>({
method: method,
url: `${Cypress.env("API_URL")}/${route}`,
body: method === "POST" || method === "PUT" ? body : undefined,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
})
);
Cypress.Commands.add("resetData", () =>
cy.crud("POST", "reset", { body: data })
);
const { _ } = Cypress;
const propExists =
(property: HeroProperty | VillainProperty | BoyProperty) =>
(entity: Hero | Villain | Boy) =>
entity.name === property ||
entity.description === property ||
entity.id === property;
const getEntities = (entityRoute: EntityRoute) =>
cy.crud("GET", entityRoute).its("body");
Cypress.Commands.add(
"getEntityByProperty",
(
entityType: EntityType,
property: HeroProperty | VillainProperty | BoyProperty
) =>
getEntities(entityRoute(entityType)).then((entities) =>
_.find(entities, propExists(property))
)
);
Cypress.Commands.add(
"findEntityIndex",
(
entityType: EntityType,
property: HeroProperty | VillainProperty | BoyProperty
) =>
getEntities(entityRoute(entityType)).then(
(body: Hero[] | Villain[] | Boy[]) => ({
entityIndex: _.findIndex(body, propExists(property)),
entityArray: body,
})
)
);
Cypress.Commands.add("visitStubbedEntities", (entityRoute: EntityRoute) => {
cy.intercept("GET", `${Cypress.env("API_URL")}/${entityRoute}`, {
fixture: `${entityRoute}.json`,
}).as(`stubbed${_.startCase(entityRoute)}`);
cy.visit(`/${entityRoute}`);
cy.wait(`@stubbed${_.startCase(entityRoute)}`);
return cy.location("pathname").should("eq", `/${entityRoute}`);
});
Cypress.Commands.add("visitEntities", (entityRoute: EntityRoute) => {
cy.intercept("GET", `${Cypress.env("API_URL")}/${entityRoute}`).as(
`get${_.startCase(entityRoute)}`
);
cy.visit(`/${entityRoute}`);
cy.wait(`@get${_.startCase(entityRoute)}`);
return cy.location("pathname").should("eq", `/${entityRoute}`);
});
Repo kökündeki tip tanımlarını güncelleyin.
// cypress.d.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { MountOptions, MountReturn } from "cypress/react";
import { HeroProperty, VillainProperty, EntityType } from "models/types";
import type { Hero } from "./models/types/Hero";
import type { Villain } from "./models/types/Villain";
import type { Boy } from "./models/types/Boy";
export {};
declare global {
namespace Cypress {
interface Chainable {
/** Yields elements with a data-cy attribute that matches a specified selector.
* ```
* cy.getByCy('search-toggle') // where the selector is [data-cy="search-toggle"]
* ```
*/
getByCy(qaSelector: string, args?: any): Chainable<JQuery<HTMLElement>>;
/** Yields elements with data-cy attribute that partially matches a specified selector.
* ```
* cy.getByCyLike('chat-button') // where the selector is [data-cy="chat-button-start-a-new-claim"]
* ```
*/
getByCyLike(
qaSelector: string,
args?: any
): Chainable<JQuery<HTMLElement>>;
/** Yields the element that partially matches the css class
* ```
* cy.getByClassLike('StyledIconBase') // where the class is class="StyledIconBase-ea9ulj-0 lbJwfL"
* ```
*/
getByClassLike(
qaSelector: string,
args?: any
): Chainable<JQuery<HTMLElement>>;
/** Mounts a React node
* @param component React Node to mount
* @param options Additional options to pass into mount
*/
mount(
component: React.ReactNode,
options?: MountOptions
): Cypress.Chainable<MountReturn>;
/** Mounts the component wrapped by all the providers:
* QueryClientProvider, ErrorBoundary, Suspense, BrowserRouter
* @param component React Node to mount
* @param options Additional options to pass into mount
*/
wrappedMount(
component: React.ReactNode,
options?: MountOptions
): Cypress.Chainable<MountReturn>;
/** Visits heroes or villains routes, uses real network, verifies path */
visitEntities(entityRoute: EntityRoute): Cypress.Chainable<string>;
/** Visits heroes or villains routes, uses stubbed network, verifies path */
visitStubbedEntities(entityRoute: EntityRoute): Cypress.Chainable<string>;
/**
* Gets an entity by name.
* ```js
* cy.getEntityByName(newHero.name).then(myHero => ...)
* ```
* @param name: Hero['name']
*/
getEntityByProperty(
entityType: EntityType,
property: HeroProperty | VillainProperty
): Cypress.Chainable<Hero | Villain | Boy>;
/**
* Given a hero property (name, description or id),
* returns the index of the hero, and the entire collection, as an object.
*/
findEntityIndex(
entityType: EntityType,
property: HeroProperty
): Cypress.Chainable<{
entityIndex: number;
entityArray: Hero[] | Villain[] | Boy[];
}>;
/**
* Performs crud operations GET, POST, PUT and DELETE.
*
* `body` and `allowedToFail are optional.
*
* If they are not passed in, body is empty but `allowedToFail` still is `false`.
*
* If the body is passed in and the method is `POST` or `PUT`, the payload will be taken,
* otherwise undefined for `GET` and `DELETE`.
* @param method
* @param route
* @param options: {body?: Hero | object; allowedToFail?: boolean}
*/
crud(
method: "GET" | "POST" | "PUT" | "DELETE",
route: string,
{
body,
allowedToFail = false,
}: { body?: Hero | object; allowedToFail?: boolean } = {}
): Cypress.Chainable<Response<Hero[] & Hero>>;
/**
* Resets the data in the database to the initial data.
*/
resetData(): Cypress.Chainable<
Response<(Hero[] & Hero) | (Villain[] & Villain) | (Boy[] & Boy)>
>;
}
}
}
Kahramanların boys aynasını oluşturun
./db.json
'i güncelleyin
./db.json
'i güncelleyinboys
grubunu ekleyin.
{
"heroes": [
{
"id": "HeroAslaug",
"name": "Aslaug",
"description": "warrior queen"
},
{
"id": "HeroBjorn",
"name": "Bjorn Ironside",
"description": "king of 9th century Sweden"
},
{
"id": "HeroIvar",
"name": "Ivar the Boneless",
"description": "commander of the Great Heathen Army"
},
{
"id": "HeroLagertha",
"name": "Lagertha the Shieldmaiden",
"description": "aka Hlaðgerðr"
},
{
"id": "HeroRagnar",
"name": "Ragnar Lothbrok",
"description": "aka Ragnar Sigurdsson"
},
{
"id": "HeroThora",
"name": "Thora Town-hart",
"description": "daughter of Earl Herrauðr of Götaland"
}
],
"villains": [
{
"id": "VillainMadelyn",
"name": "Madelyn",
"description": "the cat whisperer"
},
{
"id": "VillainHaley",
"name": "Haley",
"description": "pen wielder"
},
{
"id": "VillainElla",
"name": "Ella",
"description": "fashionista"
},
{
"id": "VillainLandon",
"name": "Landon",
"description": "Mandalorian mauler"
}
],
"boys": [
{
"id": "BoyHomelander",
"name": "Homelander",
"description": "Like Superman, but a jerk."
},
{
"id": "BoyAnnieJanuary",
"name": "Annie January",
"description": "The Defender of Des Moines."
},
{
"id": "BoyBillyButcher",
"name": "Billy Butcher",
"description": "A former member of the British special forces turned vigilante."
},
{
"id": "BoyBlackNoir",
"name": "Black Noir",
"description": "Master Martial Artist, expert hand-to-hand combatant highly trained in various forms of martial arts."
}
]
}
Veritabanı durumunu doğru şekilde sıfırlayabilmemiz için bunu ./cypress/fixtures/db.json
'a yansıtın.
{
"heroes": [
{
"id": "HeroAslaug",
"name": "Aslaug",
"description": "warrior queen"
},
{
"id": "HeroBjorn",
"name": "Bjorn Ironside",
"description": "king of 9th century Sweden"
},
{
"id": "HeroIvar",
"name": "Ivar the Boneless",
"description": "commander of the Great Heathen Army"
},
{
"id": "HeroLagertha",
"name": "Lagertha the Shieldmaiden",
"description": "aka Hlaðgerðr"
},
{
"id": "HeroRagnar",
"name": "Ragnar Lothbrok",
"description": "aka Ragnar Sigurdsson"
},
{
"id": "HeroThora",
"name": "Thora Town-hart",
"description": "daughter of Earl Herrauðr of Götaland"
}
],
"villains": [
{
"id": "VillainMadelyn",
"name": "Madelyn",
"description": "the cat whisperer"
},
{
"id": "VillainHaley",
"name": "Haley",
"description": "pen wielder"
},
{
"id": "VillainElla",
"name": "Ella",
"description": "fashionista"
},
{
"id": "VillainLandon",
"name": "Landon",
"description": "Mandalorian mauler"
}
],
"boys": [
{
"id": "BoyHomelander",
"name": "Homelander",
"description": "Like Superman, but a jerk."
},
{
"id": "BoyAnnieJanuary",
"name": "Annie January",
"description": "The Defender of Des Moines."
},
{
"id": "BoyBillyButcher",
"name": "Billy Butcher",
"description": "A former member of the British special forces turned vigilante."
},
{
"id": "BoyBlackNoir",
"name": "Black Noir",
"description": "Master Martial Artist, expert hand-to-hand combatant highly trained in various forms of martial arts."
}
]
}
./cypress/fixtures/boys.json
eklentisini ekleyin.
./cypress/fixtures/boys.json
eklentisini ekleyin.[
{
"id": "BoyHomelander",
"name": "Homelander",
"description": "Like Superman, but a jerk."
},
{
"id": "BoyAnnieJanuary",
"name": "Annie January",
"description": "The Defender of Des Moines."
},
{
"id": "BoyBillyButcher",
"name": "Billy Butcher",
"description": "A former member of the British special forces turned vigilante."
},
{
"id": "BoyBlackNoir",
"name": "Black Noir",
"description": "Master Martial Artist, expert hand-to-hand combatant highly trained in various forms of martial arts."
}
]
Bileşenleri yansıtın
Kahramanlar grubunun olduğu gibi boys için 3 bileşen oluşturuyoruz. Ayrıca bazı temel bileşenleri güncellememiz gerekiyor.
ListHeader
için yalnızca title
için tip değişir.
// src/components/ListHeader.tsx
import { MouseEvent } from "react";
import { NavLink } from "react-router-dom";
import { FiRefreshCcw } from "react-icons/fi";
import { GrAdd } from "react-icons/gr";
type ListHeaderProps = {
title: "Heroes" | "Villains" | "Boys" | "About";
handleAdd: (e: MouseEvent<HTMLButtonElement>) => void;
handleRefresh: (e: MouseEvent<HTMLButtonElement>) => void;
};
export default function ListHeader({
title,
handleAdd,
handleRefresh,
}: ListHeaderProps) {
return (
<div data-cy="list-header" className="content-title-group">
<NavLink data-cy="title" to={title}>
<h2>{title}</h2>
</NavLink>
<button data-cy="add-button" onClick={handleAdd} aria-label="add">
<GrAdd />
</button>
<button
data-cy="refresh-button"
onClick={handleRefresh}
aria-label="refresh"
>
<FiRefreshCcw />
</button>
</div>
);
}
Diğerleri aynalardır. ./src/boys/
altında bir klasör ve onun altında 3 ayna dosyası oluşturun.
// src/boys/BoyDetail.tsx
import { useState, ChangeEvent } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { FaUndo, FaRegSave } from "react-icons/fa";
import InputDetail from "components/InputDetail";
import ButtonFooter from "components/ButtonFooter";
import PageSpinner from "components/PageSpinner";
import ErrorComp from "components/ErrorComp";
import { useEntityParams } from "hooks/useEntityParams";
import { usePostEntity } from "hooks/usePostEntity";
import { Boy } from "models/Boy";
import { usePutEntity } from "hooks/usePutEntity";
export default function BoyDetail() {
const { id } = useParams();
const { name, description } = useEntityParams();
const [boy, setBoy] = useState({ id, name, description });
const { mutate: createBoy, status, error: postError } = usePostEntity("boy");
const {
updateEntity: updateBoy,
isUpdating,
isUpdateError,
} = usePutEntity("boy");
const navigate = useNavigate();
const handleCancel = () => navigate("/boys");
const handleSave = () =>
name ? updateBoy(boy as Boy) : createBoy(boy as Boy);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setBoy({ ...boy, name: e.target.value });
};
const handleDescriptionChange = (e: ChangeEvent<HTMLInputElement>) => {
setBoy({ ...boy, description: e.target.value });
};
if (status === "loading" || isUpdating) {
return <PageSpinner />;
}
if (postError || isUpdateError) {
return <ErrorComp />;
}
return (
<div data-cy="boy-detail" className="card edit-detail">
<header className="card-header">
<p className="card-header-title">{name}</p>
</header>
<div className="card-content">
<div className="content">
{id && (
<InputDetail name={"id"} value={id} readOnly={true}></InputDetail>
)}
<InputDetail
name={"name"}
value={name ? name : ""}
placeholder="e.g. Colleen"
onChange={handleNameChange}
></InputDetail>
<InputDetail
name={"description"}
value={description ? description : ""}
placeholder="e.g. dance fight!"
onChange={handleDescriptionChange}
></InputDetail>
</div>
</div>
<footer className="card-footer">
<ButtonFooter
label="Cancel"
IconClass={FaUndo}
onClick={handleCancel}
/>
<ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
</footer>
</div>
);
}
// src/boys/BoyList.tsx
import { useNavigate } from "react-router-dom";
import CardContent from "components/CardContent";
import ButtonFooter from "components/ButtonFooter";
import { FaEdit, FaRegSave } from "react-icons/fa";
import {
ChangeEvent,
MouseEvent,
useTransition,
useEffect,
useState,
useDeferredValue,
} from "react";
import { Boy } from "models/Boy";
import { BoyProperty } from "models/types";
type BoyListProps = {
boys: Boy[];
handleDeleteBoy: (boy: Boy) => (e: MouseEvent<HTMLButtonElement>) => void;
};
export default function BoyList({ boys, handleDeleteBoy }: BoyListProps) {
const deferredBoys = useDeferredValue(boys);
const isStale = deferredBoys !== boys;
const [filteredBoys, setFilteredBoys] = useState(deferredBoys);
const navigate = useNavigate();
const [isPending, startTransition] = useTransition();
// needed to refresh the list after deleting a boy
useEffect(() => setFilteredBoys(deferredBoys), [deferredBoys]);
// currying: the outer fn takes our custom arg and returns a fn that takes the event
const handleSelectBoy = (boyId: string) => () => {
const boy = deferredBoys.find((b: Boy) => b.id === boyId);
navigate(
`/boys/edit-boy/${boy?.id}?name=${boy?.name}&description=${boy?.description}`
);
};
/** returns a boolean whether the boy properties exist in the search field */
const searchExists = (searchProperty: BoyProperty, searchField: string) =>
String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==
-1;
/** given the data and the search field, returns the data in which the search field exists */
const searchProperties = (searchField: string, data: Boy[]) =>
[...data].filter((item: Boy) =>
Object.values(item).find((property: BoyProperty) =>
searchExists(property, searchField)
)
);
/** filters the boys data to see if the any of the properties exist in the list */
const handleSearch =
(data: Boy[]) => (event: ChangeEvent<HTMLInputElement>) => {
const searchField = event.target.value;
return startTransition(() =>
setFilteredBoys(searchProperties(searchField, data))
);
};
return (
<div
style={{
opacity: isPending ? 0.5 : 1,
color: isStale ? "dimgray" : "black",
}}
>
{deferredBoys.length > 0 && (
<div className="card-content">
<span>Search </span>
<input data-cy="search" onChange={handleSearch(deferredBoys)} />
</div>
)}
<ul data-cy="boy-list" className="list">
{filteredBoys.map((boy, index) => (
<li data-cy={`boy-list-item-${index}`} key={boy.id}>
<div className="card">
<CardContent name={boy.name} description={boy.description} />
<footer className="card-footer">
<ButtonFooter
label="Delete"
IconClass={FaRegSave}
onClick={handleDeleteBoy(boy)}
/>
<ButtonFooter
label="Edit"
IconClass={FaEdit}
onClick={handleSelectBoy(boy.id)}
/>
</footer>
</div>
</li>
))}
</ul>
</div>
);
}
// src/boys/Boys.tsx
import { useState } from "react";
import { useNavigate, Routes, Route } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import ErrorComp from "components/ErrorComp";
import BoyList from "./BoyList";
import BoyDetail from "./BoyDetail";
import { useGetEntities } from "hooks/useGetEntities";
import { useDeleteEntity } from "hooks/useDeleteEntity";
import { Boy } from "models/Boy";
export default function Boys() {
const [showModal, setShowModal] = useState<boolean>(false);
const { entities: boys, getError } = useGetEntities("boys");
const [boyToDelete, setBoyToDelete] = useState<Boy | null>(null);
const { deleteEntity: deleteBoy, isDeleteError } = useDeleteEntity("boy");
const navigate = useNavigate();
const addNewBoy = () => navigate("/boys/add-boy");
const handleRefresh = () => navigate("/boys");
const handleCloseModal = () => {
setBoyToDelete(null);
setShowModal(false);
};
// currying: the outer fn takes our custom arg and returns a fn that takes the event
const handleDeleteBoy = (boy: Boy) => () => {
setBoyToDelete(boy);
setShowModal(true);
};
const handleDeleteFromModal = () => {
boyToDelete ? deleteBoy(boyToDelete) : null;
setShowModal(false);
};
if (getError || isDeleteError) {
return <ErrorComp />;
}
return (
<div data-cy="boys">
<ListHeader
title="Boys"
handleAdd={addNewBoy}
handleRefresh={handleRefresh}
/>
<div>
<div>
<Routes>
<Route
path=""
element={
<BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />
}
/>
<Route path="/add-boy" element={<BoyDetail />} />
<Route path="/edit-boy/:id" element={<BoyDetail />} />
<Route
path="*"
element={
<BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />
}
/>
</Routes>
</div>
</div>
{showModal && (
<ModalYesNo
message="Would you like to delete the boy?"
onNo={handleCloseModal}
onYes={handleDeleteFromModal}
/>
)}
</div>
);
}
Boys rotasını App.tsx
'e ekleyin.
// src/App.tsx
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { ErrorBoundary } from "react-error-boundary";
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import PageSpinner from "components/PageSpinner";
import ErrorComp from "components/ErrorComp";
import Villains from "villains/Villains";
import Boys from "boys/Boys";
import "./styles.scss";
const Heroes = lazy(() => import("heroes/Heroes"));
const NotFound = lazy(() => import("components/NotFound"));
const About = lazy(() => import("About"));
const queryClient = new QueryClient();
export default function App() {
return (
<BrowserRouter>
<HeaderBar />
<div className="section columns">
<NavBar />
<main className