این روزها افکار و احساسات متفاوت، ذهن **فرانتیوم** را آشفته کردهاند.
او بارها با خودش گفته بود:
**«از الان دیگه فکر کردن رو میذارم کنار و فقط تمرکز میکنم!»**
اما چند لحظه بعد، دوباره همه چیز از اول شروع میشد.
تا اینکه به یک نتیجه رسید: **نوشتن**
**فرانتیوم** که همیشه خلاق است، تصمیم گرفت چیزی شبیه به `notepad` بسازد تا بتواند افکارش را در آن بنویسد و ذهنش را آرام کند.

# **پروژهی اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/problemset/assignments/4367/download_problem_initial_project/316815/) کلیک کنید.
<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_ کرده و آپلود کنید. **همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.**
+ **توجه:** تنها فایلهایی که در **ساختار پروژه** مشخص شدهاند، در سیستم داوری **مورد پذیرش** قرار خواهد گرفت و سایر تغییرات در سایر فایلها **بیتاثیر** خواهند بود.