شما بهتازگی به تیم **«اتاق فرمان»** یک شرکت بزرگ اضافه شدهاید جایی که داشبورد مدیریتی، قلب تصمیمگیری روزانهی شرکت است.
اما یک مشکل جدی وجود دارد:
هر روز صدها اعلان از سیستمهای مختلف دریافت می شود از خطاهای حیاتی گرفته تا پیامهای اجتماعی و هشدارهای مالی.
مدیرها میگویند:
+ «اعلانهای مهم گم میشوند!»
+ «نمیفهمیم کدامها را خواندهایم!»
+ «وقتی دنبال یک چیز میگردیم، سیستم کند و گیج میشود!»
وظیفهی شما این است که **اتاق فرمان** را طوری پیادهسازی کنید که مثل یک اتاق کنترل حرفهای عمل کند:
+ اعلانها را نمایش دهد
+ فیلتر و مرتبسازی انجام دهد
+ وضعیت خوانده/نخوانده را مدیریت کند
+ و همهچیز را حتی بعد از رفرش صفحه به خاطر بسپارد
پروژهی اولیه آماده است و **فقط منطق جاوااسکریپت داخل فایل `main.js` ناقص است**.
ظاهر کلی برنامه به شکل زیر است:

پروژه اولیه را از [این لینک](https://quera.org/qbox/download/0TmqYDB5F7/initial_project.zip) دانلود کنید. ساختار فایلهای این پروژه به صورت زیر است:
```
initial_project/
├─ index.html
├─ styles.css
├─ <mark class="violet">main.js</mark>
├─ js/
└─ data.js
├─ fonts/
```
در فایل `js/data.js` تابع `fetchNotificationsData()` وجود دارد که با کمی تاخیر (۱۵۰ms) یک لیست اعلان برمیگرداند:
```js
// Promise<Notification[]>
fetchNotificationsData()
```
هر اعلان چنین ساختاری دارد:
```js
{
id: "n1",
title: "...",
body: "...",
category: "system" | "social" | "finance",
createdAt: 1700000000000
}
```
## جزئیات پیادهسازی
<details class="yellow">
<summary>
دسترسی به المانها
</summary>
المانها را با `id` های زیر پیدا کنید:
- `searchInput`
- `categoryFilter`
- `sortSelect`
- `markAllRead`
- `unreadCount`
- `notificationsList`
**توجه مهم:** برخی از این المانها (و همچنین spinner) دارای `data-testid` هستند که در تستها استفاده میشوند**از تغییر یا حذف `data-testid` ها خودداری کنید.**
</details>
<details class="yellow">
<summary>
راهاندازی اولیه
</summary>
در شروع برنامه:
1. کنترلها (input/select/button) باید **غیرفعال** باشند.
2. `fetchNotificationsData()` را صدا بزنید و بعد از دریافت داده:
- دادهها را در state نگه دارید
- کنترلها را **فعال** کنید
- لیست را رندر کنید (بر اساس فیلترهای فعلی)
</details>
<details class="yellow">
<summary>
رندر لیست
</summary>
تابع `renderNotifications(list)` را پیادهسازی کنید:
اگر لیست خالی بود:
+ یک `<li>` با:
+ کلاس `empty`
+ `data-testid="empty"`
+ متن: `اعلانی یافت نشد`
اگر آیتم وجود داشت:
برای هر اعلان:
+ یک `<li>` با:
+ کلاس `notif`
+ `data-testid="notif-item"`
+ ویژگی `data-id="<id>"`
+ اگر اعلان خواندهشده است:
+ کلاس `is-read` به `<li>` اضافه شود
+ داخل هر آیتم دو دکمه وجود دارد:
1. **دکمه پین (اختیاری)**
+ `data-testid="pin-btn"`
+ ویژگی `aria-pressed="true | false"`
2. **دکمه تغییر وضعیت خواندهشدن**
+ `data-testid="toggle-read-btn"`
متن دکمهها آزاد است و در تستها بررسی نمیشود؛ فقط رفتار آنها مهم است.
</details>
<details class="yellow">
<summary>
شمارنده خواندهنشدهها
</summary>
عدد داخل المان `#unreadCount` باید همیشه برابر باشد با:
> تعداد اعلانهایی که **در کل دادهها** خواندهنشده هستند(نه فقط اعلانهای فیلترشده)
</details>
<details class="yellow">
<summary>
فیلترها
</summary>
دو نوع فیلتر داریم:
1. جستجو (Search)
+ متن واردشده باید (case-insensitive) در `title` یا `body` شامل باشد (`includes`)
2. دستهبندی (Category)
+ اگر `categoryFilter` مقدار داشته باشد، فقط همان دسته نمایش داده شود
</details>
<details class="yellow">
<summary>
مرتبسازی
</summary>
دو حالت:
- `newest` : جدیدترینها بالا (createdAt نزولی)
- `oldest` : قدیمیترینها بالا (createdAt صعودی)
قانون مهم: **آیتمهای پینشده همیشه بالاتر از بقیه نمایش داده شوند** (قبل از مرتبسازی زمانی).
</details>
<details class="yellow">
<summary>
Debounce + Spinner
</summary>
تایپ در searchInput:
+ اگر **هر دو فیلتر search و category خالی باشند**:
+ فوراً رندر انجام شود
+ بدون spinner
+ هر debounce معلق لغو شود
+ در غیر این صورت:
+ فوراً spinner نمایش داده شود
+ فیلتر بعد از **۸۰۰ms** از آخرین ورودی اعمال شود
تغییر categoryFilter:
+ اگر هر دو فیلتر خالی باشند:
+ فوراً و بدون spinner رندر شود
+ در غیر این صورت:
+ فوراً spinner
+ اعمال فیلتر بعد از **۳۰۰ms**
اگر کاربر قبل از پایان debounce دوباره ورودی بدهد، تایمر قبلی باید **ریست** شود.
Spinner
تابع `showSpinner()` باید:
+ یک `<li>` با کلاس `spinner-row`
+ و `data-testid="spinner"`
+ داخل لیست اعلانها نمایش دهد
</details>
## نکات
- نصب/استفاده از هر کتابخانه یا فریمورک در کد اصلی ممنوع است (فقط JS خام).
- فقط مجاز به تغییر فایل `main.js` هستید.
- از تغییر `data-testid` ها خودداری کنید تا تستها قابل اجرا بمانند.
## آنچه باید آپلود کنید
فقط فایل `main.js` نهایی را ارسال کنید.
(فایل به صورت zip ارسال شود و ساختار باید به شکل زیر باشد)
```
answer.zip
└─ main.js
```
ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.