Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/pokemon-battleground.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/pokemon-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
import {
TPokemonTypesApiResponse,
usePokedex
} from '@shared/hooks/usePokedex';
import classNames from 'classnames';
import { FormEvent } from 'react';

// 🧑🏻‍💻 1.f: we need to pass a prop called onPokemonTypesUpdate which will take a string[] setup the interface and suppky the parameter to the Form component.

export const Form = () => {
// 🧑🏻‍💻 1.c: Setup a useState<string[]> state colocation variable called selectedPokemonTypes, setSelectedPokemonTypes which will have a default of []

// ✍🏻 This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex
const { data, isLoading } = usePokedex<TPokemonTypesApiResponse[]>({
path: 'types',
queryParams: 'pageSize=8'
});

const handleSubmit = (event: FormEvent) => {
event.preventDefault();

// 🧑🏻‍💻 1.g: now we want to validate whether the selectedPokemonTypes have 4 items in the array before we call onPokemonTypesUpdate(selectedPokemonTypes).

// Once completed, head over to Screen.tsx as the Form component will be complaining about a missing prop.
};

const onPokemonTypeSelection = (type: string) => {
// 💣 We can get rid of this line once we start using the type param.
console.log(type);
// 🧑🏻‍💻 1.e: we need to check IF the selectedPokemonTypes already has the selectedType
// because we need to toggle it on and off. If it is selected, we just setSelectedPokemonTypes with the filtered out type
// if it's not in there then we set the type [...selectedPokemonTypes, type];
};

return (
<section className="text-center">
<h2 className="text-3xl font-bold mb-4 block text-yellow-400">
Select you favorite pokemon types (max 4)
</h2>
<form onSubmit={handleSubmit} noValidate>
{isLoading && (
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
<Skeleton height="h-[48px]" />
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
{data &&
data.length &&
data.map((pokemonType) => {
const isSelected =
// 🧑🏻‍💻 1.d: replace the empty array with the selectedPokemonTypes state variable.
// 💣 We can remove these ignores when we apply the code
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
[].includes(pokemonType);
const hasSelectedAllOptions = [].length === 4;

return (
<label
key={pokemonType}
className={classNames(
'bg-white p-3 font-bold rounded-md cursor-pointer relative',
!hasSelectedAllOptions &&
!isSelected &&
'hover:bg-slate-200 focus-within:bg-slate-200',
isSelected &&
'bg-blue-600 text-white focus-within:bg-blue-700 hover:bg-blue-700',
hasSelectedAllOptions &&
!isSelected &&
'opacity-80 cursor-not-allowed'
)}
>
{pokemonType}
<input
type="checkbox"
className="overflow-hidden h-0 w-0 absolute right-0 top-0 -z-10"
id={pokemonType}
name={pokemonType}
value={pokemonType}
onChange={() =>
onPokemonTypeSelection(pokemonType)
}
disabled={hasSelectedAllOptions && !isSelected}
/>
</label>
);
})}
</div>

<button
type="submit"
className="mt-6 rounded-sm py-3 px-12 text-white font-bold bg-blue-900 hover:bg-blue-950 focus-within:bg-blue-950"
>
Catch them all
</button>
</form>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
import {
TPokemonCardsApiResponse,
usePokedex
} from '@shared/hooks/usePokedex';
import classNames from 'classnames';
import { FormEvent } from 'react';

interface IPokemonOptions {
type: string;
// 🧑🏻‍💻 2.h: Add two new props called onPokemonSelection which takes a string[] and string as params and another
// variable called defaultSelectedPokemon which is an optional string[].
}

export const PokemonOptions = ({ type }: IPokemonOptions) => {
// 🧑🏻‍💻 2.d: Create a selectedPokemon useState<string[]> variable. Default value to be []

// 🧑🏻‍💻 2.i: Replace the default of selectedPokemon from [] to the defaultSelectedPokemon. This will remember which cards were selected when the component re-renders.

// ✍🏻 This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex
const { data, isLoading, isError } = usePokedex<
TPokemonCardsApiResponse[]
>({
path: 'cards',
queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`,
skip: type === undefined
});

const handleSubmit = (event: FormEvent) => {
event.preventDefault();

// 🧑🏻‍💻 2.j: call onPokemonSelection(selectedPokemon, type);
};

const togglePokemonSelection = (pokemonId: string) => {
// 💣 We can get rid of this line once we start using the type param.
console.log(pokemonId);
// 🧑🏻‍💻 2.g: We need to now update the state for when a pokemon card is selected or not.
// IF selectedPokemon includes the pokemonId then we need to de-select that pokemon card.
// ELSE we add the pokemon id to the array of pokemon cards [...selectedPokemon, pokemonId] and then setSelectedPokemon(newValues) (make this a variable as we will need it for later.)
// You should now start to see the pokemon being selected and de-selected. But the next thing we need to do is update the state within the screen. Search for 2.h
// 🧑🏻‍💻 2.k: Inside the ELSE, check if the newlySelectedPokemon has the length of 2. IF it does, call onPokemonSelection(newlySelectedPokemon, type);. Head over to the Screen.tsx component to finish it off.
};

return (
<section className="mt-8">
<h2 className="text-[32px] mb-4 font-bold block text-yellow-400 text-shadow-lg text-shadow-blue-600">
{type} Pokemon
</h2>
<form onSubmit={handleSubmit} noValidate>
<fieldset>
{isLoading && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-1 max-w-[768px]">
<Skeleton height="h-[207px]" width="w-full" />
<Skeleton height="h-[207px]" width="w-full" />
<Skeleton height="h-[207px]" width="w-full" />
<Skeleton height="h-[207px]" width="w-full" />
</div>
)}
<div
className={classNames(
'grid grid-cols-2 md:grid-cols-4 gap-1 max-w-[768px] transition-opacity',
isLoading ? 'opacity-0' : 'opacity-100'
)}
>
{data &&
data.length > 0 &&
data.map((pokemonCard) => {
// 🧑🏻‍💻 2.e: Replace the empty arrays with the selectedPokemon variable we created.
const isSelected = [].find(
(pokemonId) => pokemonId === pokemonCard.id
);
const allPokemonSelected = [].length === 2;

return (
<label
key={pokemonCard.id}
className={classNames(
'border-solid border-[6px] focus-within:border-blue-600 rounded-lg relative',
isSelected
? 'border-yellow-600'
: 'border-transparent',
!isSelected && allPokemonSelected
? 'cursor-default'
: 'cursor-pointer'
)}
>
<img
src={pokemonCard.images.small}
alt={pokemonCard.name}
className={classNames(
!isSelected && allPokemonSelected
? 'opacity-70'
: 'opacity-100'
)}
/>
<input
type="checkbox"
value={pokemonCard.id}
// 🧑🏻‍💻 2.f: Replace the empty array with the selectedPokemon variable we created.
checked={[].includes(
// 💣 We can get ride of this one we replace the empty array.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
pokemonCard.id
)}
onChange={() =>
togglePokemonSelection(pokemonCard.id)
}
className="overflow-hidden h-0 w-0 absolute right-0 top-0 -z-10"
disabled={!isSelected && allPokemonSelected}
/>
</label>
);
})}
</div>

<button
type="submit"
className="hidden"
disabled={isLoading || isError}
>
Select Pokemon
</button>
</fieldset>
</form>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Exercise: 🧼 State Colocation vs 🪜 State Lifting
*
* 🤔 Observations of this file
* So the lead developer wanted us to manage the state for the selected pokemon types & the selected pokemon at this level so those variables can be used here and within the children. Each variable will be an array of strings which represent the type or the id of the selected pokemon.
*
* We need to tackle this in stages...
*
* Stage one (follow 1.* steps) - Creating the form that returns the pokemon types and saving those selected types to the lifted state variable
* Stage two (following 2.* steps) - Using those types, we will render the pokemon options component
*
*/

export const Screen = () => {
// 🧑🏻‍💻 1.a: Create a useState<string[]> variable called selectedPokemonTypes, setSelectedPokemonTypes. Default to be an empty array.

// 🧑🏻‍💻 2.a: Create a useState<Record<string, string[]>> variable called selectedPokemon, setSelectedPokemon. Default to be an object {}.

// 🧑🏻‍💻 1.h: Create a function called onPokemonTypesUpdate which will take a types: string[] param. Pass that into the Form component as a prop. The function will just need setSelectedPokemonTypes for now.
// 🎉 STAGE ONE COMPLETED you should now be able to see the types display, select them and then the state in the screen gets updated.

// 🧑🏻‍💻 2.m: You will now start to see the happy path all working fine, however when you change to different pokemon types and receive a new set of pokemon you now get some messy state where selectedPokemon is more than 8. To fix this, write the following code inside 1.h function (before the setSelectedPokemonTypes)
/*
const newlyUpdatedPokemon = { ...selectedPokemon };

selectedPokemonTypes
.filter((type) => {
return !types.find((selectedType) => selectedType === type);
})
.forEach((type) => {
delete newlyUpdatedPokemon[type];
});

setSelectedPokemon(newlyUpdatedPokemon);
*/
// STAGE TWO completed. You have now built the screen. BUT 🐞 there is a bug where the PokemonOptions re-renders the types that did not need to update when you change your types after one try. The reason is the "key" prop using index. The api has no identifier per type. If you enjoyed this exercise have a look into fixing it and make a pr.

// 🧑🏻‍💻 2.l: Create a function called onPokemonSelection which will take a pokemon: string[], type: string
// Then create a newlySelectedPokemon variable which will be a copy of the current selectedPokemon {...selectedPokemon}
// assign the newlySelectedPokemon[type] to equal the pokemon variable.
// setSelectedPokemon(newlySelectedPokemon);

return (
<main className="h-screen p-6">
<img
src="/pokemon-battleground.webp"
alt="hello"
className="fixed will-change-scroll left-0 right-0 top-0 bottom-0 z-0 h-full w-full object-cover"
/>
<div className="fixed will-change-scroll left-0 right-0 top-0 bottom-0 z-10 h-full w-full bg-black opacity-25" />
<div className="relative z-20 max-w-[768px] mx-auto text-center">
<div>
<h1 className="text-center">
<img
src="/pokemon-logo.png"
alt="Pokemon Battle Picker"
className="inline-block w-96"
/>
<span className="text-[76px] font-bold mb-4 block text-yellow-400 text-shadow-lg text-shadow-blue-600">
Battle Picker
</span>
</h1>
</div>
<div>
{/* 🧑🏻‍💻 1.b: Render pokemon types form from ./components/Form and then head over to the form component */}
</div>
<div>
{/* 🧑🏻‍💻 2.b: Loop through the selectedPokemonTypes and pass down the type property to the PokemonOptions (./components/PokemonOptions) component. You will also need a key to be on the component. I used `${pokemonType}-${index}` */}
</div>

{/* 🧑🏻‍💻 2.c: We need to check if the KEYS in the selectedPokemon object equal 4 and the selectedPokemonTypes length is 4 before rendering the code snippet below. Head over to PokemonOptions when completed. */}
{/*
<button
type="button"
className="my-12 rounded-lg py-6 px-16 text-white text-2xl font-bold bg-blue-900 hover:bg-blue-950 focus-within:bg-blue-950"
onClick={() => alert('Ready for battle!')}
>
Begin Battle
</button>
*/}
</div>
</main>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Exercise } from './exercise';

const meta: Meta<typeof Exercise> = {
title:
'Lessons/🥉 Bronze/🧼 State Colocation vs 🪜 State Lifting/02-Exercise',
component: Exercise,
parameters: {
layout: 'fullscreen'
}
};

export default meta;
type Story = StoryObj<typeof Exercise>;

/*
* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const Default: Story = {
play: async () => {},
args: {}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Screen } from './components/Screen';

// Head over to screen to get started.
export const Exercise = () => <Screen />;
Loading
Loading