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

پروژهی اولیه
برای دانلود پروژهی اولیه روی این لینک کلیک کنید.
ساختار فایلها
little_shop/
├─ public/
├─ src/
│ ├─ components/
│ │ ├─ NoteCard.tsx
│ │ ├─ NoteEditor.tsx
│ │ └─ Toolbar.tsx
│ ├─ context/
│ │ └─ NotesContext.tsx
│ ├─ pages/
│ │ ├─ Home.tsx
│ │ └─ NotePage.tsx
│ ├─ App.tsx
│ ├─ 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
راه اندازی پروژه
-
پس از دانلود کردن فایل پروژه اولیه، آن را از حالت فشرده خارج کنید.
-
سپس در ترمینال خود دستور
npm installرا اجرا کنید. -
در نهایت پروژه را با استفاده از دستور
npm run devاجرا کنید.
در صورت نیاز از دستور npm install --force برای نصب وابستگیها استفاده کنید.
پیشنمایشهای پیادهسازی
در گیفهای زیر، میتوانید تمام عملکرد برنامه را مشاهده کنید:
- ساخت نوت:

- عملکرد
toolbar:

- حذف نوت:

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