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

تا اینکه به یک نتیجه رسید: نوشتن فرانتیوم که همیشه خلاق است، تصمیم گرفت چیزی شبیه به 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

استفاده از Context برای مدیریت نوت‌ها

  • پروژه شما یک منبع مرکزی برای نوت‌ها دارد. بنابراین، ابتدا باید یک Context Provider برای نوت‌ها داشته باشید.

  • این Provider باید تمام کامپوننت‌هایی که نیاز به دسترسی به نوت‌ها دارند را احاطه کند.

راه‌اندازی مسیرها با React Router

  • اپ شما چند صفحه دارد: صفحه اصلی و صفحه نمایش جزئیات یک نوت.

  • برای مدیریت مسیرها از BrowserRouter استفاده کنید.

  • مسیرها (Routes) رو به شکل زیر طراحی کنید:

    1. مسیر اصلی ("/") که صفحه Home را نمایش می‌دهد.

    2. مسیر جزئیات نوت ("/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

در این فایل ساختار اصلی Context مربوط به نوت‌ها آماده شده است. این Context وظیفه دارد اطلاعات یادداشت‌ها (notes) را مدیریت کرده و در اختیار سایر بخش‌های برنامه قرار دهد.

کد اولیه شامل تعریف نوع داده‌ها (Noteنوع ساختار Context (NotesContextType) و اسکلت اصلی کامپوننت NotesProvider می‌باشد. شما باید با تکمیل قسمت‌های مشخص‌شده با TODO، عملکرد کامل این Context را پیاده‌سازی کنید.

هدف این فایل

ساخت یک Context برای:

  • نگهداری لیست یادداشت‌ها در حالت (state)
  • همگام‌سازی داده‌ها با localStorage
  • فراهم‌کردن سه تابع برای مدیریت نوت‌ها:
    1. addNote برای افزودن نوت جدید
    2. updateNote برای ویرایش نوت موجود
    3. removeNote برای حذف نوت بر اساس شناسه

بخش‌هایی که باید پیاده‌سازی شوند

مقداردهی اولیه‌ی State

با استفاده از useState باید:

  • داده‌های موجود در localStorage با کلید note_app_notes_v1 را بخوانید.
  • اگر داده‌ای وجود دارد، آن را با JSON.parse تبدیل کنید.
  • اگر خطایی رخ داد یا داده‌ای وجود نداشت، آرایه‌ای خالی ([]) برگردانید.

نکته: این مقداردهی فقط یک‌بار در هنگام اجرای اولیه انجام می‌شود.

ذخیره‌سازی تغییرات در LocalStorage

باید با استفاده از useEffect کاری کنید که هر بار notes تغییر کند، مقدار جدید در localStorage ذخیره شود.

  • کلید ذخیره باید همان مقدار STORAGE_KEY باشد.
  • از JSON.stringify(notes) برای ذخیره داده‌ها استفاده کنید.

پیاده‌سازی تابع addNote

در این تابع، باید نوت جدید را به ابتدای آرایه اضافه کنید.

یعنی جدیدترین نوت‌ها در بالاترین قسمت لیست قرار بگیرند.

پیاده‌سازی تابع updateNote

  • نوتی که id آن برابر با id ورودی است را پیدا کنید.
  • فیلدهای جدید (از patch) را با نوت قبلی ترکیب کنید.
  • مقدار updatedAt را با زمان فعلی (new Date().toISOString()) به‌روزرسانی نمایید.
  • سایر نوت‌ها، بدون تغییر باقی بمانند.

پیاده‌سازی تابع removeNote

در این تابع باید نوتی که id آن برابر با مقدار ورودی است را از آرایه حذف کنید:

خروجی نهایی Context

در انتهای فایل، Provider را بنویسید.

نکات مهم

  • استفاده از useCallback برای جلوگیری از ساخت مجدد توابع در هر رندر، ضروری است.

  • اگر از useNotes خارج از NotesProvider استفاده شود، باید خطا نمایش داده شود (کد آماده این بخش در فایل وجود دارد).

  • نوع داده‌ها (Note و NotesContextType) را به هیچ عنوان تغییر ندهید!

پس از تکمیل این فایل، با استفاده از useNotes() در سایر کامپوننت‌ها (مانند Home و NotePage) می‌توانید به لیست نوت‌ها و توابع مدیریت آن‌ها دسترسی پیدا کنید.

کامپوننت Home.tsx

در این فایل، باید صفحه‌ی اصلی برنامه‌ی یادداشت‌ها (Home) را تکمیل کنید. در این صفحه، لیست نوت‌ها نمایش داده می‌شود و کاربر می‌تواند نوت جدیدی اضافه کند.

هدف:

کاربر بتواند:

  • نوت جدیدی بسازد.
  • در صورت خالی بودن لیست نوت‌ها، پیام مناسب ببیند.
  • در صورت وجود نوت‌ها، آن‌ها را در قالب کارت مشاهده کند.

مراحل پیاده‌سازی:

اتصال به کانتکست

از useNotes استفاده کنید تا به notes و addNote دسترسی پیدا کنید.

تابع handleAdd

  1. این تابع با کلیک روی دکمه‌ی «نوت جدید» فراخوانی می‌شود.

  2. داخل آن باید:

    • یک شناسه‌ی یکتا (id) برای نوت بسازید (مثلاً با Date.now().toString()).
    • یک نوت جدید با مقادیر پیش‌فرض بسازید. برای مثال:
  3. نوت را با استفاده از addNote(newNote) به لیست اضافه کنید.

  4. سپس با navigate(/note/${id}) کاربر را به صفحه‌ی همان نوت، هدایت کنید.

نمایش نوت‌ها

  1. در صورتی که هیچ نوتی وجود نداشته باشد، باید چنین متنی نمایش داده شود:
نوتی وجود ندارد
  1. در غیر این صورت، لیست نوت‌ها را با استفاده از کامپوننت NoteCard رندر کنید.

    • از id برای مقدار key استفاده کنید.
    • مقدار note را به کامپوننت NoteCard پاس بدهید.

کامپوننت 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 کامپوننتی است که امکان ویرایش محتوای یک نوت را فراهم می‌کند. وظایف کامپوننت به شکل زیر است:

  1. نمایش محتوای نوت در ادیتور.
  2. ذخیره تغییرات هنگام تایپ با به‌روزرسانی context.
  3. کنترل Paste برای جلوگیری از وارد شدن محتوای ناخواسته.
  4. استفاده از Toolbar برای فرمت‌دهی متن با انتخاب متن ذخیره‌شده.

مراحل پیاده‌سازی

مقداردهی اولیه محتوا در ادیتور

این useEffect مسئول قرار دادن محتوای نوت در ادیتور هنگام بارگذاری نوت یا تغییر نوت انتخاب‌شده است.

  • نکته: اگر محتوایی وجود نداشت باید محتوای آن را "" قرار دهید.
  • نکته: از editorRef.current.innerHTML برای قرار دادن محتوا استفاده کنید.
  • نکته: بعد از مقداردهی، می‌توانید فوکوس را روی ادیتور بگذارید.

ذخیره‌ی تغییرات با تابع 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} />
  1. editorRef: رفرنس ادیتور برای اعمال تغییرات.
  2. savedRangeRef: محدوده انتخاب متن که توسط Toolbar برای اعمال فرمت استفاده می‌شود.
  • نکته: محدوده انتخاب کاربر (savedRangeRef) توسط useEffect آخر که خودتان نوشتید مدیریت می‌شود؛ نیازی به پیاده‌سازی آن نیست.

  • نکته: همه تغییرات باید با updateNote در context ذخیره شوند تا صفحه اصلی و سایر کامپوننت‌ها نیز به‌روز شوند.

  • نکته: برای بهینه بودن، ذخیره با debounce پیشنهاد می‌شود تا تغییرات سریع پشت سر هم باعث فشار روی state نشود.

کامپوننت Toolbar.tsx

این کامپوننت یک نوار ابزار برای ادیتور متن است و وظیفه آن اعمال فرمت‌بندی (bold, italic, underline)، تغییر رنگ متن و تراز متن (align) است. برای پیاده‌سازی، مراحل زیر را دنبال کنید:

ایجاد state برای رنگ متن

یک استیت برای مدیریت رنگ متن ایجاد کنید.

  • مقدار زیر را به عنوان مقدار پیشفرض، قرار دهید:
#111827
  1. این state برای ذخیره رنگ انتخابی توسط کاربر است.
  2. هنگام تغییر رنگ، هم باید state به‌روزرسانی شود و هم دستور اعمال رنگ روی متن انتخاب‌شده اجرا شود.

بازگرداندن Selection ذخیره شده

در هنگام استفاده از Toolbar، ممکن است کاربر متنی را انتخاب کرده باشد و بخواهد روی همان متن تغییرات اعمال کند (مثل بولد، ایتالیک یا تغییر رنگ).

  • مرورگر، هنگام کلیک روی دکمه‌ها ممکن است selection را از دست بدهد.

  • بنابراین لازم است که قبل از اعمال هر دستور، محدوده انتخاب‌شده قبلی کاربر را دوباره فعال کنیم.

  • این کار باعث می‌شود که تغییرات دقیقاً روی همان متن انتخاب‌شده اعمال شود و تجربه کاربری روان باقی بماند.

نکته: برای نگهداری و دسترسی به selection، از رفرنس‌های ادیتور و محدوده ذخیره‌شده استفاده می‌کنیم، اما پیاده‌سازی جزئیات را می‌توان به مرحله بعدی واگذار کرد.

رفرنس‌ها: editorRef و savedRangeRef

تابع exec

تابعی به نام exec بسازید که دستورهای مختلف (bold, italic, underline, color, align) را اعمال کند:

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") → متن را وسط‌چین می‌کند.

دکمه‌های 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 کرده و آپلود کنید. همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشه‌ی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمره‌ای دریافت نخواهید کرد.

  • توجه: تنها فایل‌هایی که در ساختار پروژه مشخص شده‌اند، در سیستم داوری مورد پذیرش قرار خواهد گرفت و سایر تغییرات در سایر فایل‌ها بی‌تاثیر خواهند بود.

ارسال پاسخ برای این سؤال
فایلی انتخاب نشده است.