ساخت صفحه انتخاب کاراکتر در React (بخش اول) — از صفر تا صد
React یک کتابخانه جاوا اسکریپت است که به طور عمده به جهت کمک به ساخت رابطهای کاربری عالی در عین حفظ سادگی مشهور شده است. وبسایتهای مشهور و بزرگی مانند فیسبوک، نتفلیکس و نیویورکتایمز وباپلیکیشنهای خود را با استفاده از React طراحی کرده و نگهداری میکنند. این کتابخانه به لطف مشارکت متن-باز زیادی که در آن وجود دارد، روز به روز بهبود زیادی پیدا میکند. در این مقاله مراحل ساخت یک صفحه انتخاب کاراکتر مقدماتی را با بهرهگیری از قابلیتهای React بررسی میکنیم. در این فرایند با روش حل برخی مسائل آشنا میشویم و امیدواریم این راهنما برای توسعهدهندگان تازهکار React مفید باشد. در نهایت صفحهای مانند زیر به دست خواهیم آورد:
با مراجعه به این ریپوی گیتهاب (+) میتوانید کد کامل بررسی شده در این راهنما را به همراه کاراکترهای بیشتر ملاحظه کنید.
ما برای ساخت جلوههای موجود در مثال این مقاله از هیچ کتابخانه UI استفاده نکردهایم و صرفاً یک اسپینر بارگذاری نصب کردهایم. دلیل این کار آن است که هر کس بتواند این راهنما را مطالعه کرده و نیازی به مطالعه API کتابخانهها برای درک مثالهای این راهنما نداشته باشد.
در این راهنما قصد داریم یک پروژه React را به سرعت با دستور create-react-app ایجاد کنیم. بنابراین در ادامه با دستور زیر یک پروژه میسازیم. ما پروژه خود را در این راهنما character-select نامیدهایم.
npx create-react-app character-select
در نهایت وارد دایرکتوری مربوطه میشویم:
cd character-select
درون مدخل اصلی src/index.js کمی تمیزکاری میکنیم:
1import React from 'react'
2import ReactDOM from 'react-dom'
3import * as serviceWorker from './serviceWorker'
4import App from './App'
5ReactDOM.render(<App />, document.getElementById('root'))
6serviceWorker.unregister()
استایلهای آغازین به صورت زیر هستند:
فایل src/styles.css
1body {
2 margin: 0;
3 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
4 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
5 'Helvetica Neue', sans-serif;
6 -webkit-font-smoothing: antialiased;
7 -moz-osx-font-smoothing: grayscale;
8 background: rgb(23, 30, 34);
9}
10code {
11 font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 monospace;
13}
14.root {
15 padding: 20px 0;
16}
اکنون به فایل src/App.js میرویم و از عنصر root آغاز میکنیم، زیرا استایل ها را از قبل تعریف کردهایم:
1import React from 'react'
2import styles from './styles.module.css'
3
4const App = () => <div className={styles.root}>{null}</div>
5
6export default App
پیش از آن که آغاز کنیم ابتدا گردش کار این صفحه انتخاب کاراکتر را توضیح میدهیم، بدین ترتیب مطمئن میشویم که همگی درک درستی از آن یافتهایم.
فرض کنید ما مشغول یک بازی MMORPG هستیم. همه بازیکنان با ایجاد یک کاراکتر آغاز میکنند. هر بازیکن به صورت پیشفرض با کلاس مبتدی (Novice) آغاز میکند. زمانی که به سطح 10 رسید، میتواند به کلاس دوم وارد شود. در این راهنما ما صرفاً کلاسهای Sorceress و Knight را داریم، اما در بازیهای MMORPG به طور معمول کلاسهای دیگری مانند Archer و Necromancer نیز وجود دارند.
زمانی که بازیکن به سطح 10 برسد و یک کلاس انتخاب کند، روی یک دکمه به نام Morph کلیک میکند و ظاهر کاراکتر وی بر همین اساس تغییر مییابد. این اقدامات به صورت خودکار موجب اسکرول به پایین صفحه میشود تا همه چیز تعاملی به نظر بیاید. در نهایت بازیکن به صفحه جدیدی هدایت میشود.
ابتدا کار خود را با ارائه صفحهای آغاز میکنیم که به بازیکنان خوشامدگویی میکند و سپس دو کلاس مختلف که میتوانید کاراکترشان را به آنها تبدیل کنند را نمایش میدهیم.
اگر بازیکن کلاس sorceress را انتخاب کند، میتواند به این نوع تبدیل شود و مسیر خود را برای تبدیل شدن به یک بازیکن بهتر در بازی ادامه دهد. همین مفهوم در مورد گزینه Knight نیز صدق میکند. اما یک گزینه مخفی نیز وجود دارد. اگر بازیکن به قدر کافی هوشمند و زیرک باشد، متوجه میشود که میتواند هر دو کلاس را انتخاب کند و به کاراکتری خارقالعاده تبدیل شود. زندگی پر از شگفتی است و بازی نیز باید بازتابی از زندگی باشد تا واقعگرایانه به نظر برسد.
ما برای عرضه صفحه قصد داریم بازیکنان را با یک هدر، یک تصویر از کلاس مبتدی (چون در ادامه هرگز آن را نخواهند دید) و یک پیام دلگرمکننده مواجه کنیم که جبرانی برای تلاش سختشان در بازی باشد.
فایل src/App.js
1import React from 'react'
2import noviceImg from './resources/novice.jpg'
3import styles from './styles.module.css'
4
5const App = () => (
6 <div className={styles.root}>
7 <h1 className={styles.header}>
8 You are a <em>Novice</em>
9 </h1>
10 <div className={styles.content}>
11 <div className={styles.characterBox} style={{ width: 200, height: 150 }}>
12 <img alt="" src={noviceImg} />
13 </div>
14 </div>
15 <small className={styles.subheader}>
16 Congratulations on reaching level 10!
17 </small>
18 </div>
19)
20
21export default App
موارد زیر را نیز به فایل styles.css اضافه کنید:
1.content {
2 display: flex;
3 justify-content: center;
4}
5
6.header {
7 text-align: center;
8 color: rgb(252, 216, 169);
9 font-weight: 300;
10 margin: 0;
11}
12
13.subheader {
14 color: #fff;
15 text-align: center;
16 font-weight: 300;
17 width: 100%;
18 display: block;
19}
20
21.characterBox {
22 transition: all 0.1s ease-out;
23 width: 300px;
24 height: 250px;
25 border: 1px solid rgb(194, 5, 115);
26 background: rgb(82, 26, 134);
27 margin: 12px 6px;
28 overflow: hidden;
29}
30
31.characterBox img {
32 width: 100%;
33 height: 100%;
34 object-fit: cover;
35 cursor: pointer;
36}
اینک اگر به کامپوننت نگاه کنید، میبینید که عنصر root شامل یک هدر، یک کانتینر محتوا و یک هدر فرعی به عنوان فرزند بیواسطه است. اشاره کردیم که قصد داریم تصویری از یک مبتدی به کاربر نمایش دهیم. این همان کاری است که درون عنصر div با کلاسی به نام styles.content انجام میشود:
ما نماهای کلاس CSS را برای هدر و هدر فرعی تعریف کردهایم زیرا حس کردیم که این موارد ممکن است در ادامه در اینترفیسهای دیگر مانند زمانی که بازیکن به یک بخش جدید هدایت میشود، مورد استفاده مجدد قرار گیرند. منظور ما از کلمه «بخش» چیزی است که شامل هدر و یک بدنه است و از این رو حرکت مجازی به شمار میرود. همچنین میتوانیم از «کادر کاراکتر» (character box) برای کاراکترهای دیگر مانند mages یا مواردی دیگر استفاده کنیم و از این رو نام کلاس characterBox. را نیز برای نگهداری موارد مرتبط در ادامه تعریف کردیم.
اکنون که سادهترین بخش راهنما را تنظیم کردیم، میتوانیم به بخشهای جذابتر برسیم.
کار بعدی که باید انجام دهیم این است که صفحههای options یا selection را بسازیم. این صفحه مسئول نمایش دادن بخش انتخاب کلاس کاراکتر به بازیکن است. این کاراکترها به نام Sorceress و Knight نامیده میشوند. این همان صفحهای است که بازیکنان به محض رسیدن به سطح 10 مشاهده میکنند.
برای اجرای مؤثر این کار باید در مورد هدف گام بعدی برای بازیکن فکر کنیم. هدف این صفحه برای بازیکنها آن است که ین یک کاراکتر Sorceress و Knight انتخاب کنند، از این رو داشتن یک کادر انتخاب کاراکتر و عرضه آن به بازیکن مناسب خواهد بود.
اکنون باید از ایجاد کامپوننتهای چندگانه که اساساً کار یکسانی را انجام میدهند تا حد امکان احتراز کنیم. انجام این کار رویه مناسبی محسوب نمیشود چون در آینده برای بازگشت به کد و تغییر دادن آن به هر دلیل احتمالی دچار مشکل خواهید شد. همچنین در زمان دیباگ کردن اشکالهای کوچک با مشکل مواجه میشوید.
با در نظر گرفتن موارد فوق، در section بعدی که به گزینههای انتخاب کاربر مربوط است، میتوانید آن را مانند بخش قبلی سازماندهی کنید، یعنی دارای هدر، محتوا/بدنه و هدر فرعی باشد.
ما از این مفهوم دوباره استفاده میکنیم تا صفحه انتخاب کاراکتر را با تجرید هدر، هدر فرعی و کانتینر (یا محتوا) ایجاد کنیم.
به طور معمول این کامپوننتهای با قابلیت استفاده مجدد را در فایلهای مجزایی قرار میدهیم و بدین ترتیب میتوانیم مستقیماً آنها را به صورت ماژولهای منفردی import کنیم. از طرف دیگر برای صرفهجویی در زمان و فضا آنها را در مسیر src/components.js قرار میدهیم.
در این بخش یک فایل به نام components.js در همان دایرکتوری ایجاد میکنیم و موارد با استفاده مجدد را به نام exports در آن تعریف میکنیم:
فایل src/components.js
1export const Header = ({ children, ...rest }) => (
2 // eslint-disable-next-line
3 <h1 className={styles.header} {...rest}>
4 {children}
5 </h1>
6)
7
8export const Subheader = ({ children, ...rest }) => (
9 <small className={styles.subheader} {...rest}>
10 {children}
11 </small>
12)
13
14export const Content = ({ children, ...rest }) => (
15 <div className={styles.content} {...rest}>
16 {children}
17 </div>
18)
اکنون کامپوننت App را با این کامپوننت جدید که قابلیت استفاده مجدد دارد تعویض میکنیم:
فایل src/App.js
1import React from 'react'
2import noviceImg from './resources/novice.jpg'
3import styles from './styles.module.css'
4import { Header, Subheader, Content } from './components'
5
6const App = () => (
7 <div className={styles.root}>
8 <Header>
9 You are a <em>Novice</em>
10 </Header>
11 <Content>
12 <div className={styles.characterBox} style={{ width: 200, height: 150 }}>
13 <img alt="" src={noviceImg} />
14 </div>
15 </Content>
16 <Subheader>Congratulations on reaching level 10!</Subheader>
17 </div>
18)
19
20export default App
چنان که میبینید کد اینک زیباتر شده است. بنابراین در ادامه کاری میکنیم که صفحه کلاسهای کاراکتر را نشان دهد:
فایل src/App.js
1import React from 'react'
2import noviceImg from './resources/novice.jpg'
3import sorceressImg from './resources/sorceress.jpg'
4import knightImg from './resources/knight.jpg'
5import styles from './styles.module.css'
6import { Header, Subheader, Content } from './components'
7
8const App = () => (
9 <div className={styles.root}>
10 <Header>
11 You are a <em>Novice</em>
12 </Header>
13 <Content>
14 <div className={styles.characterBox} style={{ width: 200, height: 150 }}>
15 <img alt="" src={noviceImg} />
16 </div>
17 </Content>
18 <Subheader>Congratulations on reaching level 10!</Subheader>
19 <div style={{ margin: '25px auto' }}>
20 <Header>Choose your destiny</Header>
21 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
22 <Content>
23 <div className={styles.characterBox}>
24 <h2>Sorceress</h2>
25 <img alt="" src={sorceressImg} />
26 </div>
27 <div className={styles.characterBox}>
28 <h2>Knight</h2>
29 <img alt="" src={knightImg} />
30 </div>
31 </Content>
32 </div>
33 </div>
34)
35
36export default App
در کد فوق یک مورد جدید به styles.module.css اضافه میکنیم:
1.characterBox h2 {
2 transition: all 0.3s ease-out;
3 text-align: center;
4 color: rgb(213, 202, 255);
5 font-style: italic;
6 font-weight: 500;
7}
با این تغییر، اینترفیس ما اینک به صورت زیر در میآید:
در کد میبینید که میتوانیم از Header ،Subheader و Content برای بخش بعدی نیز استفاده کنیم. اینک اینترفیس یکنواخت به نظر میرسد و مزیت عمدهای به دست آوردهایم: ما اکنون تنها کافی است کامپوننتهای هدر/هدر فرعی/ محتوا را به جای چند جای مختلف، صرفاً در یک جای منفرد تغییر دهیم.
برخی مزیتهای قابل توجه دیگر که از این رویکرد به دست میآید، «مستندسازی ضمنی» (implicit documentation) است. بدین ترتیب مطمئن میشویم که این کامپوننتهای هدر و هدر فرعی در مراجعات بعدی به راحتی قابل درک خواهند بود.
بخش بعدی که میخواهیم بسازیم این است که کادرهای Sorceress و Knight به محض کلیک شدن باید نوعی عمل اجرا کنند.
تا این جا صرفاً یک دستگیره onSelect بیاستفاده تعریف کردهایم، بنابراین نوعی structure تعریف میکنیم تا بتوانیم به طور ثابت یادآوری کنیم که برای استفادههای بعدی به نوعی اکشن کلیک نیاز داریم:
1const App = () => {
2 const onSelect = (e) => {
3 console.log("Don't mind me. I'm useless until I become useful")
4 }
5
6 return (
7 <div className={styles.root}>
8 <Header>
9 You are a <em>Novice</em>
10 </Header>
11 <Content>
12 <div
13 className={styles.characterBox}
14 style={{ width: 200, height: 150 }}
15 >
16 <img alt="" src={noviceImg} />
17 </div>
18 </Content>
19 <Subheader>Congratulations on reaching level 10!</Subheader>
20 <div style={{ margin: '25px auto' }}>
21 <Header>Choose your destiny</Header>
22 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
23 <Content>
24 <div onClick={onSelect} className={styles.characterBox}>
25 <h2>Sorceress</h2>
26 <img alt="" src={sorceressImg} />
27 </div>
28 <div onClick={onSelect} className={styles.characterBox}>
29 <h2>Knight</h2>
30 <img alt="" src={knightImg} />
31 </div>
32 </Content>
33 </div>
34 </div>
35 )
36}
اینک همه چیز عالی به نظر میرسد، اما بدون وجود یک نوع تغییر بصری، بازیکن هیچ راهی برای دانستن این که کدام کاراکتر انتخاب شده است، نخواهد داشت:
ابتدا با ایجاد یک جلوه hover آن را کمی تعاملیتر میسازیم که یک گام کوچک رو به جلو محسوب میشود، زیرا بسیار بهتر از تصویر ثابت است.
اکنون از آنجا که میخواهیم نامهای کلاسها را در هم ادغام کنیم تا چند جلوه به صورت موازی برای عناصر منفرد رخ بدهند، باید کتابخانه کارآمد classnames (+) را برای اجرای ادغام نصب کنیم:
npm install --save classnames
قدرت کتابخانه classnames زمانی رخ مینماید که بخواهید نامهای کلاس را بر مبنای شرایط خاصی اعمال کنید. ما به این امکان نیاز خواهیم داشت، زیرا عناصر خاصی باید در مواقع خاصی پنهان یا نمایان شوند.
اینک برخی استایلها را به عناصر کادر کاراکتر اضافه میکنیم:
1.characterBox:hover h2 {
2 color: rgb(191, 255, 241);
3}
4
5.characterBox img {
6 transition: all 0.3s ease-out;
7 width: 100%;
8 height: 100%;
9 object-fit: cover;
10 cursor: pointer;
11}
12
13.characterBox img.tier2:hover {
14 animation: hueRotate 2s infinite;
15 transform: scale(1.05);
16}
17
18@keyframes hueRotate {
19 0% {
20 filter: hue-rotate(0deg);
21 }
22 50% {
23 filter: hue-rotate(260deg) grayscale(100%);
24 }
25 100% {
26 filter: hue-rotate(0deg);
27 }
28}
زمانی که بازیکن روی این کادرها میرود، برای برجستهتر کردن آن، یک فیلتر hue-rotate که مداوماً تغییر مییابد اعمال میکنیم. بدین ترتیب بازیکن برای تبدیل شدن به کلاس دوم هیجان خواهد داشت.
در حال حاضر این جلوه hover هیچ کاری انجام نمیدهد، زیرا باید نامهای کلاس جدید را چنان که نمایش یافته در CSS اعمال کنیم. تنها کاری که باید انجام دهیم، اعمال خصوصیت className روی عناصر تصویر tier2 است.
فایل src/App.js
1<div style={{ margin: '25px auto' }}>
2 <Header>Choose your destiny</Header>
3 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
4 <Content>
5 <div onClick={onClick} className={styles.characterBox}>
6 <h2>Sorceress</h2>
7 <img alt="" src={sorceressImg} className={styles.tier2} />
8 </div>
9 <div onClick={onClick} className={styles.characterBox}>
10 <h2>Knight</h2>
11 <img alt="" src={knightImg} className={styles.tier2} />
12 </div>
13 </Content>
14</div>
اینک یک جلوه hover تغییر رنگ داریم:
اکنون که جلوه hover را داریم باید یک استایل برای مواردی که یکی از آنها انتخاب میشوند نیز داشته باشیم. این بدان معنی است که باید برخی استایلها زمانی که کاربر کاراکتری را انتخاب میکند حفظ شوند.
بدین ترتیب باید از قلابهای react استفاده کنیم. یک قلاب سفارشی به نام useLevelUpScreen درست بالای کامپوننت App میسازیم و حالت select را همراه با دستگیره onSelect برای بهروزرسانی آن تعریف میکنیم:
فایل src/App.js
1import React from 'react'
2import cx from 'classnames'
3import noviceImg from './resources/novice.jpg'
4import sorceressImg from './resources/sorceress.jpg'
5import knightImg from './resources/knight.jpg'
6import styles from './styles.module.css'
7import { Header, Subheader, Content } from './components'
8
9const useLevelUpScreen = () => {
10 const [selected, setSelected] = React.useState([])
11 const onSelect = (type) => (e) => {
12 setSelected((prevSelected) => {
13 if (prevSelected.includes(type)) {
14 return prevSelected.filter((t) => t !== type)
15 }
16 return [...prevSelected, type]
17 })
18 }
19 return {
20 selected,
21 onSelect,
22 }
23}
24
25const App = () => {
26 const { selected, onSelect } = useLevelUpScreen()
27 return (
28 <div className={styles.root}>
29 <Header>
30 You are a <em>Novice</em>
31 </Header>
32 <Content>
33 <div
34 className={styles.characterBox}
35 style={{ width: 200, height: 150 }}
36 >
37 <img alt="" src={noviceImg} />
38 </div>
39 </Content>
40 <Subheader>Congratulations on reaching level 10!</Subheader>
41 <div style={{ margin: '25px auto' }}>
42 <Header>Choose your destiny</Header>
43 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
44 <Content>
45 <div onClick={onSelect('Sorceress')} className={styles.characterBox}>
46 <h2>Sorceress</h2>
47 <img alt="" src={sorceressImg} className={styles.tier2} />
48 </div>
49 <div onClick={onSelect('Knight')} className={styles.characterBox}>
50 <h2>Knight</h2>
51 <img alt="" src={knightImg} className={styles.tier2} />
52 </div>
53 </Content>
54 </div>
55 </div>
56 )
57}
58
59export default App
درون useLevelUpScreen حالت selected را تعریف کردهایم که به ما کمک میکند تعیین کنیم بازیکن کدام کلاس tier2 را انتخاب کرده است. دستگیره onSelect همان API برای بهروزرسانی حالت محسوب میشود. این API از نسخه callback مربوط به useState جهت تضمین این که آخرین بهروزرسانی را در حالت selected دریافت کرده است بهره میجوید. درون callback بررسی میکند که آیا type (که در این مثال Knight یا Sorceress است) قبلاً انتخاب شده است یا نه. اگر چنین باشد فرض میکنیم که بازیکن تصمیم گرفته است، انتخاب خود را لغو کند و از این رو آن را برای بهروزرسانی بعدی حالت فیلتر میکنیم و یا برعکس.
سپس دستگیره onSelect را روی عناصری که در کامپوننت App به آن نیاز دارند اعمال میکنیم:
فایل src/App.js
1const App = () => {
2 const { selected, onSelect } = useLevelUpScreen()
3
4 return (
5 <div className={styles.root}>
6 <Header>
7 You are a <em>Novice</em>
8 </Header>
9 <Content>
10 <div
11 className={styles.characterBox}
12 style={{ width: 200, height: 150 }}
13 >
14 <img alt="" src={noviceImg} />
15 </div>
16 </Content>
17 <Subheader>Congratulations on reaching level 10!</Subheader>
18 <div style={{ margin: '25px auto' }}>
19 <Header>Choose your destiny</Header>
20 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
21 <Content>
22 <div onClick={onSelect('Sorceress')} className={styles.characterBox}>
23 <h2>Sorceress</h2>
24 <img alt="" src={sorceressImg} className={styles.tier2} />
25 </div>
26 <div onClick={onSelect('Knight')} className={styles.characterBox}>
27 <h2>Knight</h2>
28 <img alt="" src={knightImg} className={styles.tier2} />
29 </div>
30 </Content>
31 </div>
32 </div>
33 )
34}
اینک باید بهروزرسانیها را روی کاراکترهایی که انتخاب میشوند و یا نمیشوند دریافت کنیم. البته این حالت هنوز در رابط کاربری برای بازیکن بازتاب نمییابد، زیرا آن را هنوز روی کامپوننت اعمال نکردهایم، این کار را در بخش بعدی انجام میدهیم. در نخستین گام قصد داریم از پکیج classnames برای اعمال مشروط نام کلاس روی عناصر به صورت زیر استفاده کنیم.
فایل src/App.js
1const App = () => {
2 const { selected, onSelect } = useLevelUpScreen()
3
4 return (
5 <div className={styles.root}>
6 <Header>
7 You are a <em>Novice</em>
8 </Header>
9 <Content>
10 <div
11 className={styles.characterBox}
12 style={{ width: 200, height: 150 }}
13 >
14 <img alt="" src={noviceImg} />
15 </div>
16 </Content>
17 <Subheader>Congratulations on reaching level 10!</Subheader>
18 <div style={{ margin: '25px auto' }}>
19 <Header>Choose your destiny</Header>
20 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
21 <Content>
22 <div
23 onClick={onSelect('Sorceress')}
24 className={cx(styles.characterBox, {
25 [styles.selectedBox]: selected.includes('Sorceress'),
26 })}
27 >
28 <h2>Sorceress</h2>
29 <img
30 alt=""
31 src={sorceressImg}
32 className={cx(styles.tier2, {
33 [styles.selected]: selected.includes('Sorceress'),
34 })}
35 />
36 </div>
37 <div
38 onClick={onSelect('Knight')}
39 className={cx(styles.characterBox, {
40 [styles.selectedBox]: selected.includes('Knight'),
41 })}
42 >
43 <h2>Knight</h2>
44 <img
45 alt=""
46 src={knightImg}
47 className={cx(styles.tier2, {
48 [styles.selected]: selected.includes('Knight'),
49 })}
50 />
51 </div>
52 </Content>
53 </div>
54 </div>
55 )
56}
با افزودن این استایلهای جدید، باید استایلها را بهروزرسانی کنیم تا از آنها استفاده کنند.
فایل src/styles.css
1.selectedBox {
2 border: 1px solid rgb(24, 240, 255) !important;
3}
4.characterBox img.tier2:hover,
5.characterBox img.selected {
6 animation: hueRotate 2s infinite;
7 transform: scale(1.05);
8}
توجه کنید که .characterBox img.selected پس از خط hover و به عنوان همنیا الحاق یافته است.
با این تغییر اینک دو کادر انتخاب کاراکتر واکنشپذیر زیبا داریم:
در اوایل این راهنما اشاره کردیم که اگر بازیکن به قدر کافی زیرک و هوشیار باشد میتواند درک کند که امکان انتخاب همزمان هر دو تبدیل Sorceress و Knight وجود دارد تا با چیز غیرمنتظرهای مواجه شود که یک کاراکتر پنهان است. بدیهی است که میخواهیم نوعی عناصر UI در ا اختیار بازیکن قرار دهیم تا بتوانند کلاس مبتدی خود را به نیابت از آن تغییر دهند و به این منظور به یک button نیاز داریم.
به این ترتیب یک button ساده مینویسیم. یک دستگیره onMorph به آن الحاق میکنیم که قصد داریم به همراه اعمال styles.morph روی className دکمه ایجاد کنیم:
فایل src/App.js
1const App = () => {
2 const { selected, onSelect, morphed, onMorph } = useLevelUpScreen()
3
4 return (
5 <div className={styles.root}>
6 <Header>
7 You are a <em>Novice</em>
8 </Header>
9 <Content>
10 <div
11 className={styles.characterBox}
12 style={{ width: 200, height: 150 }}
13 >
14 <img alt="" src={noviceImg} />
15 </div>
16 </Content>
17 <Subheader>Congratulations on reaching level 10!</Subheader>
18 <div style={{ margin: '25px auto' }}>
19 <Header>Choose your destiny</Header>
20 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
21 <Content>
22 <div
23 onClick={onSelect('Sorceress')}
24 className={cx(styles.characterBox, {
25 [styles.selectedBox]: selected.includes('Sorceress'),
26 })}
27 >
28 <h2>Sorceress</h2>
29 <img
30 alt=""
31 src={sorceressImg}
32 className={cx(styles.tier2, {
33 [styles.selected]: selected.includes('Sorceress'),
34 })}
35 />
36 </div>
37 <div
38 onClick={onSelect('Knight')}
39 className={cx(styles.characterBox, {
40 [styles.selectedBox]: selected.includes('Knight'),
41 })}
42 >
43 <h2>Knight</h2>
44 <img
45 alt=""
46 src={knightImg}
47 className={cx(styles.tier2, {
48 [styles.selected]: selected.includes('Knight'),
49 })}
50 />
51 </div>
52 </Content>
53 </div>
54 <div className={styles.morph}>
55 <button
56 name="morph"
57 type="button"
58 className={styles.morph}
59 onClick={onMorph}
60 >
61 Morph
62 </button>
63 </div>
64 </div>
65 )
66}
اگر به آنچه از قلاب useLevelUpScreen بازمیگردد نگاه کنید، میبینید که دو مورد morphed و onMorph اضافه شدهاند. این موارد قرار است درون یک قلاب سفارشی تعریف شوند.
فایل src/useLevelUpScreen.js
1const useLevelUpScreen = () => {
2 const [selected, setSelected] = React.useState([])
3 const [morphed, setMorphed] = React.useState(false)
4
5 const onSelect = (type) => (e) => {
6 setSelected((prevSelected) => {
7 if (prevSelected.includes(type)) {
8 return prevSelected.filter((t) => t !== type)
9 }
10 return [...prevSelected, type]
11 })
12 }
13
14 const onMorph = () => {
15 setTimeout(() => {
16 setMorphed(true)
17 }, 1500) // simulating a real server / api call response time
18 }
19
20 return {
21 selected,
22 onSelect,
23 morphed,
24 onMorph,
25 }
26}
در این بخش استایل نام کلاس styles.morph به صورت زیر است.
فایل src/styles.module.css
1.morph {
2 margin: 50px auto;
3 text-align: center;
4}
نتیجه کار نیز تا به این جا چنین بوده است:
بهتر است دکمه morph را تا زمانی که بازیکن کاراکتری را انتخاب نکرده است پنهان کنیم تا تمرکز بازیکن روی انتخاب یک کلاس کاراکتر باشد. بنابراین یک جلوه پنهان روی آن اعمال میکنیم تا زمانی که selected به صورت زیر برقرار شود:
1{
2 !!selected.length && (
3 <div>
4 <button
5 name="morph"
6 type="button"
7 className={styles.morph}
8 onClick={onMorph}
9 >
10 Morph
11 </button>
12 </div>
13 )
14}
نکته: ما دکمه را درون یک عنصر div قرار دادهایم تا بتوانیم یک کنترل دقیقتر روی موقعیت و فاصلهبندی دکمه داشته باشیم.
اینک بازیکن به خوبی میداند که وی باید چیزی را انتخاب کند چون این تنها گزینهای است که پیش روی او قرار گرفته است:
این دکمه کمی ساده به نظر میرسد. ما میخواهیم بازیکن با انگیزه بماند و از این که تا این سطح 10 آمده است خوشحال باشد. بنابراین در گام بعدی آیکونهایی در سمت چپ و راست دکمه morph قرار میدهیم که میتوان با نصب react-icons آنها را تعریف کرد:
npm install --save react-icons
نکته: اگر در زمان نصب پکیج با استفاده از NPM با خطایی مواجه شدید، تلاش کنید آن را با دستور yarn نصب کنید و پکیج را با دستور زیر مجدداً اضافه کنید:
yarn add react-icons
پس از آن میتوانید دوباره به NPM بازگشته و سرور را آغاز کنید. سپس این کامپوننت آیکون را ایمپورت میکنیم:
import { MdKeyboardTab } from 'react-icons/md'
ما یک آیکون فلش راست قبل و یک آیکون هم پس از آن قرار دادیم. اینک آیکونهای فلش ثابت هستند و به سمت دکمه اشاره میکنند. باید با استایلبندی به آنها جان ببخشیم و از این رو جلوه تغییر رنگ با تغییر مداوم به آنها میدهیم. علاوه بر این آیکون فلش سمت راست را نیز طوری اصلاح میکنیم که به سمت داخل اشاره کند:
استایلهایی که استفاده شده است به صورت زیر بودهاند.
فایل src.styles.module.css
1.morphArrow {
2 color: rgb(123, 247, 199);
3 transform: scale(2);
4 animation: morphArrow 2s infinite;
5}
6
7.morphArrowFlipped {
8 composes: morphArrow;
9 transform: scale(-2, 2);
10}
11
12@keyframes morphArrow {
13 0% {
14 opacity: 1;
15 color: rgb(123, 247, 199);
16 }
17 40% {
18 opacity: 0.4;
19 color: rgb(248, 244, 20);
20 }
21 100% {
22 opacity: 1;
23 color: rgb(123, 247, 199);
24 }
25}
اینک وقتی این صفحه را با نسخههای قبلی مقایسه میکنیم میبینیم که بازیکنها انگیزه بسیار بیشتری برای آغاز فصل بعدی پیدا میکنند.
بدین ترتیب به پایان بخش اول این آموزش می رسیم، برای دیدن بخش دوم روی لینک زیر کلیک کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- واکشی (Fetch) کردن داده ها در اپلیکیشن های React — به زبان ساده
- ۲۲ ابزار مهم برای توسعه دهندگان React — فهرست کاربردی
- آموزش ری اکت (React) — مجموعه مقالات مجله فرادرس
==