ساخت صفحه انتخاب کاراکتر در React (بخش دوم) — از صفر تا صد

۵۱ بازدید
آخرین به‌روزرسانی: ۲۰ شهریور ۱۴۰۲
زمان مطالعه: ۱۱ دقیقه
ساخت صفحه انتخاب کاراکتر در React (بخش دوم) — از صفر تا صد

در بخش قبلی این آموزش با مقدمات طراحی یک صفحه انتخاب کاراکتر در React آشنا شدیم. و اینک بخش دوم تقدیم شما می‌شود.

فهرست مطالب این نوشته

تابع onMorph کنونی حالت morphed را در زمان کلیک شدن به true تنظیم می‌کند و از این رو می‌توانیم کلاس کاراکتر مخفی را که بازیکن انتخاب کرده است به محض true شدن morphed نمایش دهیم. ما آن را درست زیر عنصر div شامل دکمه morph نمایش می‌دهیم.

فایل src/App.js

1// at the top:
2import sageImg from './resources/sage.jpg'
3// ...
4
5{
6  morphed && (
7    <div className={styles.morphed}>
8      <Header>Congratulations!</Header>
9      <Content>
10        <div className={styles.characterBox}>
11          <img src={sageImg} />
12        </div>
13      </div>
14      <Subheader>
15        You have morphed into a <em>Sage</em>
16      </Subheader>
17    </div>
18  )
19}

فایل src/styles.module.css

1.morphed {
2  animation: flashfade 4s forwards;
3  opacity: 0;
4}
5@keyframes flashfade {
6  0% {
7    opacity: 0;
8  }
9  60% {
10    opacity: 0.7;
11  }
12  100% {
13    opacity: 1;
14  }
15}

کلاس مخفی که بازیکن به آن تبدیل می‌شود، یک Sage است که به صورت زیر نمایش می‌یابد:

 صفحه انتخاب کاراکتر در React

گرچه این وضعیت می‌تواند مناسب تلقی شود، اما به نظر می‌رسد همچنان می‌توان بهبودهایی روی آن اعمال کرد. به بیان دیگر به نظر می‌رسد که بازیکن به مقداری عمل اسکرول کردن نیاز دارد و از این رو باید با رابط کاربری سر و کله بزند.

در این مرحله یک کتابخانه کوچک به نام react-scroll-to-component نصب می‌کنیم که به ما امکان اسکرول کردن به صفحه بازیکن و اساساً به هر عنصر را با ارسال ارجاع آن عنصر می‌دهد:

npm install --save react-scroll-to-component

آن را در فایل src/App.js ایمپورت می‌کنیم:

import scrollToComponent from 'react-scroll-to-component'

اکنون یک ref ایجاد کرده و آن را به عنصر الصاق می‌کنیم:

1const App = () => {
2  const morphedRef = React.createRef()
3  const { selected, onSelect, morphed, onMorph } = useLevelUpScreen({ morphedRef })
4  
5// ...
6
7   {morphed && (
8    <div
9      className={cx({
10        [styles.morphed]: morphed,
11        [styles.hidden]: !morphed,
12      })}
13    >
14      <Header>Congratulations!</Header>
15      <Content>
16        <div ref={morphedRef} className={styles.characterBox}>
17          <img src={sageImg} />
18        </div>
19      </Content>
20      <Subheader>
21        You have morphed into a <em>Sage</em>
22      </Subheader>
23    </div>
24  )}

از آنجا که می‌خواهیم جلوه اسکرول کردن، روان به نظر برسد، باید مقداری به ارتفاع صفحه اضافه کنیم تا فضای بیشتری پدید آید. این کار در عمل با افزودن یک div خالی با مقداری ارتفاع در زمان true شدن morphed ممکن خواهد بود:

1{
2  morphed && (
3    <div
4      className={cx({
5        [styles.morphed]: morphed,
6        [styles.hidden]: !morphed,
7      })}
8    >
9      <Header>Congratulations!</Header>
10      <Content>
11        <div ref={morphedRef} className={styles.characterBox}>
12          <img src={sageImg} />
13        </div>
14      </Content>
15      <Subheader>
16        You have morphed into a <em>Sage</em>
17      </Subheader>
18    </div>
19  )
20}
21{
22  morphed && <div style={{ height: '30vh' }} />
23}

اما اینک یک مشکل داریم. این ارتفاع اضافی در زمانی که ناحیه sega هنوز نمایان نشده است، ایجاد نمی‌شود. به بیان دیگر کارکرد اسکرول شدن کار نمی‌کند، زیرا در زمان فراخوانی هنوز فضای اضافی وجود ندارد. به منظور رفع این اشکال باید یک حالت دیگر به نام morphing اضافه کنیم که مقداری زمان به ما بدهد تا بتوانیم true شدن morphed بازیکن را در رابط کاربری معطل بکنیم:

1const useLevelUpScreen = ({ morphedRef }) => {
2  const [selected, setSelected] = React.useState([])
3  const [morphing, setMorphing] = React.useState(false)
4  const [morphed, setMorphed] = React.useState(false)
5  
6  const onSelect = (type) => (e) => {
7    setSelected((prevSelected) => {
8      if (prevSelected.includes(type)) {
9        return prevSelected.filter((t) => t !== type)
10      }
11      return [...prevSelected, type]
12    })
13  }
14  
15  const onMorph = () => {
16    setMorphing(true)
17    setTimeout(() => {
18      setMorphed(true)
19      setMorphing(false)
20    }, 1500)
21  }
22  
23  React.useEffect(() => {
24    if (morphed) {
25      scrollToComponent(morphedRef.current, {
26        offset: 100,
27        align: 'middle',
28        duration: 1000,
29      })
30    }
31  }, [morphed, morphedRef])
32  
33  return {
34    selected,
35    onSelect,
36    morphed,
37    onMorph,
38    morphing,
39  }
40}

اما اکنون با مشکل جدیدی مواجه شده‌ایم. به نظرمی رسد که morphed عناصر درون خود را از رندر شدن بازمی‌دارد و این مسدود شدن از به‌کارگیری منطق درون قاب زمانی 1.5 ثانیه‌ای ناشی می‌شود:

1const App = () => {
2  const morphedRef = React.createRef()
3  const { selected, onSelect, morphing, morphed, onMorph } = useLevelUpScreen()
4  
5// ...
6
7{morphed && (
8  <div
9    className={cx({
10      [styles.morphed]: morphed,
11      [styles.hidden]: !morphed,
12    })}
13  >
14    <Header>Congratulations!</Header>
15    <Content>
16      <div ref={morphedRef} className={styles.characterBox}>
17        <img src={sageImg} />
18      </div>
19    </Content>
20    <Subheader>
21      You have morphed into a <em>Sage</em>
22    </Subheader>
23  </div>
24)}
25{morphed && <div style={{ height: '30vh' }} />}

برای حل این مشکل باید گزاره شرطی && morphed  را برداریم و به جای آن از پکیج classnames برای ادغام استایل‌های اضافی استفاده کنیم. این استایل‌ها آن رفتار را شبیه‌سازی می‌کنند و عناصر را در درخت react حفظ می‌کنند. بدین ترتیب می‌توانیم از قابلیت‌هایی مانند انیمیشن نیز استفاده کنیم:

1<div
2  className={cx({
3    [styles.morphed]: morphed,
4    [styles.hidden]: !morphed,
5  })}
6>
7  <Header>Congratulations!</Header>
8  <Content>
9    <div ref={morphedRef} className={styles.characterBox}>
10      <img src={sageImg} />
11    </div>
12  </Content>
13  <Subheader>
14    You have morphed into a <em>Sage</em>
15  </Subheader>
16</div>
17{
18  morphing || (morphed && <div style={{ height: '30vh' }} />)
19}

همچنین باید ref دیگر را نیز روی دکمه morph اعمال کنیم تا زمانی که کاربر یک کلاس کاراکتر را انتخاب می‌کند نیز صفحه اسکرول شود.

فایل src/App.js

1const useLevelUpScreen = ({ morphRef, morphedRef }) => {
2  
3// ...
4
5const 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  scrollToComponent(morphRef.current, {
13    offset: 300,
14    align: 'bottom',
15    duration: 1000,
16  })
17}
18
19const onMorph = () => {
20  if (!morphing) setMorphing(true)
21  setTimeout(() => {
22    setMorphing(false)
23    setMorphed(true)
24  }, 1500)
25}
26
27// ...
28
29return {
30  selected,
31  onSelect,
32  morphed,
33  morphing,
34  onMorph,
35}
36
37const App = () => {
38  const morphRef = React.createRef()
39  const morphedRef = React.createRef()
40  // ...
41 <div
42   ref={morphRef}
43    className={cx(styles.morph, {
44      [styles.hidden]: !selected.length,
45    })}
46  >
47  <MdKeyboardTab className={styles.morphArrow} />
48  <button
49    ref={morphRef}
50    name='morph'
51    type='button'
52    className={styles.morph}
53    style={{ opacity: morphed ? '0.4' : 1 }}
54    onClick={onMorph}
55    disabled={morphed}
56  >
57    {morphing ? 'Morphing...' : morphed ? 'Morphed' : 'Morph'}
58  </button>
59  <MdKeyboardTab className={styles.morphArrowFlipped} />
60</div>

در مثال فوق زمانی که morph انجام شد، استایل زیر را اعمال کردیم:

style={{ opacity: morphed? '0.4': 1 }}

تا به بازیکن نشان دهیم که دکمه دیگر در دسترس نیست. یک خصوصیت غیر فعال‌سازی نیز برای غیر فعال کردن رویدادهای کلیک به صورت زیر اعمال کردیم:

disabled={morphed{

همچنین متن را بر اساس به‌روزرسانی حالت morph با کد زیر تغییر دادیم:

{morphing? 'Morphing...': morphed? 'Morphed': 'Morph'}

تا کاربر به طور مداوم با نگاه کردن به چیزهایی که در حال تغییر هستند، مشغول باشد. همچنین بخش ) && selected.length !!} را حذف کردیم چون انیمیشن را مسدود می‌کرد و یک ref به نام morphRef چنان که در بخش قبلی دیدیم روی آن اعمال کردیم.

در نهایت در قلاب سفارشی، پیاده‌سازی scrollToComponent را نیز در انتهای تابع onSelect اعمال کردیم تا اسکرول را روی دکمه morph نیز انیمیت کنیم.

ویرایش نهایی

زمانی که morphing کامل شد نوعی انیمیشن بارگذاری را شبیه‌سازی می‌کنیم تا کاربر بد‌اند که می‌خواهیم به بخش بعدی برویم:

1<div
2  className={cx(styles.next, {
3    [styles.hidden]: !ready,
4  })}
5>
6  <div>
7    <RingLoader size={60} color="rgb(213, 202, 255)" loading />
8    <p>Loading...</p>
9  </div>
10</div>

استایل‌های آن چنین است:

1.next {
2  text-align: center;
3  margin: 35px auto;
4  display: flex;
5  justify-content: center;
6}
7.next p {
8  font-family: Patua One, sans-serif;
9  font-weight: 300;
10  text-align: center;
11  color: #fff;
12}

می‌توانید ببینید که یک حالت جدید به نام ready نیز وجود دارد، بنابراین باید آن را در قلاب سفارشی پیاده‌سازی کنیم:

1const useLevelUpScreen = ({ morphRef, morphedRef }) => {
2  const [selected, setSelected] = React.useState([])
3  const [morphing, setMorphing] = React.useState(false)
4  const [morphed, setMorphed] = React.useState(false)
5  const [ready, setReady] = React.useState(false)
6  
7  const onSelect = (type) => (e) => {
8    setSelected((prevSelected) => {
9      if (prevSelected.includes(type)) {
10        return prevSelected.filter((t) => t !== type)
11      }
12      return [...prevSelected, type]
13    })
14    scrollToComponent(morphRef.current, {
15      offset: 300,
16      align: 'bottom',
17      duration: 1000,
18    })
19  }
20  
21  const onMorph = () => {
22    setMorphing(true)
23    setTimeout(() => {
24      setMorphing(false)
25      setMorphed(true)
26    }, 1500)
27  }
28  
29  React.useEffect(() => {
30    if (morphed && !ready) {
31      scrollToComponent(morphedRef.current, {
32        offset: 100,
33        align: 'middle',
34        duration: 1000,
35      })
36      setTimeout(() => {
37        setReady(true)
38      }, 2000)
39    }
40  }, [morphed, morphedRef, ready])
41  
42  return {
43    selected,
44    onSelect,
45    morphed,
46    morphing,
47    onMorph,
48    ready,
49  }
50}

در نهایت قصد داریم کل صفحه را فید کنیم تا با پایان یافتن بخش کنونی صفحه‌های بعدی را نمایش دهیم. این بدان معنی است که باید حالت دیگری به نام shutdown به قلاب سفارشی اضافه کنیم که فراخوانی می‌شود و یک نام کلاس جدید به عنصر div مربوط به root اعمال می‌کند. حالت shutdown تنها زمانی که ready به صورت true درآید مورد استفاده قرار می‌گیرد.

1const useLevelUpScreen = ({ morphRef, morphedRef }) => {
2  const [selected, setSelected] = React.useState([])
3  const [morphing, setMorphing] = React.useState(false)
4  const [morphed, setMorphed] = React.useState(false)
5  const [ready, setReady] = React.useState(false)
6  const [shutdown, setShutdown] = React.useState(false)
7  
8  const onSelect = (type) => (e) => {
9    setSelected((prevSelected) => {
10      if (prevSelected.includes(type)) {
11        return prevSelected.filter((t) => t !== type)
12      }
13      return [...prevSelected, type]
14    })
15    scrollToComponent(morphRef.current, {
16      offset: 300,
17      align: 'bottom',
18      duration: 1000,
19    })
20  }
21  
22  const onMorph = () => {
23    setMorphing(true)
24    setTimeout(() => {
25      setMorphing(false)
26      setMorphed(true)
27    }, 1500)
28  }
29  
30  React.useEffect(() => {
31    if (morphed && !ready) {
32      scrollToComponent(morphedRef.current, {
33        offset: 100,
34        align: 'middle',
35        duration: 1000,
36      })
37    setTimeout(() => {
38      setReady(true)
39    }, 2000)
40    }
41  }, [morphed, morphedRef, ready])
42  
43  React.useEffect(() => {
44    if (ready && !shutdown) {
45      setTimeout(() => {
46        setShutdown(true)
47      }, 2000)
48    }
49  }, [ready, shutdown])
50  
51  return {
52    selected,
53    onSelect,
54    morphed,
55    morphing,
56    onMorph,
57    ready,
58    shutdown,
59  }
60}
61
62const App = () => {
63  const morphRef = React.createRef()
64  const morphedRef = React.createRef()
65  
66  const {
67    selected,
68    onSelect,
69    morphing,
70    morphed,
71    onMorph,
72    ready,
73    shutdown,
74  } = useLevelUpScreen({
75    morphRef,
76    morphedRef,
77  })
78  
79  const onClick = (e) => {
80    console.log("Don't mind me. I'm useless until I become useful")
81  }
82  
83  return (
84    <div
85      className={cx(styles.root, {
86        [styles.shutdown]: shutdown,
87      })}

نتیجه نهایی چنین است:

 صفحه انتخاب کاراکتر در React

اینک کد کامل به صورت زیر باید باشد.

فایل src/App.js

1import React from 'react'
2import cx from 'classnames'
3import { RingLoader } from 'react-spinners'
4import { MdKeyboardTab } from 'react-icons/md'
5import scrollToComponent from 'react-scroll-to-component'
6import noviceImg from './resources/novice.jpg'
7import sorceressImg from './resources/sorceress.jpg'
8import knightImg from './resources/knight.jpg'
9import sageImg from './resources/sage.jpg'
10import styles from './styles.module.css'
11import { Header, Subheader, Content } from './components'
12
13const useLevelUpScreen = ({ morphRef, morphedRef }) => {
14  const [selected, setSelected] = React.useState([])
15  const [morphing, setMorphing] = React.useState(false)
16  const [morphed, setMorphed] = React.useState(false)
17  const [ready, setReady] = React.useState(false)
18  const [shutdown, setShutdown] = React.useState(false)
19  
20  const onSelect = (type) => (e) => {
21    setSelected((prevSelected) => {
22      if (prevSelected.includes(type)) {
23        return prevSelected.filter((t) => t !== type)
24      }
25      return [...prevSelected, type]
26    })
27    scrollToComponent(morphRef.current, {
28      offset: 300,
29      align: 'bottom',
30      duration: 1000,
31    })
32  }
33  
34  const onMorph = () => {
35    setMorphing(true)
36    setTimeout(() => {
37      setMorphing(false)
38      setMorphed(true)
39    }, 1500)
40  }
41  
42  React.useEffect(() => {
43    if (morphed && !ready) {
44      scrollToComponent(morphedRef.current, {
45        offset: 100,
46        align: 'middle',
47        duration: 1000,
48      })
49      setTimeout(() => {
50        setReady(true)
51      }, 2000)
52    }
53  }, [morphed, morphedRef, ready])
54  
55  React.useEffect(() => {
56    if (ready && !shutdown) {
57      setTimeout(() => {
58        setShutdown(true)
59      }, 2000)
60    }
61  }, [ready, shutdown])
62  
63  return {
64    selected,
65    onSelect,
66    morphed,
67    morphing,
68    onMorph,
69    ready,
70    shutdown,
71  }
72}
73
74const App = () => {
75  const morphRef = React.createRef()
76  const morphedRef = React.createRef()
77  
78  const {
79    selected,
80    onSelect,
81    morphing,
82    morphed,
83    onMorph,
84    ready,
85    shutdown,
86  } = useLevelUpScreen({
87    morphRef,
88    morphedRef,
89  })
90  
91  return (
92    <div
93      className={cx(styles.root, {
94        [styles.shutdown]: shutdown,
95      })}
96    >
97      <Header>
98        You are a <em>Novice</em>
99      </Header>
100      <Content>
101        <div
102          className={styles.characterBox}
103          style={{ width: 200, height: 150 }}
104        >
105          <img alt="" src={noviceImg} />
106        </div>
107      </Content>
108      <Subheader>Congratulations on reaching level 10!</Subheader>
109      <div style={{ margin: '25px auto' }}>
110        <Header>Choose your destiny</Header>
111        <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
112        <Content>
113          <div
114            onClick={onSelect('Sorceress')}
115            className={cx(styles.characterBox, {
116              [styles.selectedBox]: selected.includes('Sorceress'),
117            })}
118          >
119            <h2>Sorceress</h2>
120            <img
121              alt=""
122              src={sorceressImg}
123              className={cx(styles.tier2, {
124                [styles.selected]: selected.includes('Sorceress'),
125              })}
126            />
127          </div>
128          <div
129            onClick={onSelect('Knight')}
130            className={cx(styles.characterBox, {
131              [styles.selectedBox]: selected.includes('Knight'),
132            })}
133          >
134            <h2>Knight</h2>
135            <img
136              alt=""
137              src={knightImg}
138              className={cx(styles.tier2, {
139                [styles.selected]: selected.includes('Knight'),
140              })}
141            />
142          </div>
143        </Content>
144      </div>
145      <div
146        ref={morphRef}
147        className={cx(styles.morph, {
148          [styles.hidden]: !selected.length,
149        })}
150      >
151        <MdKeyboardTab className={styles.morphArrow} />
152        <button
153          ref={morphRef}
154          name="morph"
155          type="button"
156          className={styles.morph}
157          style={{ opacity: morphed ? '0.4' : 1 }}
158          onClick={onMorph}
159          disabled={morphed}
160        >
161          {morphing ? 'Morphing...' : morphed ? 'Morphed' : 'Morph'}
162        </button>
163        <MdKeyboardTab className={styles.morphArrowFlipped} />
164      </div>
165      <div
166        className={cx({
167          [styles.morphed]: morphed,
168          [styles.hidden]: !morphed,
169        })}
170      >
171        <Header>Congratulations!</Header>
172        <Content>
173          <div ref={morphedRef} className={styles.characterBox}>
174            <img src={sageImg} />
175          </div>
176        </Content>
177        <Subheader>
178          You have morphed into a <em>Sage</em>
179        </Subheader>
180      </div>
181      <div
182        className={cx(styles.next, {
183          [styles.hidden]: !ready,
184        })}
185      >
186        <div>
187          <RingLoader size={60} color="rgb(213, 202, 255)" loading />
188          <p>Loading...</p>
189        </div>
190      </div>
191    </div>
192  )
193}
194
195export default App

فایل src/components.js

1import React from 'react'
2import cx from 'classnames'
3import styles from './styles.module.css'
4
5export const Header = ({ children, ...rest }) => (
6  // eslint-disable-next-line
7  <h1 className={styles.header} {...rest}>
8    {children}
9  </h1>
10)
11
12export const Subheader = ({ children, ...rest }) => (
13  <small className={styles.subheader} {...rest}>
14    {children}
15  </small>
16)
17
18export const Content = ({ children, ...rest }) => (
19  <div className={styles.container} {...rest}>
20    {children}
21  </div>
22)

فایل src/styles.module.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}
10
11code {
12  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13    monospace;
14}
15
16.root {
17  padding: 20px 0;
18}
19
20.container {
21  display: flex;
22  justify-content: center;
23}
24
25.header {
26  text-align: center;
27  color: rgb(252, 216, 169);
28  font-weight: 300;
29  margin: 0;
30}
31
32.subheader {
33  color: #fff;
34  text-align: center;
35  font-weight: 300;
36  width: 100%;
37  display: block;
38}
39
40.characterBox {
41  transition: all 0.1s ease-out;
42  width: 300px;
43  height: 250px;
44  border: 1px solid rgb(194, 5, 115);
45  background: rgb(82, 26, 134);
46  margin: 12px 6px;
47  overflow: hidden;
48}
49
50.characterBox img {
51  width: 100%;
52  height: 100%;
53  object-fit: cover;
54  cursor: pointer;
55}
56
57.selectedBox {
58  border: 1px solid rgb(24, 240, 255) !important;
59}
60
61.characterBox h2 {
62  transition: all 0.3s ease-out;
63  text-align: center;
64  color: rgb(213, 202, 255);
65  font-style: italic;
66  font-weight: 500;
67}
68
69.characterBox:hover h2 {
70  color: rgb(191, 255, 241);
71}
72
73.characterBox img {
74  transition: all 0.3s ease-out;
75  width: 100%;
76  height: 100%;
77  object-fit: cover;
78  cursor: pointer;
79}
80
81.characterBox img.tier2:hover,
82.characterBox img.selected {
83  animation: hueRotate 2s infinite;
84  transform: scale(1.05);
85}
86
87.morph {
88  margin: 30px auto;
89  text-align: center;
90}
91
92.morphArrow {
93  color: rgb(123, 247, 199);
94  transform: scale(2);
95  animation: morphArrow 2s infinite;
96}
97
98.morphArrowFlipped {
99  composes: morphArrow;
100  transform: scale(-2, 2);
101}
102
103@keyframes morphArrow {
104  0% {
105    opacity: 1;
106    color: rgb(123, 247, 199);
107  }
108  40% {
109    opacity: 0.4;
110    color: rgb(248, 244, 20);
111  }
112  100% {
113    opacity: 1;
114    color: rgb(123, 247, 199);
115  }
116}
117
118button.morph {
119  cursor: pointer;
120  transition: all 0.2s ease-out;
121  border-radius: 25px;
122  padding: 14px 22px;
123  color: #fff;
124  background: rgb(35, 153, 147);
125  border: 1px solid #fff;
126  font-family: Patua One, sans-serif;
127  font-size: 1.2rem;
128  text-transform: uppercase;
129  letter-spacing: 2px;
130  margin: 0 20px;
131}
132
133button.morph:hover {
134  background: none;
135  border: 1px solid rgb(35, 153, 147);
136  color: rgb(35, 153, 147);
137}
138
139.morphed {
140  animation: flashfade 4s forwards;
141  opacity: 0;
142}
143
144@keyframes flashfade {
145  0% {
146    opacity: 0;
147  }
148  60% {
149    opacity: 0.7;
150  }
151  100% {
152    opacity: 1;
153  }
154}
155
156.hidden {
157  visibility: hidden;
158}
159
160.next {
161  text-align: center;
162  margin: 35px auto;
163  display: flex;
164  justify-content: center;
165}
166
167.next p {
168  font-family: Patua One, sans-serif;
169  font-weight: 300;
170  text-align: center;
171  color: #fff;
172}
173
174@keyframes hueRotate {
175  0% {
176    filter: hue-rotate(0deg);
177  }
178  50% {
179    filter: hue-rotate(260deg) grayscale(100%);
180  }
181  100% {
182    filter: hue-rotate(0deg);
183  }
184}
185
186.shutdown {
187  animation: shutdown 3s forwards;
188}
189
190@keyframes shutdown {
191  100% {
192    opacity: 0;
193  }
194}

ممکن است متوجه شده باشید که در سراسر این راهنما برخی کدهای تکراری وجود داشتند. فرض کنید مجبور باشید یک تغییر ناگهانی روی کادرهای انتخاب کاراکتر داشته باشید، مثلاً اندازه آن‌ها را تغییر دهید. بدین ترتیب اگر یکی از آن‌ها را عوض کنید، باید کل فایل را اسکن کنید و کادر انتخاب دیگر را پیدا کنید تا آن را نیز تغییر دهید تا رابط کاربری یک دست بماند.

در حال حاضر، کادرهای انتخاب Sorceress و Knight یکسان هستند و باید به صورت یکسانی نیز بمانند. اما اگر دو کاراکتر tier2 دیگر به بازی اضافه کنیم چطور؟ بدین ترتیب باید کلی کد تکراری داشته باشیم. از این رو ایده بهتر این است که آن را به صورت یک کامپوننت مستقل تجرید کنیم. این کار مزیت عمده‌ای به صورت امکان سفارشی‌سازی بهتر نیز فراهم می‌آورد.

در صورتی که کادرهای انتخاب کاراکتر را تجرید کنیم، کد به صورت زیر در می‌آید:

فایل src/App.js

1const characterSelections = [
2  { type: 'Sorceress', src: sorceressImg },
3  { type: 'Knight', src: knightImg },
4  { type: 'Shapeshifter', src: shapeshifterImg },
5  { type: 'Bandit', src: banditImg },
6  { type: 'Archer', src: archerImg },
7  { type: 'Blade Master', src: bladeMasterImg },
8  { type: 'Destroyer', src: destroyerImg },
9  { type: 'Summoner', src: summonerImg },
10  { type: 'Phantom', src: phantomImg },
11]
12
13const charSelectionMapper = characterSelections.reduce(
14  (acc, { type, src }) => ({
15    ...acc,
16    [type]: src,
17  }),
18  {},
19)
20
21const App = () => {
22  const morphRef = React.createRef()
23  const morphedRef = React.createRef()
24  
25  const {
26    selected,
27    onSelect,
28    morphing,
29    morphed,
30    onMorph,
31    ready,
32    shutdown,
33  } = useLevelUpScreen({
34    morphRef,
35    morphedRef,
36  })
37  
38  return (
39    <div
40      className={cx(styles.root, {
41        [styles.shutdown]: shutdown,
42      })}
43    >
44      <Header>
45        You are a <em>Novice</em>
46      </Header>
47      <Content>
48        <CharacterBox
49          style={{ width: 200, height: 150 }}
50          imgProps={{ src: noviceImg }}
51          disableFlashing
52        />
53      </Content>
54      <Subheader>Congratulations on reaching level 10!</Subheader>
55      <div style={{ margin: '25px auto' }}>
56        <Header>Choose your destiny</Header>
57        <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
58        <Content display="grid">
59          {characterSelections.map((props, index) => (
60            <CharacterBox
61              key={`char_selection_${index}`}
62              onClick={onSelect(props.type)}
63              isSelected={selected === props.type}
64              {...props}
65            />
66          ))}
67        </Content>
68      </div>
69      <div
70        ref={morphRef}
71        className={cx(styles.morph, {
72          [styles.hidden]: !selected,
73        })}
74      >
75        <MdKeyboardTab className={styles.morphArrow} />
76        <button
77          ref={morphRef}
78          name="morph"
79          type="button"
80          className={styles.morph}
81          style={{ opacity: morphed ? '0.4' : 1 }}
82          onClick={onMorph}
83          disabled={morphed}
84        >
85          {morphing ? 'Morphing...' : morphed ? 'Morphed' : 'Morph'}
86        </button>
87        <MdKeyboardTab className={styles.morphArrowFlipped} />
88      </div>
89      <div
90        className={cx({
91          [styles.morphed]: morphed,
92          [styles.hidden]: !morphed,
93        })}
94      >
95        <Header>Congratulations!</Header>
96        <Content>
97          <CharacterBox
98            ref={morphedRef}
99            type={selected}
100            headerProps={{ className: styles.unique }}
101            imgProps={{ src: charSelectionMapper[selected] }}
102          />
103        </Content>
104        <Subheader>
105          You have morphed into a <em>{selected}</em>
106        </Subheader>
107      </div>
108      <div
109        className={cx(styles.next, {
110          [styles.hidden]: !ready,
111        })}
112      >
113        <div>
114          <RingLoader size={60} color="rgb(213, 202, 255)" loading />
115          <p>Loading...</p>
116        </div>
117      </div>
118    </div>
119  )
120}

فایل src/components.js

1// ...
2
3const CharacterBox = React.forwardRef(
4  (
5    {
6      isSelected,
7      type,
8      headerProps = {},
9      imgProps = {},
10      src,
11      disableFlashing,
12      ...rest
13    },
14    ref,
15  ) => (
16    <div
17      ref={ref}
18      className={cx(styles.characterBox, {
19        [styles.selectedBox]: isSelected,
20      })}
21      {...rest}
22    >
23      {type && <h3 {...headerProps}>{type}</h3>}
24      <img
25        {...imgProps}
26        src={src || imgProps.src}
27        className={cx(styles.tier2, imgProps.className, {
28          [styles.selected]: isSelected,
29          [styles.noAnimation]: !!disableFlashing,
30        })}
31        alt=""
32      />
33    </div>
34  ),
35)

سخن پایانی

بدین ترتیب به انتهای این مقاله‌ی دو بخشی می‌رسیم. امیدواریم از این راهنما بهره لازم را برده باشید و توانسته باشیم به دانش شما در این زمینه بیافزاییم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *