**فرانتیوم** این روزها حالِ جالبی ندارد; **درس میخوانَد، کار میکند، حتی تفریح هم میکند،**
اما انگار فکرهای زیادی که توی سرش میچرخد **و احساسِ ازدسترفتهاش باعث شده نسبت به همهچیز سِر باشد!**

تنها چیزی که فرانتیوم را روی پا نگه داشته، **فقط و فقط کار کردن بود** — یادتان هست؟
ذهنش هم خیلی خلاق بود؛
برای همین تصمیم گرفت با **ایموجیهای مختلف** و **رنگهای گوناگون** کاری کند تا ذهنِ آشفتهاش را به تصویر بکشد.
# **پروژهی اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/contest/assignments/91619/download_problem_initial_project/310785/) کلیک کنید.
<details class="red">
<summary>**ساختار فایلها**</summary>
```
happy_sad_angry/
├─ icons/
│ ├─ angry.png
│ ├─ happy.png
│ └─ sad.png
├─ <mark class="orange" title="این فایل را تکمیل کنید">index.html</mark>
├─ <mark class="orange" title="این فایل را تکمیل کنید">index.js</mark>
└─ styles.css
```
</details>
# **جزئیات پیادهسازی**
پس از **پیادهسازی کامل** و **صحیح** تمام موارد ذکرشده، باید نتیجهای مشابه تصویر زیر مشاهده کنید:

<details class="purple">
<summary>**پیادهسازی اِلمان** `container`</summary>
**هر سطری** که در ادامه پیادهسازی میشود، **باید درون این** `container` قرار گیرد. در مجموع، **چهار سطر** خواهیم داشت.
+ **توجه:** در نهایت، باید در مجموع **۱۰ آیکون** استفاده شده باشد، بهطوری که تعداد آیکونهای هر سطر با توضیحات دادهشده مطابقت داشته باشد. در صورت عدم رعایت این مورد، **نمرهای به شما تعلق نخواهد گرفت.**
<details class="yellow">
<summary>**پیادهسازی ساختار هر سطر**</summary>
+ **هر سطر** باید در **یک** `div` قرار داشته باشد.
+ **هر سطر** باید دارای **کلاس** `container_item` باشد.
</details>
<details class="yellow">
<summary>**پیادهسازی سطرهای فرد**</summary>
در سطرهای فرد، **تنها باید دو آیکون** `happy` وجود داشته باشد.
**سطر اول فرد** باید دارای **کلاس** `row-blue` و **سطر فرد بعدی** دارای **کلاس** `row-red` باشد.
+ همچنین **هر سطر دارای دو آیکون** *(سطر فرد)* **باید کلاس** `two-icons` را نیز داشته باشد.
</details>
<details class="yellow">
<summary>**پیادهسازی سطرهای زوج**</summary>
در **سطرهای زوج** باید **سه آیکون** بهترتیب **از سمت چپ** وجود داشته باشد:
1. `happy`
2. `sad`
3. `angry`
**سطر اول زوج** باید دارای **کلاس** `row-teal` باشد و **سطر زوج بعدی** دارای **کلاس** `row-lightred`.
</details>
+ **توجه:** برای **هر آیکون** باید یک **تگ** `img` ایجاد کرده و **مسیر آن آیکون** را در **ویژگی** `src` قرار دهید.
+ **توجه:** آیکونهای ذکرشده را میتوانید در **پوشهی** `icons` در میان **فایلهای پروژه اولیه** پیدا کنید.
</details>
# **آنچه باید آپلود کنید**
+ **توجه:** در صورتی که تمام موارد را پیادهسازی کردهاید، اما نتیجهی دلخواه **مشاهده نمیشود،** ممکن است نیاز باشد به **تگ** _script_ پراپتیای اضافه کنید یا از یک **رویداد** *(event)* **جاوا اسکریپتی** در فایل *index.js* استفاده کنید تا مشکل برطرف شود.
+ **توجه:** در این سوال **شما مجاز به ایجاد تغییرات در فایلهای** `index.html` و `index.js` هستید.
+ **توجه:** پس از اعمال تغییرات، این دو فایل را در یک فایل _Zip_ قرار داده و آپلود کنید.
**فرانتیوم** همیشه عاشق داستانهای جادویی بود. به نظر او، **قشنگترین** داستانهای جادوگری مربوط به دنیای _هری پاتر_ و _ویچر_ است. یکی از تفریحاتش این بود که روی تختش دراز بکشد، چشمهایش را ببندد و در دنیای خیالی خودش غرق شود.
به نظرش این کار یکی از لذتبخشترین لحظههای زندگی بود.
یک روز بعد از کاری طولانی و خستهکننده، روی کاناپه نشسته بود **و غرق در خیالپردازی بود.** در ذهنش، خودش را جادوگری نامی میدید که مشغول اجرای طلسمها و جادوجنبل است.
ناگهان با خودش فکر کرد:
**«توی دنیای فرانت چطوری میشه جادو کرد؟»**
— آهاااااااااااااااااااااااا!
جوابش چیزی نبود جز **خلق انیمیشنهای جادویی با کدنویسی!**

# **پروژهی اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/contest/assignments/91619/download_problem_initial_project/310786/) کلیک کنید.
<details class="red">
<summary>**ساختار فایلها**</summary>
```plaintext
landing
├─ images/
│ ├─ 1.webp
│ ├─ 2.jpg
│ ├─ 3.webp
│ └─ 4.webp
├─ index.html
├─ <mark class="orange" title="این فایل را پیادهسازی کنید">index.js</mark>
└─ styles.css
```
</details>
# **جزئیات پروژه**
**پیادهسازی این سوال به سه قسمت مهم تقسیم میشود:**
1. **اسلایدر**
2. **اسکرول خودکار**
3. **ظاهر شدن باکس**
<details class="purple">
<summary>**پیادهسازی اسلایدر در** `first-section`</summary>
در این بخش یک **اسلایدر تصویری** با **چهار تصویر متفاوت** وجود دارد.
---
+ در **فایل** `index.html` میتوانید ساختار اسلایدر را مشاهده کنید.
هر تصویر درون یک المان با کلاس `slide` قرار دارد و همهی آنها درون عنصری با کلاس `slides` قرار گرفتهاند.
+ در دو سمت اسلایدر، **دو پیکان ناوبری** با کلاسهای `prev` و `next` وجود دارند.
- با کلیک روی پیکان **سمت راست** (`.next`) باید به **تصویر بعدی** برویم.
- با کلیک روی پیکان **سمت چپ** (`.prev`) باید به **تصویر قبلی** برگردیم.
---
**نکات مربوط به جابهجایی:**
- اگر روی **تصویر چهارم** باشیم و روی پیکان سمت راست کلیک کنیم، باید به **تصویر اول** بازگردیم.
- اگر روی **تصویر اول** باشیم و روی پیکان سمت چپ کلیک کنیم، باید به **تصویر چهارم** منتقل شویم.
---
**ویژگی اسلاید خودکار:**
- اسلایدر باید هر **۴ ثانیه یکبار** بهصورت خودکار به تصویر بعدی برود. جابهجایی اسلایدها در **حالت خودکار** نیز باید مشابه موارد گفته شده در بخش **نکات مربوط به جابهجایی** باشد.
- زمانی که کاربر با پیکانها به صورت دستی اسلاید را تغییر میدهد، **تایمر خودکار باید از ابتدا ریست شود.**
*(یعنی اگر ۳ ثانیه گذشته باشد و کاربر کلیک کند، شمارش دوباره از ۰ شروع میشود.)*
---
**نکات فنی:**
- میتوانید از **خاصیت** `transform: translateX(...)` برای **جابهجایی اسلایدها** استفاده کنید.
- **منطق خودکارسازی** حرکت را با `setInterval` پیادهسازی کنید و در زمان تعامل کاربر آن را بازنشانی (`clearInterval` و سپس شروع مجدد) کنید.

</details>
<details class="purple">
<summary>**پیادهسازی اسکرول خودکار در** `second-section`</summary>
در این بخش قرار است یک **حرکت افقی خودکار** برای ردیف ایموجیها پیادهسازی شود.
---
+ در **فایل** `index.html` تعدادی ایموجی درون یک عنصر با **کلاس** `scroll-track` قرار گرفتهاند که داخل یک **عنصر والد** با کلاس `infinite-scroll-wrapper` هستند.
+ هدف این است که این ردیف ایموجیها از لحظهی **بارگذاری صفحه**، بهصورت **پیوسته و نرم** به سمت **چپ** حرکت کنند تا زمانی که به انتهای مسیر برسند.
---
+ برای این منظور میتوانید با استفاده از **جاوااسکریپت** و ویژگی CSS `transform: translateX(...)` موقعیت افقی عنصر `scroll-track` را بهصورت تدریجی تغییر دهید.
+ از متد `requestAnimationFrame` برای ایجاد حرکت روان و بدون لگ استفاده کنید.
+ سرعت حرکت میتواند با مقدار افزایشی (مثلاً `1px` در هر فریم) کنترل شود.
---
**نکتهها:**
- حرکت باید **بهصورت خودکار و بدون دخالت کاربر** آغاز شود.
- زمانی که اسکرول به انتهای مسیر رسید (عرض کل `scroll-track` منهای عرض والد `wrapper`)، حرکت متوقف میشود.
- نیازی به استفاده از CSS Animation یا Keyframes نیست؛ منطق باید کاملاً با **JS** کنترل شود.

</details>
<details class="purple">
<summary>**پیادهسازی ظاهر شدن باکسها در** `third-section`</summary>
در این سکشن دو بخش به نامهای `first-box` و `second-box` وجود دارد که از نظر ساختار کلی مشابه هستند، اما **جهت نمایش و ترتیب انیمیشن آنها متفاوت است.**
+ **توجه کنید:** تمام انیمیشنها زمانی فعال میشوند که کاربر **در حال اسکرول به سمت پایین صفحه باشد** و المان مربوطه وارد محدودهی دید کاربر شود (حدود `10px` قبل از رسیدن باکس به viewport).
+ با ورود المان به محدودهی دید، **کلاس `active` اضافه میشود** و CSSهای مربوط به `opacity` و `transform` برای تصویر و متنها اعمال میگردد.
**توجه:** در فایل CSS، کامنتی با متن `/* third section */` قرار دارد. با بررسی استایلهای بعد از آن، میتوانید پیادهسازی این بخش را بهتر درک کنید.
## **اِلمان** `first-box`
1. تصویر در سمت **چپ** قرار دارد و بهصورت **دایرهای کامل** است.
2. متنها در سمت **راست** قرار دارند.
**انیمیشن:**
+ **باکس** `first-box` با اضافه شدن **کلاس** `active` فعال میشود.
+ تصویر از **سمت چپ** ظاهر میشود.
+ متنها (پاراگرافها) به ترتیب و با **تأخیر زمانی** ظاهر میشوند و **حداقل** `opacity > 0.9` دارند.
+ این انیمیشن **فقط یکبار** اجرا میشود.
## **اِلمان** `second-box`
1. تصویر در سمت **راست** قرار دارد و بهصورت **دایرهای کامل** است.
2. متنها در سمت **چپ** قرار دارند.
**انیمیشن:**
+ **باکس** `second-box` با اضافه شدن **کلاس** `active` فعال میشود.
+ تصویر از **سمت راست** ظاهر میشود.
+ متنها (پاراگرافها) به ترتیب و با **تأخیر زمانی** ظاهر میشوند و حداقل `opacity > 0.9` دارند.
+ این انیمیشن **فقط یکبار** اجرا میشود.

</details>
# **آنچه باید آپلود کنید**
+ **توجه:** پس از پیادهسازی موارد خواسته شده، **فایل** `index.js` را برای سیستم داوری ارسال کنید.
+ **توجه:** شما مجاز به افزودن فایل جدیدی در این ساختار **نیستید** و تنها باید تغییرات را در **فایل** `index.js` اعمال کنید.
+ **توجه:** برای **جلوگیری از بروز مشکل** در کار با *DOM،* پیشنهاد میشود یک رویداد *DOMContentLoaded* تعریف کرده و کدهای خود را درون آن قرار دهید.
**خسته** از هرچی که بود، **خسته** از هرچی که هست...
**فرانتیوم** امروز واقعاً از این وضعیت خسته شده بود!
با خودش گفت:
«چرا برای یه خرید ساده باید توی این گرمای تابستون، حتماً تا فروشگاه برم؟ اگه میتونستم خیلی راحت برم توی سایت، محصولم رو ببینم، آدرسم رو وارد کنم و تموم! چی میشد؟»
فرانتیوم فهمید که **غر زدن فایده نداره.** وقتشه خودش دستبهکار بشه و **فروشگاه آنلاین خودش** رو راه بندازه!

# **پروژهی اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/contest/assignments/91619/download_problem_initial_project/310787/) کلیک کنید.
<details class="red">
<summary>**ساختار فایلها**</summary>
```
little_shop
├─ api/
│ ├─ <mark class="orange" title="این فایل را تکمیل کنید">getAllProducts.js</mark>
│ └─ <mark class="orange" title="این فایل را تکمیل کنید">getProductById.js</mark>
├─ data/
│ └─ mockProducts.js
├─ images/
│ ├─ headphone.webp
│ ├─ laptop.jpg
│ └─ s25.png
├─ scripts/
│ ├─ <mark class="orange" title="این فایل را تکمیل کنید">checkout.js</mark>
│ ├─ <mark class="orange" title="این فایل را تکمیل کنید">index.js</mark>
│ └─ <mark class="orange" title="این فایل را تکمیل کنید">productDetail.js</mark>
├─ styles/
│ ├─ spinner.css
│ └─ style.css
├─ checkout.html
├─ index.html
├─ productDetail.html
└─ success.html
```
</details>
# **جزئیات پیادهسازی**
در گیفهای زیر میتوانید فرایندهای مختلف برنامه را مشاهده کنید:
+ صفحهی لیست محصولات

+ صفحهی جزئیات محصول

+ رفتن به صفحهی تکمیل پرداخت

+ فرایند استپر

طبق توضیحات زیر، باید فایلهای موجود در **دایرکتوری** `api` را ایجاد و تکمیل کنید.
<details class="purple">
<summary>**پیادهسازی دایرکتوری** `api`</summary>
در **فایل** `mockProducts`، یک آرایه از محصولات تعریف شده است.
## **پیادهسازی پرامیسها**
در این بخش، باید در **دایرکتوری** `api` **دو فایل جاوااسکریپت** با نامهای زیر ایجاد کنید:
1. `getAllProducts.js`
2. `getProductById.js`
<details class="yellow">
<summary>**پیادهسازی فایل** `getAllProducts`</summary>
در این فایل باید **تابعی** پیادهسازی شود که **تمام محصولات** را در صورت *resolve* شدن، بازگرداند.
در صورتی که *reject* شود، باید شیئی شامل موارد زیر بازگردانده شود:
+ `status`**: عدد** `401`
+ `data`**: آرایهای خالی** `[]`
+ `message`**: یک رشته با محتوای زیر**
```plaintext
محصولی یافت نشد، لطفا دوباره تلاش بکنید.
```
## **جزئیات پیادهسازی**
1. این تابع **باید** با استفاده از *Promise* پیادهسازی شود.
2. برای **شبیهسازی تاخیر پاسخ سرور،** از `setTimeout` با مدت زمان **۸۰۰ میلیثانیه** استفاده کنید.
3. درون این تاخیر، یک **مقدار تصادفی** با `Math.random()` تولید میشود:
+ اگر مقدار تصادفی **کمتر** از `0.5` باشد، *Promise* با دادههای `mockProducts` *resolve* شود.
+ در غیر این صورت، *Promise* با شیء خطا (دارای `status`، `data` و `message`) *reject* شود.
</details>
<details class="yellow">
<summary>**پیادهسازی فایل** `getProductById`</summary>
در این فایل باید **تابعی** پیادهسازی شود که **محصول مورد نظر را با استفاده از** `id` همان محصول **پیدا کند.**
در صورت *resolve* شدن، محصول پیدا شده **بازگردانده** میشود.
در صورت *reject* شدن، باید شیئی شامل موارد زیر **بازگردانده** شود:
+ `status`: عدد `404`
+ `message`: یک رشته با محتوای زیر:
```plaintext
محصول مورد نظر یافت نشد.
```
## **جزئیات پیادهسازی**
1. این تابع **باید** با استفاده از *Promise* پیادهسازی شود.
2. برای شبیهسازی تاخیر پاسخ سرور، از `setTimeout` با مدت زمان **۸۰۰ میلیثانیه** استفاده کنید.
3. درون این تاخیر، **یک مقدار تصادفی با** `Math.random()` تولید میشود:
+ اگر مقدار تصادفی **کمتر** از `0.5` باشد **و** محصولی با `id` مورد نظر پیدا شود، *Promise* با محصول مورد نظر *resolve* شود.
+ **در غیر این صورت،** *Promise* با شیء خطا (دارای `status` و `message`) *reject* شود.
</details>
+ **نکته:** این منطق به شما کمک میکند **رفتار واقعی یک درخواست شبکه** *(موفق یا ناموفق بودن پاسخ سرور)* را شبیهسازی کنید.
</details>
طبق توضیحات زیر، باید فایلهای موجود در **دایرکتوری** `scripts` را تکمیل کنید.
+ **توجه:** در فایلها، کامنتهای راهنما درج شدهاند که به شما در تکمیل توابع کمک میکنند.
<details class="purple">
<summary>**پیادهسازی فایل** `index.js`</summary>
در این فایل هدف شما این است که **محصولات** را از `getAllProducts` دریافت کرده، نمایش دهید و وضعیت **بارگذاری** و **خطا** را مدیریت کنید.
1. ابتدا باید **سه المنت** را از صفحه `index.html` دریافت کنید:
+ `spinner`: برای **نمایش لودر** در هنگام بارگذاری.
+ `errorBox`: برای **نمایش پیام خطا.**
+ `productsContainer`: **برای نمایش لیست محصولات.**
<details class="yellow">
<summary>**پیادهسازی تابع** `renderProducts`</summary>
این تابع مسئول **نمایش محصولات** است.
ورودی تابع یک آرایه از محصولات است و خروجی آن افزودن کارتهای محصولات به `productsContainer` میباشد.
### وظایف تابع:
1. برای هر محصول یک **کارت محصول** بسازید.
2. اطلاعات نمایش داده شده برای هر محصول شامل موارد زیر باشد:
+ تصویر (`image`) با کلاس `card-img`
+ عنوان محصول (`title`)
+ توضیحات محصول (`description`)
+ قیمت محصول (`price`) با فرمت `toLocaleString()` و واحد تومان
+ دستهبندی محصول (`category`)
+ وضعیت موجودی (`storage`) با کلاسهای `instock` یا `outstock` و متن مناسب:
+ موجود در انبار → `موجود در انبار`
+ ناموجود → `ناموجود`
3. هر کارت باید روی کلیک کاربر، به صفحه جزئیات محصول (`productDetail.html`) با `id` همان محصول هدایت شود.
ساختار کارت محصول به شکل زیر است
```js
<div class="card"">
<img src="${p.image}" alt="${p.title}" class="card-img"/>
<h2>${p.title}</h2>
<p>${p.description}</p>
<span class="price">قیمت: ${p.price.toLocaleString()} تومان</span>
<span class="category">دستهبندی: ${p.category}</span>
<span class="${p.storage ? "instock" : "outstock"}">
${p.storage ? "موجود در انبار" : "ناموجود"}
</span>
</div>
```
</details>
<details class="yellow">
<summary>**تابع loadProducts**</summary>
این تابع **مسئول مدیریت بارگذاری و نمایش خطا** است و از `async/await` برای دریافت محصولات استفاده میکند.
### وظایف تابع:
1. هنگام شروع بارگذاری:
+ نمایش `spinner` → حذف کلاس `hidden`
+ مخفی کردن `errorBox` و `productsContainer` → افزودن کلاس `hidden`
2. فراخوانی تابع `getAllProducts()` و دریافت آرایه محصولات
3. نمایش محصولات با استفاده از `renderProducts`
4. پس از بارگذاری:
+ مخفی کردن `spinner`
+ اگر آرایه محصولات خالی بود → نمایش پیام خطا `"هیچ محصولی یافت نشد."`
+ در غیر این صورت → نمایش `productsContainer`
5. مدیریت خطا:
+ مخفی کردن `spinner`
+ نمایش پیام خطا از `err.message` در `errorBox`
</details>
**نکته:** فراموش نکنید که **تابع** `loadProducts` را فراخوانی کنید!
**نکته:** در داخل تابع `loadProducts` باید از *try/catch* برای **مدیریت خطاها** استفاده شود.
</details>
<details class="purple">
<summary>**فایل** `productDetail.js`</summary>
این فایل مسئول **نمایش جزئیات یک محصول** است که با استفاده از شناسه (`id`) محصول، اطلاعات آن را از *API* دریافت کرده و در صفحه نمایش میدهد.
1. ابتدا باید سه المنت را از صفحه `productDetail.html` دریافت کنید:
+ `spinner`: **برای نمایش لودر در هنگام بارگذاری.**
+ `errorBox`: **برای نمایش پیام خطا.**
+ `productDetail`: **برای نمایش محصول.**
<details class="yellow">
<summary>**تابع** `getIdFromUrl`</summary>
این تابع مسئول **استخراج شناسه محصول** (`id`) از *URL* صفحه است.
</details>
<details class="yellow">
<summary>**تابع** `renderProduct`</summary>
این تابع وظیفه نمایش جزئیات محصول در صفحه را دارد.
### **وظایف تابع:**
1. **مخفی کردن** `errorBox` و **پاک کردن پیامهای خطا**
2. **نمایش** `productDetail`
3. افزودن محتوای محصول شامل:
+ **تصویر محصول** (`product.image`) با جایگزین پیشفرض `"images/placeholder.png"`
+ **عنوان محصول** (`product.title`)
+ **توضیحات محصول** (`product.description`)
+ **قیمت محصول** (`product.price.toLocaleString()`)
+ **دستهبندی محصول** (`product.category`)
+ **وضعیت موجودی محصول** (`product.storage`)
4. **افزودن دکمه خرید** (`buy-btn`) که با کلیک کاربر به صفحه `checkout.html` با همان `id` هدایت شود.
ساختار جزئیات هر محصول:
```js
<img src="${product.image || "images/placeholder.png"}" alt="${
product.title
}" />
<h2>${product.title}</h2>
<p class="description">${product.description}</p>
<p><strong>قیمت:</strong> ${product.price.toLocaleString()} تومان</p>
<p><strong>دستهبندی:</strong> ${product.category}</p>
<p><strong>وضعیت:</strong> ${
product.storage ? "✅ موجود در انبار" : "❌ ناموجود"
}</p>
<button
class="buy-btn"
onclick="window.location.href='checkout.html?id=${encodeURIComponent(
product.id
)}'">
خرید محصول
</button>
```
</details>
<details class="yellow">
<summary>**تابع** `loadProduct`</summary>
این تابع مسئول **مدیریت بارگذاری محصول و نمایش خطا** است و از `async/await` استفاده میکند.
### وظایف تابع:
1. هنگام شروع بارگذاری:
+ نمایش `spinner`
+ مخفی کردن `errorBox` و `productDetail`
+ پاک کردن محتوای قبلی `productDetail`
2. دریافت `id` از *URL* با `getIdFromUrl()`
3. فراخوانی `getProductById(id)` برای دریافت محصول
4. در صورت موفقیت، **فراخوانی** `renderProduct(product)`
5. در صورت خطا، **نمایش پیام خطا** در `errorBox`
6. در نهایت، **مخفی کردن** `spinner`
</details>
**نکته:** فراموش نکنید که تابع `loadProduct` را فراخوانی کنید!
**نکته:** در داخل **تابع** `loadProduct` باید از *try/catch* برای مدیریت خطاها استفاده شود.
</details>
<details class="purple">
<summary>**فایل** `checkout.js`</summary>
+ این فایل مسئول مدیریت **فرم چندمرحلهای پرداخت** *(Checkout)* است و شامل منطق تغییر مراحل، اعتبارسنجی فیلدها و نمایش خلاصه سفارش است. المانهایی که باید با آنها کار کنید به شکل زیر است:
```js
const steps = document.querySelectorAll(".step"); // مراحل پروگرس
const forms = document.querySelectorAll(".form-step"); // فرمهای هر مرحله
const nextBtns = document.querySelectorAll(".next"); // دکمههای مرحله بعد
const prevBtns = document.querySelectorAll(".prev"); // دکمههای مرحله قبل
const lines = document.querySelectorAll(".line"); // خطوط پروگرس
```
<details class="yellow">
<summary>**تابع** `getIdFromUrl`</summary>
این `id` برای **دریافت اطلاعات محصول** از *API* در مرحله خلاصه استفاده میشود.
+ در صورت **نبودن** `id` در *URL،* اطلاعات محصول در مرحله خلاصه با **علامت** "—" نمایش داده میشود.
</details>
<details class="yellow">
<summary>**تابع** *showStep*</summary>
این تابع **مرحله جاری فرم را نمایش میدهد**:
+ فرم مربوط به مرحله جاری **فعال** میشود (`active`)
+ مراحل تکمیل شده و خطوط پروگرس نیز **فعال** میشوند
+ اگر مرحله آخر *(Step 3)* باشد، تابع `fillSummary` فراخوانی میشود
**کلاسهای CSS:**
| کلاس | کاربرد |
| ------------- | ------------------------ |
| `active` | نمایش فرم یا مرحله فعال |
| `line active` | رنگ و پر شدن خطوط پروگرس |
+ **نکته:** مرحله بعد تنها زمانی قابل نمایش است که **تمام ورودیهای مرحله جاری پر شده باشند**.
</details>
<details class="yellow">
<summary>**اعتبارسنجی** `validateStep`</summary>
**تابع** `validateStep(index)`:
+ بررسی همه ورودیها، سلکتها و تکستاریاهایی که `required` هستند
+ اگر ورودی خالی باشد، **کلاس** `error` **اضافه** میشود و **فوکوس** روی آن قرار میگیرد
+ اگر ورودی پر باشد، **کلاس** `error` **حذف** میشود
+ اگر همه ورودیها پر باشند، `true` **برمیگرداند** و **اجازه رفتن به مرحله بعد داده میشود**
**کلاس CSS:**
+ `error` نمایش ورودی با خطا (مثلاً رنگ قرمز)
+ **توجه:** کاربر نمیتواند به مرحله بعد برود تا زمانی که تمام فیلدهای الزامی پر شده باشند.
</details>
<details class="yellow">
<summary>**توضیح مراحل فرم**</summary>
| مرحله | شرح |
| ------------------------ | ---------------------------------------------------------------------------------------------- |
| **Step 1: اطلاعات شخصی** | ورودیها: `name`، `lastname`، `code`، `phone` (تمامی فیلدها `required`) |
| **Step 2: اطلاعات آدرس** | ورودیها: `province`، `city`، `address` (تمامی فیلدها `required`) |
| **Step 3: خلاصه خرید** | نمایش اطلاعات جمعآوریشده از مراحل ۱ و ۲ و اطلاعات محصول از API. دکمه `submit` برای ثبت نهایی |
+ ** توجه:** مرحله ۳ صرفاً نمایش اطلاعات است و ورودی جدیدی ندارد.
</details>
<details class="yellow">
<summary>**پر کردن خلاصه سفارش** `fillSummary`</summary>
+ **اطلاعات شخصی** و **آدرس** از فرمهای مراحل قبل خوانده میشوند
+ **اطلاعات محصول** با استفاده از `getProductById` دریافت میشوند
+ اطلاعات در المنتهای خلاصه (`summary-*`) نمایش داده میشوند
**مثال المنتها:**
```js
document.getElementById("summary-name").textContent = ...;
document.getElementById("summary-product").textContent = ...;
document.getElementById("summary-price").textContent = ...;
```
+ **توجه:** در صورت خطای دریافت محصول، پیام مناسب نمایش داده شود.
</details>
<details class="yellow">
<summary>**دکمههای** *Next* **و** *Prev*</summary>
1. **دکمهی Next:** ابتدا `validateStep(currentStep)` اجرا میشود و فقط در صورت اعتبارسنجی موفق، به مرحله بعد میرود
2. **دکمهی Prev:** بدون اعتبارسنجی به مرحله قبل برمیگردد
</details>
<details class="yellow">
<summary>**کلیک روی مراحل Stepper**</summary>
1. کاربر میتواند فقط به مراحل تکمیل شده یا فعلی برود
2. اگر مرحله بالاتر را انتخاب کند، ابتدا اعتبارسنجی همه مراحل قبل انجام میشود
</details>
<details class="yellow">
<summary>**دکمهی submit**</summary>
1. بررسی اعتبارسنجی مرحله آخر
2. در صورت موفقیت، هدایت به صفحه `success.html`
</details>
<details class="yellow">
<summary>**مدیریت ورودیها**</summary>
1. هر ورودی با `required` هنگام تغییر مقدار، کلاس `error` حذف میشود
2. **جلوگیری** از نمایش خطا پس از اصلاح ورودی توسط کاربر
</details>
<details class="blue">
<summary>**کلاسهای مهم** *CSS*</summary>
| کلاس | کاربرد |
| --------------- | -------------------------- |
| `active` | فرم یا مرحله فعال |
| `error` | ورودی اشتباه |
| `line active` | پر شدن خطوط پروگرس |
| `next` / `prev` | دکمههای ناوبری بین مراحل |
| `submit` | دکمه ثبت نهایی |
| `summary-*` | نمایش اطلاعات جمعآوری شده |
</details>
</details>
# **آنچه باید آپلود کنید**
+ **توجه**: پس از اعمال تغییرات، فایلهای داخل `scripts` و `api` را _Zip_ کرده و آپلود کنید. **همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.**
<details class="red">
<summary>**ساختار مناسب برای آپلود پاسخ**</summary>
```
answer/
├─ api/
│ ├─ getAllProducts.js
│ └─ getProductById.js
└─ scripts/
├─ checkout.js
├─ index.js
└─ productDetail.js
```
</details>
----------
از آنجایی که فرانتیوم علاقهی زیادی به گربهها داشت، تعداد زیادی گربه در خانهاش نگه میداشت.
او برای آنها یک زمین بازی در حیاط خانهاش ساخته بود. در طول روز، همیشه تعدادی از گربهها در زمین بازی بودند، اما گاهی فرانتیوم نمیدانست کدام گربهها مشغول بازی هستند.
به همین دلیل به ذهنش رسید که چرا برای گربهها یک سامانهی ورود و خروج درست نکند؟

# **پروژهی اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/contest/assignments/91619/download_problem_initial_project/310784/) کلیک کنید.
<details class="red">
<summary>**ساختار فایلها**</summary>
```
samane/
├─ public/
│ └─ vite.svg
├─ src/
│ ├─ api/
│ │ ├─ attendance.api.ts
│ │ ├─ client.ts
│ │ ├─ products.api.ts
│ │ └─ users.api.ts
│ ├─ app/
│ │ ├─ App.tsx
│ │ └─ routes.tsx
│ ├─ components/
│ │ ├─ Layout/
│ │ │ ├─ MainLayout.tsx
│ │ │ └─ Sidebar.tsx
│ │ ├─ Modal/
│ │ │ └─ GenericModal.tsx
│ │ └─ Table/
│ │ └─ GenericTable.tsx
│ ├─ features/
│ │ ├─ dashboard/
│ │ │ ├─ DashboardPage.tsx
│ │ │ ├─ EntryModal.tsx
│ │ │ ├─ ExitModal.tsx
│ │ │ └─ useAttendance.ts
│ │ ├─ products/
│ │ │ ├─ ProductFormModal.tsx
│ │ │ ├─ ProductPage.tsx
│ │ │ └─ useProducts.ts
│ │ └─ users/
│ │ ├─ starter/
│ │ │ ├─ UserFormModal_starter.tsx
│ │ │ └─ useUsers_starter.ts
│ │ ├─ UserFormModal.tsx
│ │ ├─ UsersPage.tsx
│ │ └─ useUsers.ts
│ ├─ types/
│ │ └─ index.ts
│ ├─ index.css
│ └─ main.tsx
├─ eslint.config.js
├─ index.html
├─ package.json
├─ README.md
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts
```
</details>
# **پیشنمایشهای پیادهسازی**
در گیفهای زیر، میتوانید تمام عملکرد برنامه را مشاهده کنید:



# **جزئیات پیادهسازی**
<details class="purple">
<summary>**فایل index.ts**</summary>
در این فایل شما باید **تایپهای مورد نیاز پروژه** را تعریف کنید، چرا که در ادامه در **تمامی کامپوننتها، فرمها و APIها** از این تایپها استفاده خواهد شد.
کامنتهای موجود در فایل به طور دقیق مشخص کردهاند که **هر تایپ چه پراپرتیهایی** نیاز دارد و نوع دادهی آنها چیست. بنابراین با دقت و صرف زمان مناسب، تایپها را **به درستی و کامل** تعریف کنید تا در مراحل بعدی پروژه با خطاهای تایپاسکریپتی مواجه نشوید.
**نکات مهم هنگام پیادهسازی:**
+ از نامهای پیشنهادی در کامنتها استفاده کنید تا هماهنگی با سایر بخشها حفظ شود.
+ برای مقادیر محدود، مثل جنسیت، حتماً **نوع محدود (union type)** تعریف کنید تا مقدار دیگری پذیرفته نشود.
+ پراپرتیهای اختیاری را با `?` مشخص کنید.
+ تایپهای مرتبط با پاسخ API یا payload ها باید دقیقاً مشابه کامنتها تعریف شوند تا سایر بخشها بتوانند بدون خطا با آنها کار کنند.
**توجه:** پس از تکمیل تایپها حتما تمام کامنتها را پاک کنید!
</details>
<details class="purple">
<summary>**کامپوننت main.tsx**</summary>
+ یک `QueryClient` بسازید و آن را با `QueryClientProvider` در بالاترین سطح پروژه قرار دهید.
+ بعد از اینکار، میتوانید در کل پروژه از `useQuery`, `useMutation` و سایر hooks های React Query استفاده کنید.
+ این ساختار پایه برای مدیریت state های async و دادههای API است و نیازی به تغییر `App` یا فایلهای دیگر نیست.
</details>
<details class="purple">
<summary>**پیادهسازی Routes.ts و App.tsx**</summary>
<details class="yellow">
<summary>**فایل Routes.ts**</summary>
**وظیفه:**
+ پروژه را به router وصل میکند تا مسیریابی در اپلیکیشن کار کند.
+ بالاترین سطح اپلیکیشن را مدیریت میکند و `RouterProvider` از `react-router-dom` را رندر میکند.
**نکات مهم:**
+ `router` که از فایل `routes.tsx` صادر شده، باید به `RouterProvider` داده شود.
</details>
<details class="yellow">
<summary>**کامپوننت App.tsx**</summary>
**وظیفه:**
+ تعریف مسیرهای اپلیکیشن و تعیین component هایی که در هر مسیر نمایش داده میشوند.
+ استفاده از `createBrowserRouter` برای مدیریت مسیرهای SPA.
**نکات مهم:**
+ مسیر اصلی `/` با `MainLayout` نمایش داده میشود.
+ `MainLayout` شامل نوار کناری، header و فضای نمایش صفحات است.
+ مسیرهای داخلی:
+ `/` → `DashboardPage`
+ `/users` → `UsersPage`
+ `/products` → `ProductPage`
+ مسیرها باید با `children` زیر `MainLayout` تعریف شوند تا layout ثابت بماند.
</details>
</details>
<details class="purple">
<summary>**دایرکتوری api**</summary>
<details class="yellow">
<summary>**فایل client.ts**</summary>
## پیادهسازی `axiosClient`
### هدف
ساخت یک **کلاینت استاندارد Axios** برای مدیریت یکپارچهی درخواستها و پاسخهای API.
این کلاینت باید در تمام بخشهای پروژه برای ارتباط با سرور استفاده شود و مسئول مدیریت خطاها نیز باشد.
### مقدمه
[`axios`](https://axios-http.com/docs/intro) یک کتابخانهی HTTP Client برای مرورگر و Node.js است که:
+ از `Promise` پشتیبانی میکند.
+ امکان ارسال درخواستهای `GET`, `POST`, `PUT`, `DELETE` و ... را فراهم میکند.
+ با TypeScript سازگار است و قابلیت تایپ کردن `request` و `response` دارد.
### گامهای پیادهسازی
1. **ایجاد کلاینت Axios**
ابتدا یک کلاینت Axios با تنظیمات پایه بسازید:
`baseURL`
تمام مسیرهای API باید با مقدار `/api` آغاز شوند. مثلاً اگر `axiosClient.get("/users")` فراخوانی شود، مسیر نهایی `/api/users` خواهد بود.
`headers`
هدر پیشفرض تمام درخواستها باید شامل `"Content-Type": "application/json"` باشد.
2. **افزودن Interceptor برای پاسخها**
+ در `axios` میتوانید از **interceptors** استفاده کنید تا قبل یا بعد از ارسال درخواست، منطق خاصی اجرا کنید.
+ در این پروژه، فقط نیاز است **interceptor پاسخ (response interceptor)** را پیادهسازی کنید.
+ این interceptor باید:
+ در حالت موفق، پاسخ را بدون تغییر بازگرداند.
+ در حالت خطا، جزئیات خطا (مثلاً `error.response`) را در کنسول چاپ کند تا در زمان توسعه قابل بررسی باشد.
+ سپس، خطا را بازگرداند تا در لایههای بالاتر (مثلاً در React Query) بتوان آن را مدیریت کرد.
3. **برگرداندن مقدار نهایی**
+ در پایان، `axiosClient` را export کنید تا در سایر بخشهای پروژه قابل استفاده باشد.
#### نکته
در این فایل نیازی به مدیریت توکن یا احراز هویت نیست. هدف تنها ایجاد یک ساختار اولیهی استاندارد برای ارسال درخواستها و هندل کردن خطاهاست.
</details>
<details class="yellow">
<summary>**فایل users.api.ts**</summary>
این فایل شامل سه تابع اصلی برای مدیریت دادههای کاربر بهصورت **mock (شبیهسازیشده)** است.
هدف از این فایل، فراهم کردن رابطی ساده برای دریافت، اضافهکردن و ریستکردن لیست کاربران است.
<details class="purple">
<summary>**تابع getUsers**</summary>
**توضیح:**
این تابع برای دریافت لیست کاربران استفاده میشود.
در صورت ارسال پارامتر `q`، کاربران بر اساس نام یا نام خانوادگی فیلتر میشوند.
**ورودی:**
+ `q` (اختیاری): رشتهای برای جستوجو در نام یا نام خانوادگی.
**خروجی:**
+ `Promise<User[]>` → لیستی از کاربران (بهصورت async).
</details>
<details class="purple">
<summary>**تابع createUser**</summary>
**توضیح:**
برای افزودن کاربر جدید به لیست استفاده میشود.
تابع بهصورت خودکار شناسه (id) تولید کرده و کاربر جدید را به آرایه اضافه میکند.
**ورودی:**
+ `newUser`: شیئی شامل مشخصات کاربر **بهجز فیلد `id`**
(زیرا `id` بهصورت خودکار ساخته میشود)
**خروجی:**
+ `Promise<User>` → کاربر ایجاد شده همراه با `id` جدید.
</details>
<details class="purple">
<summary>**تابع resetUsers**</summary>
**توضیح:**
لیست کاربران را به حالت اولیه (۱۰ کاربر اول) برمیگرداند.
در واقع تمام کاربران جدید اضافهشده حذف میشوند.
</details>
**نکته:** تمام توابع بهصورت شبیهسازیشده دارای تأخیر (delay) هستند تا رفتار واقعی API را تداعی کنند.
</details>
<details class="yellow">
<summary>**فایل products.api.ts**</summary>
این فایل شامل توابعی برای **مدیریت دادههای محصولات بهصورت mock** است.
<details class="purple">
<summary>**تابع getProducts**</summary>
**توضیح:**
این تابع لیست کامل محصولات را برمیگرداند.
همه دادهها از یک آرایهی شبیهسازیشده (`mockProducts`) خوانده میشوند.
**ورودی:**
ندارد.
**خروجی:**
+ `Promise<Product[]>` → لیستی از تمام محصولات.
</details>
<details class="purple">
<summary>**تابع createProduct**</summary>
**توضیح:**
برای افزودن محصول جدید استفاده میشود.
شناسه (`id`) بهصورت خودکار تولید شده و محصول جدید به لیست افزوده میشود.
**ورودی:**
+ `newProduct`: شیئی شامل نام و قیمت محصول (بدون `id`).
**خروجی:**
+ `Promise<Product>` → محصول ایجادشده همراه با `id` تصادفی.
</details>
<details class="purple">
<summary>**تابع resetProducts**</summary>
**توضیح:**
لیست محصولات را به حالت اولیه بازمیگرداند.
تمام محصولاتی که بعداً اضافه شدهاند حذف میشوند و دادهها به آرایهی اولیه برمیگردند.
</details>
**نکته:** همهی توابع شامل تأخیر مصنوعی هستند تا رفتار واقعی درخواستهای شبکه را شبیهسازی کنند
</details>
<details class="yellow">
<summary>**فایل attendance.api.ts**</summary>
این فایل مسئول **مدیریت ورود و خروج کاربران** و محاسبهی هزینهی حضور آنهاست.
همچنین ارتباط بین کاربران (`user`)، محصولات (`product`) و سوابق حضور (`attendance`) را شبیهسازی میکند.
## ثابتها و تنظیمات
```ts
const BASE_FEE_PER_HOUR = 10000;
```
هر کاربر بهازای هر **ساعت حضور** باید مبلغ ۱۰٬۰۰۰ تومان پرداخت کند.
هزینه بر اساس دقیقه محاسبه میشود (`ratePerMinute = BASE_FEE_PER_HOUR / 60`).
<details class="purple">
<summary>**تابع getPresentUsers**</summary>
**توضیح:**
لیست تمام کاربرانی را برمیگرداند که **وارد شدهاند اما هنوز خارج نشدهاند**.
**ورودی:**
ندارد.
**خروجی:**
+ `Promise<AttendanceRecord[]>` → لیست کاربران حاضر.
</details>
<details class="purple">
<summary>**تابع enterUser**</summary>
**توضیح:**
ثبت ورود یک کاربر جدید به سیستم.
اگر کاربر هنوز خارج نشده باشد، خطا میدهد.
**ورودی:**
+ `payload.userId` → شناسهی کاربر.
+ `payload.enteredAt` (اختیاری) → زمان ورود (در صورت عدم ارسال، زمان فعلی ثبت میشود).
**خروجی:**
+ `Promise<AttendanceRecord>` → رکورد ثبتشدهی ورود.
</details>
<details class="purple">
<summary>**تابع exitUser**</summary>
**توضیح:**
ثبت خروج کاربر از سیستم و محاسبهی هزینهی نهایی.
در زمان خروج، مدت حضور و محصولات خریداریشده لحاظ میشوند.
**مراحل عملکرد تابع:**
1. بررسی میکند که رکورد حضور وجود داشته باشد و خروج قبلاً ثبت نشده باشد.
2. مدت حضور (برحسب دقیقه) محاسبه میشود.
3. هزینهی پایه بر اساس زمان محاسبه میشود.
4. قیمت محصولات انتخابشده (از `getProducts()`) جمع زده میشود.
5. مجموع کل (`total`) = `baseFee + productsTotal`.
6. مقدار خروج (`exitedAt`) و محصولات (`productIds`) در رکورد ذخیره میشوند.
</details>
<details class="purple">
<summary>**تابع resetAttendance**</summary>
**توضیح:**
تمام سوابق حضور را پاک کرده و سیستم را به حالت اولیه بازمیگرداند.
</details>
**نکته:** مثل سایر فایلهای API، همهی توابع شامل تأخیر شبیهسازیشده هستند
</details>
</details>
<details class="purple">
<summary>**دایرکتوری components**</summary>
<details class="yellow">
<summary>**فایل Layout**</summary>
<details class="purple">
<summary>**کامپوننت MainLayout**</summary>
در این فایل، ساختار اصلی صفحه تعریف شده است که شامل دو بخش اصلی میباشد:
+ `Sidebar` برای نمایش منوی کناری
+ `Content` برای نمایش محتوای متغیر (داینامیک)
در قسمت مشخصشده با کامنت `Content`، باید محتوای هر صفحه (مثلاً داشبورد، کاربران، تنظیمات و ...) بهصورت **داینامیک** نمایش داده شود.
هدف این است که هنگام تغییر مسیر (Route)، فقط بخش محتوایی بهروز شود و ساختار کلی صفحه (مثل Sidebar) ثابت بماند.
</details>
<details class="purple">
<summary>**کامپوننت Sidebar.tsx**</summary>
در این فایل باید منوی کناری (Sidebar) را از نظر **منطق عملکرد** پیادهسازی کنید.
تمام ساختار و استایل مورد نیاز از قبل آماده است، اما هیچ رفتار یا دادهی منطقی وجود ندارد.
### هدف:
نمایش یک منوی ناوبری در سمت راست صفحه که کاربر بتواند با کلیک بر روی هر آیتم، بین صفحات مختلف جابهجا شود و آیتم فعال مشخص باشد.
### گامهای پیادهسازی
#### 1. **ایمپورتهای مورد نیاز**
از کتابخانههای زیر استفاده کنید:
+ `useNavigate` و `useLocation` از `react-router-dom` برای مدیریت مسیرها
+ آیکونهای مورد نیاز از `@mui/icons-material`
(مثلاً `DashboardIcon`, `PeopleIcon`, `InventoryIcon`)
#### 2. **ایجاد آرایه آیتمها**
در داخل کامپوننت، آرایهای به نام `items` بسازید که شامل آبجکتهایی با ساختار زیر باشد:
```ts
{
text: string; // عنوان آیتم (مثلاً "کاربران")
icon: JSX.Element; // آیکون مربوطه
path: string; // مسیر صفحه (مثلاً "/users")
}
```
به عنوان مثال، سه آیتم با مسیرهای زیر نیاز است:
+ `/` برای صفحهی داشبورد
+ `/users` برای صفحهی کاربران
+ `/products` برای صفحهی هزینههای مازاد
#### 3. **رندر کردن آیتمها**
درون تگ `<List>`، از متد `map` برای نمایش تمام آیتمها استفاده کنید.
در هر `ListItemButton`:
+ مسیر فعلی را با `location.pathname` مقایسه کنید تا آیتم فعال (`selected`) مشخص شود.
+ هنگام کلیک، با استفاده از `navigate(path)` کاربر را به صفحهی جدید هدایت کنید.
#### 4. **نکته مهم**
شما **نباید ساختار، استایل یا layout** را تغییر دهید.
فقط منطق مربوط به:
+ ایجاد آرایه آیتمها
+ تشخیص مسیر فعال
+ جابهجایی بین صفحات
را پیادهسازی کنید.
</details>
</details>
<details class="yellow">
<summary>**فایل Modal**</summary>
</details>
<details class="yellow">
<summary>**فایل Table**</summary>
در این بخش باید یک **کامپوننت عمومی جدول (Generic Table)** پیادهسازی کنید که بتواند هر نوع دادهای را بر اساس ستونهای دادهشده نمایش دهد.
### هدف:
ساخت کامپوننتی که با دریافت آرایهای از ستونها و دادهها، جدول را بهصورت داینامیک نمایش دهد.
این کامپوننت باید بتواند برای هر نوع دادهای (Users، Products، Records و...) قابل استفاده باشد.
### مشخصات Props
#### 1. `columns: Column[]`
هر ستون شامل دو مقدار است:
```ts
{
key: string; // کلید مربوط به فیلد داده (مثلاً "name" یا "price")
label: string; // عنوان نمایشی در هدر جدول
}
```
#### 2. `data: T[]`
آرایهای از آبجکتها که هرکدام یک ردیف جدول هستند.
هر شیء میتواند شامل هر کلید دلخواهی باشد (بنابراین باید از `Generic Type` استفاده کنید).
### وظیفه شما
درون جدول، باید موارد زیر را پیادهسازی کنید:
#### بخش سرستون (TableHead)
+ با استفاده از `columns.map` تمام ستونها را رندر کنید.
+ مقدار `col.label` باید در هر سلول نمایش داده شود.
+ جهت متن در سلولها باید راستچین باشد (`align="right"`).
#### بخش بدنه (TableBody)
+ با استفاده از `data.map`، تمام سطرهای جدول را ایجاد کنید.
+ برای هر ردیف (`row`) از `columns.map` استفاده کنید تا ترتیب ستونها مطابق `columns` باشد.
+ مقدار هر سلول باید از `row[col.key]` خوانده شود.
### نکات مهم
1. جدول باید **کاملاً داینامیک** باشد — هیچ ستونی نباید بهصورت دستی نوشته شود.
2. برای کلیدهای `TableRow` و `TableCell` از مقادیر منحصربهفرد (مثل index یا key) استفاده کنید.
3. نوع دادهی ورودی را بهصورت **Generic** تعریف کنید تا برای هر مدل داده قابل استفاده باشد:
```ts
const GenericTable = <T extends Record<string, any>>({...}: GenericTableProps<T>)
```
### نتیجه مورد انتظار
هنگام استفاده از این کامپوننت به شکل زیر:
```tsx
<GenericTable
columns={[
{ key: "name", label: "نام" },
{ key: "price", label: "قیمت" },
]}
data={[
{ name: "آب معدنی", price: 2000 },
{ name: "چیپس", price: 3000 },
]}
/>
```
</details>
</details>
<details class="purple">
<summary>**دایرکتوری features**</summary>
در این بخش سه ویژگی (Feature) با نامهای زیر وجود دارد:
1. **users**
2. **products**
3. **dashboard**
در ادامه، هر سه دایرکتوری را بهصورت جداگانه بررسی میکنیم و تمام مواردی را که باید پیادهسازی کنید، توضیح خواهیم داد.
<details class="yellow">
<summary>**هوکها**</summary>
در هر فایل باید یکسری هوک نوشته شوند که آنها را شرح میدهیم
<details class="purple">
<summary>**فایل useUsers.ts**</summary>
در این فایل، دو هوک مرتبط با مدیریت کاربران باید پیادهسازی شوند:
1. **`useUsers`**
2. **`useCreateUser`**
هر دو هوک از کتابخانهی **React Query (TanStack Query)** برای مدیریت دادههای سمت سرور و بهروزرسانی خودکار UI استفاده میکنند.
### پیادهسازی هوک `useUsers`
در این بخش، باید از هوک `useQuery` مربوط به **React Query** استفاده کنید.
مراحل کلی به این صورت است:
1. ابتدا یک کلید ثابت برای کش دادهها با نام مثلاً `"users"` تعریف کنید.
2. سپس از `useQuery` برای فراخوانی تابعی مانند `getUsers` استفاده کنید که دادهها را از API برمیگرداند.
3. اگر در ورودی هوک یک مقدار جستجو (`q`) دریافت میکنید، آن را هم در `queryKey` لحاظ کنید تا کش برای هر مقدار جستجو منحصربهفرد باشد.
4. در نهایت، هوک باید دادهها، وضعیت بارگذاری و خطا را برگرداند.
**نکته:**
تابع `getUsers` را از مسیر `../../api/users.api` ایمپورت کنید و نوع دادهی خروجی را از `User` تعریفشده در `../../types` بگیرید.
### پیادهسازی هوک `useCreateUser`
در این بخش باید از هوک `useMutation` استفاده کنید تا امکان **افزودن کاربر جدید** فراهم شود.
مراحل پیشنهادی:
1. از `useQueryClient` برای دسترسی به کش سراسری React Query استفاده کنید.
2. از `useMutation` برای فراخوانی تابعی مانند `createUser` بهره بگیرید. این تابع باید اطلاعات کاربر جدید را به API ارسال کند.
3. در بخش `onSuccess`، با استفاده از `invalidateQueries` کش مربوط به کلید `"users"` را پاک کنید تا پس از افزودن کاربر جدید، لیست کاربران بهروز شود.
**نکته:**
برای ورودی تابع ایجاد کاربر، از نوع `Omit<User, "id">` استفاده کنید تا شناسه (`id`) که توسط سرور ایجاد میشود، ارسال نگردد.
</details>
<details class="purple">
<summary>**فایل useProducts.ts**</summary>
در این فایل باید دو هوک برای مدیریت دادههای مربوط به محصولات پیادهسازی شوند:
1. **`useProducts`** برای دریافت فهرست محصولات از سرور
2. **`useCreateProduct`** برای ایجاد محصول جدید و بهروزرسانی دادهها پس از آن
### پیادهسازی هوک `useProducts`
در این قسمت، باید با استفاده از `useQuery` دادههای مربوط به محصولات را از API دریافت کنید.
مراحل کلی:
1. ابتدا یک کلید کش برای دادههای محصولات با نام `"products"` تعریف کنید تا React Query دادهها را بر اساس آن ذخیره و مدیریت کند.
2. سپس از `useQuery` برای فراخوانی تابع `getProducts` استفاده کنید. این تابع دادهها را از سرور برمیگرداند.
3. هوک باید دادهها، وضعیت بارگذاری (`isLoading`)، خطا (`error`) و سایر وضعیتهای مربوط به Query را برگرداند تا در کامپوننتهای دیگر قابل استفاده باشد.
**نکته:**
تابع `getProducts` باید از مسیر `../../api/products.api` ایمپورت شود و نوع دادهی خروجی آن `Product[]` باشد که از مسیر `../../types` در دسترس است.
### پیادهسازی هوک `useCreateProduct`
در این بخش باید هوکی برای ایجاد محصول جدید پیادهسازی کنید. برای این کار از `useMutation` استفاده میشود.
مراحل پیشنهادی:
1. از `useQueryClient` برای دسترسی به کش سراسری React Query استفاده کنید.
2. از `useMutation` برای ارسال دادههای محصول جدید با تابع `createProduct` بهره بگیرید.
3. در قسمت `onSuccess`، با استفاده از `invalidateQueries` کش مربوط به `"products"` را پاک کنید تا پس از ایجاد محصول، لیست محصولات بهروزرسانی شود.
**نکته:**
در این بخش باید نوع ورودی تابع ایجاد محصول را بهصورت `Omit<Product, "id">` تعریف کنید، چون شناسه محصول توسط سرور ساخته میشود.
</details>
<details class="purple">
<summary>**فایل useAttendance.ts**</summary>
در این فایل چهار هوک مهم برای مدیریت حضور و غیاب کاربران وجود دارد:
1. **`usePresentUsers`** → دریافت لیست کاربران حاضر
2. **`useEnterUser`** → ثبت ورود کاربر
3. **`useExitUser`** → ثبت خروج کاربر
4. **`useResetAttendance`** → ریست کردن دادههای حضور و غیاب
### پیادهسازی هوک `usePresentUsers`
+ این هوک برای دریافت تمام رکوردهای حاضر کاربران استفاده میشود.
+ باید از `useQuery` استفاده کنید و یک **کلید کش سراسری** تعریف کنید، مثلاً `"attendance"`.
+ تابع `queryFn` باید `getPresentUsers` باشد تا دادهها را از سرور بگیرد.
+ هوک باید وضعیت بارگذاری (`isLoading`)، دادهها (`data`) و خطا (`error`) را برگرداند تا در کامپوننتها قابل استفاده باشد.
### پیادهسازی هوک `useEnterUser`
+ این هوک برای ثبت ورود یک کاربر جدید است.
+ از `useMutation` استفاده کنید و تابع `mutationFn` آن `enterUser` باشد.
+ بعد از موفقیت، باید کش مربوط به `"attendance"` را با `invalidateQueries` تازه کنید تا لیست کاربران حاضر بروز شود.
+ نوع ورودی دادهها (`payload`) باید مطابق `EnterPayload` باشد.
### پیادهسازی هوک `useExitUser`
+ مشابه `useEnterUser` است ولی برای ثبت خروج کاربران.
+ `mutationFn` اینجا تابع `exitUser` است و بعد از موفقیت کش `"attendance"` باید تازه شود.
+ نوع دادهی ورودی `ExitPayload` و خروجی `ExitResponse` است.
### پیادهسازی هوک `useResetAttendance`
+ این هوک برای ریست کردن کل دادههای حضور و غیاب استفاده میشود.
+ از `useMutation` استفاده کنید و تابع `mutationFn` آن `resetAttendance` باشد.
+ پس از موفقیت، کش `"attendance"` را با `invalidateQueries` تازه کنید.
+ این هوک هیچ دادهای به عنوان ورودی نمیگیرد و خروجی آن هم `void` است.
</details>
</details>
</details>
این روزها افکار و احساسات متفاوت، ذهن **فرانتیوم** را آشفته کردهاند.
او بارها با خودش گفته بود:
**«از الان دیگه فکر کردن رو میذارم کنار و فقط تمرکز میکنم!»**
اما چند لحظه بعد، دوباره همه چیز از اول شروع میشد.
تا اینکه به یک نتیجه رسید: **نوشتن**
**فرانتیوم** که همیشه خلاق است، تصمیم گرفت چیزی شبیه به `notepad` بسازد تا بتواند افکارش را در آن بنویسد و ذهنش را آرام کند.

# **پروژهی اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/contest/assignments/91619/download_problem_initial_project/310783/) کلیک کنید.
<details class="red">
<summary>**ساختار فایلها**</summary>
```plaintext
little_shop/
├─ public/
├─ src/
│ ├─ components/
│ │ ├─ NoteCard.tsx
│ │ ├─ <mark class="orange" title="این فایل را تکمیل کنید">NoteEditor.tsx</mark>
│ │ └─ <mark class="orange" title="این فایل را تکمیل کنید">Toolbar.tsx</mark>
│ ├─ context/
│ │ └─ <mark class="orange" title="این فایل را تکمیل کنید">NotesContext.tsx</mark>
│ ├─ pages/
│ │ ├─ <mark class="orange" title="این فایل را تکمیل کنید">Home.tsx</mark>
│ │ └─ <mark class="orange" title="این فایل را تکمیل کنید">NotePage.tsx</mark>
│ ├─ <mark class="orange" title="این فایل را تکمیل کنید">App.tsx</mark>
│ ├─ index.css
│ ├─ main.tsx
│ └─ vite-env.d.ts
├─ eslint.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ README.md
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts
```
</details>
<details class="yellow">
<summary>**راه اندازی پروژه**</summary>
+ پس از دانلود کردن **فایل پروژه اولیه،** آن را از حالت فشرده خارج کنید.
+ سپس در ترمینال خود **دستور** `npm install` را **اجرا** کنید.
+ در نهایت پروژه را با استفاده از **دستور** `npm run dev` **اجرا** کنید.
در صورت نیاز از **دستور** `npm install --force` برای **نصب وابستگیها** استفاده کنید.
</details>
# **پیشنمایشهای پیادهسازی**
در گیفهای زیر، میتوانید تمام عملکرد برنامه را مشاهده کنید:
+ **ساخت نوت:**

+ **عملکرد** `toolbar`:

+ **حذف نوت:**

# **جزئیات پیادهسازی**
در باکسهای زیر، بهطور کامل توضیح داده شده که **هر فایل** یا **هر کامپوننت** چگونه باید پیادهسازی شود. با توجه به این توضیحات، راهنماییها را با دقت دنبال کنید. همچنین در تمام فایلها، **کامنتهای راهنمای مفیدی** وجود دارد که با مطالعه آنها و داکیومنت میتوانید پروژه را بهطور کامل پیادهسازی کنید.
<details class="purple">
<summary>**کامپوننت** `App.tsx`</summary>
### **استفاده از** *Context* **برای مدیریت نوتها**
+ پروژه شما **یک منبع مرکزی** برای نوتها دارد. بنابراین، ابتدا باید یک *Context Provider* برای نوتها داشته باشید.
+ این *Provider* باید **تمام کامپوننتهایی که نیاز به دسترسی به نوتها دارند را احاطه کند.**
### **راهاندازی مسیرها با** *React Router*
+ اپ شما چند صفحه دارد: **صفحه اصلی** و **صفحه نمایش جزئیات یک نوت.**
+ برای **مدیریت مسیرها** از `BrowserRouter` استفاده کنید.
+ **مسیرها** (`Routes`) رو به شکل زیر طراحی کنید:
1. **مسیر اصلی** (`"/"`) که صفحه *Home* را نمایش میدهد.
2. **مسیر جزئیات نوت** (`"/note/:id"`) که صفحه *NotePage* را نمایش میدهد.
+ `:id` یک **پارامتر داینامیک** است که مشخص میکند کدام نوت را **نمایش** دهیم.
### **ترکیب** *Context* **و** *Router*
+ **پرووایدر** *(Provider)* نوتها باید **تمام** *Router* را احاطه کند. به این صورت، تمامی صفحات به *Context* دسترسی خواهند داشت. ساختار منطقی آن به شکل زیر خواهد بود:
```plaintext
NotesProvider
└── BrowserRouter
└── Routes
├── Route "/"
└── Route "/note/:id"
```
### **اجزای صفحات**
+ **صفحه اصلی** (`Home`) مسئول **نمایش همه نوتها** و **گزینه ایجاد نوت جدید** میباشد.
+ **صفحه جزئیات نوت** (`NotePage`) مسئول **نمایش** و **ویرایش** یک نوت خاص میباشد که `id` آن از **پارامتر مسیر** گرفته میشد.
### **نکات کلیدی**
+ وقتی `NotesProvider` را روی *Router* قرار میدهید، میتونید در داخل هر *Route* از *Context* استفاده کنید.
</details>
<details class="purple">
<summary>**فایل** `NotesContext.tsx`</summary>
در این فایل **ساختار اصلی** *Context* مربوط به نوتها آماده شده است. این *Context* وظیفه دارد **اطلاعات یادداشتها** (`notes`) را **مدیریت** کرده و **در اختیار سایر بخشهای برنامه قرار دهد.**
کد اولیه شامل تعریف **نوع دادهها** (`Note`)، **نوع ساختار** *Context* (`NotesContextType`) و اسکلت اصلی کامپوننت `NotesProvider` میباشد. شما باید با **تکمیل قسمتهای مشخصشده** با `TODO`، عملکرد کامل این *Context* را پیادهسازی کنید.
## **هدف این فایل**
ساخت یک *Context* برای:
+ نگهداری لیست یادداشتها در حالت (state)
+ همگامسازی دادهها با `localStorage`
+ فراهمکردن سه تابع برای مدیریت نوتها:
1. `addNote` برای **افزودن نوت جدید**
2. `updateNote` برای **ویرایش نوت موجود**
3. `removeNote` برای **حذف نوت بر اساس شناسه**
## **بخشهایی که باید پیادهسازی شوند**
<details class="yellow">
<summary>**مقداردهی اولیهی** `State`</summary>
با استفاده از `useState` باید:
+ **دادههای موجود** در `localStorage` با **کلید** `note_app_notes_v1` را بخوانید.
+ اگر دادهای وجود دارد، آن را با `JSON.parse` **تبدیل** کنید.
+ اگر **خطایی** رخ داد یا دادهای وجود **نداشت،** آرایهای خالی (`[]`) برگردانید.
> **نکته:** این مقداردهی **فقط یکبار** در هنگام اجرای اولیه انجام میشود.
</details>
<details class="yellow">
<summary>**ذخیرهسازی تغییرات در** `LocalStorage`</summary>
باید با استفاده از `useEffect` کاری کنید که **هر بار** `notes` تغییر کند، **مقدار جدید** در `localStorage` ذخیره شود.
+ **کلید ذخیره** باید همان مقدار `STORAGE_KEY` باشد.
+ از `JSON.stringify(notes)` برای **ذخیره دادهها** استفاده کنید.
</details>
<details class="yellow">
<summary>**پیادهسازی تابع** `addNote`</summary>
در این تابع، باید **نوت جدید** را به **ابتدای** آرایه اضافه کنید.
> یعنی **جدیدترین** نوتها در **بالاترین قسمت لیست** قرار بگیرند.
</details>
<details class="yellow">
<summary>**پیادهسازی تابع** `updateNote`</summary>
+ نوتی که `id` آن برابر با `id` ورودی است را پیدا کنید.
+ **فیلدهای جدید** (از `patch`) را با **نوت قبلی** ترکیب کنید.
+ **مقدار** `updatedAt` را با زمان فعلی (`new Date().toISOString()`) بهروزرسانی نمایید.
+ سایر نوتها، **بدون تغییر** باقی بمانند.
</details>
<details class="yellow">
<summary>**پیادهسازی تابع** `removeNote`</summary>
در این تابع باید **نوتی که** `id` آن برابر با مقدار ورودی است را از آرایه **حذف** کنید:
</details>
### **خروجی نهایی** *Context*
در انتهای فایل، *Provider* را بنویسید.
<details class="red">
<summary>**نکات مهم**</summary>
+ استفاده از `useCallback` برای **جلوگیری** از ساخت مجدد توابع در هر رندر، **ضروری** است.
+ اگر از `useNotes` خارج از `NotesProvider` استفاده شود، **باید خطا نمایش داده شود** *(کد آماده این بخش در فایل وجود دارد).*
+ **نوع دادهها** (`Note` و `NotesContextType`) **را به هیچ عنوان تغییر ندهید!**
پس از تکمیل این فایل، با استفاده از `useNotes()` در سایر کامپوننتها (مانند `Home` و `NotePage`) میتوانید به **لیست نوتها** و توابع مدیریت آنها دسترسی پیدا کنید.
</details>
</details>
<details class="purple">
<summary>**کامپوننت** `Home.tsx`</summary>
در این فایل، باید **صفحهی اصلی برنامهی یادداشتها** (`Home`) را تکمیل کنید.
در این صفحه، **لیست نوتها** نمایش داده میشود و **کاربر میتواند نوت جدیدی اضافه کند.**
### **هدف:**
کاربر بتواند:
+ **نوت جدیدی بسازد.**
+ در صورت **خالی بودن** لیست نوتها، **پیام مناسب** ببیند.
+ در صورت وجود نوتها، آنها را در قالب کارت مشاهده کند.
### **مراحل پیادهسازی:**
<details class="yellow">
<summary>**اتصال به کانتکست**</summary>
از `useNotes` استفاده کنید تا به `notes` و `addNote` دسترسی پیدا کنید.
</details>
<details class="yellow">
<summary>**تابع** `handleAdd`</summary>
1. این تابع با **کلیک** روی دکمهی **«نوت جدید»** فراخوانی میشود.
2. داخل آن باید:
+ یک **شناسهی یکتا (id)** برای نوت بسازید (مثلاً با `Date.now().toString()`).
+ یک **نوت جدید** با مقادیر پیشفرض بسازید. برای مثال:
3. نوت را با استفاده از `addNote(newNote)` به لیست **اضافه** کنید.
4. سپس با `navigate(`/note/${id}`)` کاربر را به صفحهی همان نوت، **هدایت** کنید.
</details>
<details class="yellow">
<summary>**نمایش نوتها**</summary>
1. در صورتی که **هیچ نوتی وجود نداشته باشد،** باید چنین متنی نمایش داده شود:
```plaintext
نوتی وجود ندارد
```
2. در غیر این صورت، **لیست نوتها** را با استفاده از کامپوننت `NoteCard` رندر کنید.
+ از `id` برای مقدار `key` استفاده کنید.
+ مقدار `note` را به کامپوننت `NoteCard` پاس بدهید.
</details>
</details>
<details class="purple">
<summary>**کامپوننت** `NotePage.tsx`</summary>
در این فایل، باید **صفحهی ویرایش یک نوت خاص** را بسازید.
کاربر در این صفحه میتواند **عنوان نوت** را تغییر دهد، محتوای آن را **ویرایش** کند و در صورت تمایل آن را **حذف** کند.
کاربر بتواند:
+ **نوت خاصی** را با توجه به `id` از مسیر *URL* مشاهده کند.
+ **عنوان نوت را تغییر دهد.**
+ **محتوای نوت** را در `NoteEditor` **ویرایش** کند.
+ **نوت را حذف کرده و به صفحهی اصلی بازگردد.**
+ یا **بدون حذف،** به صفحهی قبل برگردد.
### **مراحل پیادهسازی:**
<details class="yellow">
<summary>**استخراج اطلاعات از** *URL*</summary>
+ با استفاده از `useParams` باید آیدی نوت را از *URL* استخراج کنید.
</details>
<details class="yellow">
<summary>**استفاده از کانتکست**</summary>
### **دسترسی به دادههای** *Context*
با استفاده از `useNotes` از مقادیر زیر را استفاده کنید:
+ `notes`: **لیست تمام نوتها**
+ `updateNote`: **تابعی برای بهروزرسانی نوتها**
+ `removeNote`: **تابعی برای حذف نوتها**
</details>
<details class="yellow">
<summary>**پیدا کردن نوت مورد نظر**</summary>
باید نوتی را پیدا کنید که **شناسهاش** با `id` برابر باشد.
</details>
<details class="yellow">
<summary>**نمایش پیام خطا اگر نوت پیدا نشد**</summary>
اگر نوتی با آن شناسه **وجود نداشت،** پیام مناسب را نمایش دهید:
```html
<div className="p-6">نوت مورد نظر پیدا نشد</div>
```
</details>
<details class="yellow">
<summary>**ویرایش عنوان نوت**</summary>
ورودی بالای صفحه برای ویرایش عنوان نوت استفاده میشود.
+ مقدار این ورودی باید برابر با `note.title` باشد.
+ با استفاده از **تابع مناسب** که از `context` دریافت میکنید، مقدار `title` را بهروزرسانی کنید.
</details>
<details class="yellow">
<summary>**دکمههای کنترل**</summary>
در بالای صفحه دو دکمه وجود دارد:
**بازگشت:** با کلیک روی آن باید کاربر به صفحهی قبلی برگردد.
**حذف نوت:** با کلیک روی آن باید نوت حذف شود و کاربر به **صفحهی اصلی** (`"/"`) هدایت شود
</details>
<details class="yellow">
<summary>**ویرایش محتوای نوت**</summary>
در پایین صفحه باید از **کامپوننت** `NoteEditor` برای **ویرایش محتوای نوت** استفاده کنید.
</details>
**نکته:** مطمئن شوید که فقط در صورتی که `note` وجود دارد، `NoteEditor` را رندر میکنید.
**نکته:** مقدارهای `title` و `content` از *context* مدیریت میشوند، بنابراین نیازی به *state* محلی جدا **نیست.**
</details>
<details class="purple">
<summary>**کامپوننت** `NoteEditor.tsx`</summary>
### **هدف فایل**
`NoteEditor` **کامپوننتی** است که **امکان ویرایش محتوای یک نوت** را فراهم میکند. وظایف کامپوننت به شکل زیر است:
1. نمایش محتوای نوت در ادیتور.
2. ذخیره تغییرات هنگام تایپ با بهروزرسانی *context.*
3. **کنترل** *Paste* برای **جلوگیری** از وارد شدن **محتوای ناخواسته.**
4. استفاده از *Toolbar* برای فرمتدهی متن با انتخاب متن ذخیرهشده.
### **مراحل پیادهسازی**
<details class="yellow">
<summary>**مقداردهی اولیه محتوا در ادیتور**</summary>
این `useEffect` مسئول **قرار دادن محتوای نوت در ادیتور هنگام بارگذاری نوت یا تغییر نوت انتخابشده** است.
+ **نکته:** اگر محتوایی وجود **نداشت** باید محتوای آن را `""` قرار دهید.
+ **نکته:** از `editorRef.current.innerHTML` برای قرار دادن محتوا استفاده کنید.
+ **نکته:** بعد از مقداردهی، میتوانید **فوکوس** را روی **ادیتور** بگذارید.
</details>
<details class="yellow">
<summary>**ذخیرهی تغییرات با تابع** `save`</summary>
برای جلوگیری از ثبت لحظهای تغییرات، بهتر است از **تابع** `debounce` استفاده کنید:
```
// TODO: تابعی برای ذخیره تغییرات ایجاد کنید
// نکته: برای بهینهتر شدن، میتوانید از debounce استفاده کنید تا تغییرات سریع پشت سر هم ثبت نشوند
const save;
```
</details>
<details class="yellow">
<summary>**ذخیرهی تغییرات هنگام تایپ**</summary>
برای ثبت محتوا هنگام تایپ:
```ts
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
// محتوای جدید را دریافت کنید و با استفاده از updateNote ذخیره کنید
// با استفاده از تابع آپدیت تغییرات نوت را آپدیت کنید
};
```
+ **نکته:** مقدار جدید را میتوان از `e.currentTarget.innerHTML` دریافت کرد.
+ **نکته:** سپس با `save(html)` یا مستقیماً با `updateNote` ذخیره کنید.
</details>
<details class="yellow">
<summary>**کنترل** *Paste*</summary>
برای جلوگیری از وارد شدن محتوای اضافی هنگام *Paste:*
```ts
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
// از e.preventDefault برای جلوگیری از paste پیشفرض استفاده کنید
// سپس متن ساده را در محل کرسر درج کنید
};
```
+ **نکته:** متن را از `e.clipboardData.getData("text/plain")` بگیرید.
+ **نکته:** با استفاده از *Selection API،* متن را در محل فعلی کرسر وارد کنید.
</details>
<details class="yellow">
<summary>**استفاده از** *Toolbar*</summary>
**کامپوننت** *Toolbar* بالای ادیتور برای فرمتدهی متن است:
```ts
<Toolbar editorRef={editorRef} savedRangeRef={savedRangeRef} />
```
1. `editorRef`: **رفرنس ادیتور برای اعمال تغییرات.**
2. `savedRangeRef`: محدوده انتخاب متن که توسط *Toolbar* برای اعمال فرمت استفاده میشود.
</details>
+ **نکته:** محدوده انتخاب کاربر (`savedRangeRef`) توسط *useEffect* آخر که خودتان نوشتید مدیریت میشود؛ **نیازی به پیادهسازی آن نیست.**
+ **نکته:** همه تغییرات باید با `updateNote` در *context* ذخیره شوند تا صفحه اصلی و سایر کامپوننتها نیز بهروز شوند.
+ **نکته:** برای بهینه بودن، **ذخیره** با `debounce` پیشنهاد میشود تا تغییرات سریع پشت سر هم باعث فشار روی *state* **نشود.**
</details>
<details class="purple">
<summary>**کامپوننت** `Toolbar.tsx`</summary>
این **کامپوننت** یک نوار ابزار برای ادیتور متن است و وظیفه آن اعمال فرمتبندی *(bold, italic, underline)،* تغییر رنگ متن و تراز متن *(align)* است. برای پیادهسازی، مراحل زیر را دنبال کنید:
<details class="yellow">
<summary>**ایجاد** *state* **برای رنگ متن**</summary>
**یک استیت برای مدیریت رنگ متن ایجاد کنید.**
+ مقدار زیر را به عنوان **مقدار پیشفرض،** قرار دهید:
```
#111827
```
1. این *state* برای **ذخیره رنگ انتخابی توسط کاربر است.**
2. هنگام تغییر رنگ، هم باید *state* **بهروزرسانی** شود و هم دستور اعمال رنگ روی متن انتخابشده **اجرا** شود.
</details>
<details class="yellow">
<summary>**بازگرداندن** *Selection* **ذخیره شده**</summary>
در هنگام استفاده از *Toolbar،* ممکن است کاربر متنی را انتخاب کرده باشد و بخواهد روی همان متن تغییرات اعمال کند *(مثل بولد، ایتالیک یا تغییر رنگ).*
+ مرورگر، هنگام **کلیک روی دکمهها** ممکن است *selection* را از دست بدهد.
+ بنابراین لازم است که قبل از اعمال هر دستور، **محدوده انتخابشده قبلی کاربر را دوباره فعال کنیم.**
+ این کار باعث میشود که تغییرات **دقیقاً** روی همان متن انتخابشده اعمال شود و تجربه کاربری روان باقی بماند.
> **نکته:** برای نگهداری و دسترسی به *selection،* از رفرنسهای ادیتور و محدوده ذخیرهشده استفاده میکنیم، اما پیادهسازی جزئیات را میتوان به مرحله بعدی واگذار کرد.
**رفرنسها:** `editorRef` و `savedRangeRef`
</details>
<details class="yellow">
<summary>**تابع** `exec`</summary>
**تابعی** به نام `exec` بسازید که دستورهای مختلف *(bold, italic, underline, color, align)* را اعمال کند:
```ts
const exec = (command: string, value?: string) => {
// ۱. ابتدا selection را بازیابی کنید
restoreSelection();
// ۲. اجرای دستور
try {
document.execCommand(command, false, value);
} catch {
// اگر مرورگر دستور را پشتیبانی نکرد، خطا را نادیده بگیرید
}
};
```
1. مثال: `exec("bold")` → متن انتخابشده را بولد میکند.
2. مثال: `exec("foreColor", "#ff0000")` → رنگ متن را تغییر میدهد.
3. مثال: `exec("justifyCenter")` → متن را وسطچین میکند.
</details>
<details class="purple">
<summary>**دکمههای** *Toolbar*</summary>
برای هر دکمه:
+ **Bold, Italic, Underline**
```html
<button
type="button"
aria-label="Bold"
title="Bold"
onMouseDown={(e) => e.preventDefault()} // جلوگیری از blur شدن ادیتور
onClick={() => exec("bold")} // دستور را اجرا کنید
>
<FiBold size={18} />
</button>
```
+ **انتخاب رنگ متن**
```html
<label>
<MdFormatColorText size={18} />
<input
type="color"
value={color}
onChange={(e) => {
setColor(e.target.value); // state را آپدیت کنید
exec("foreColor", e.target.value); // دستور تغییر رنگ را اجرا کنید
}}
className="hidden"
/>
</label>
```
+ **تراز متن (Align Left, Center, Right)**
```html
<button onClick={() => exec("justifyLeft")}>...</button>
<button onClick={() => exec("justifyCenter")}>...</button>
<button onClick={() => exec("justifyRight")}>...</button>
```
+ **نکته:** همیشه قبل از `exec`*، selection* را با `restoreSelection` بازگردانید.
</details>
## **نکات مهم**
+ استفاده از `onMouseDown={e => e.preventDefault()}` **ضروری** است تا کلیک روی دکمه باعث از دست رفتن فوکوس ادیتور نشود.
+ استفاده از `document.execCommand` **سادهترین روش** برای اعمال فرمتهای متنی در ادیتورهای contentEditable است.
+ برای رنگ متن، **همیشه** از مقدار `color` که در *state* ذخیره شده استفاده کنید.
+ **تراز متن فقط روی متن انتخابشده اعمال میشود.**
</details>
# **آنچه باید آپلود کنید**
+ **توجه:** پس از اعمال تغییرات، کل پروژه را _Zip_ کرده و آپلود کنید. **همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.**
+ **توجه:** تنها فایلهایی که در **ساختار پروژه** مشخص شدهاند، در سیستم داوری **مورد پذیرش** قرار خواهد گرفت و سایر تغییرات در سایر فایلها **بیتاثیر** خواهند بود.