মূলত প্রকাশিত এখানে
রিমিক্স একটি অপেক্ষাকৃত নতুন, পূর্ণ-স্ট্যাক JS ফ্রেমওয়ার্ক, JS সম্প্রদায়ের কিছু জায়ান্টদের দ্বারা সমর্থিত যেমন কেন্ট সি ডডস, রায়ান টি. ফ্লোরেন্স এবং মাইকেল জ্যাকসন. Next.js না আসা পর্যন্ত, আপনার SPA তৈরির জন্য বিভিন্ন টুলসকে একত্রিত করে JS অ্যাপস তৈরি করার ডি-ফ্যাক্টো উপায় ছিল। Next.js কিছু পরিমাণে বিপ্লব ঘটিয়েছে এবং কিছু সময়ের জন্য অপ্রতিদ্বন্দ্বী হয়ে গেছে। যাইহোক, রেডউডজে, ব্লিটজজে এবং এখন রিমিক্সের সুস্থ প্রতিযোগিতার সাথে গত বছর বা তারও বেশি সময় ধরে ল্যান্ডস্কেপ দ্রুত পরিবর্তন হচ্ছে। এই সমস্ত সরঞ্জামগুলি আরও সৃজনশীল, নির্ভরযোগ্য এবং ওয়েব ডেভেলপমেন্টের কিছু পুরানো সমস্যা সমাধান করার চেষ্টা করছে সবচেয়ে গুরুত্বপূর্ণভাবে, বিকাশকারী বন্ধুত্বপূর্ণ উপায় যাতে একটি কার্যকরী ওয়েব অ্যাপ তৈরি করা JS বিকাশকারীদের জন্য ডিফল্ট হয়ে ওঠে।
এই স্থানটিতে এই সমস্ত সরঞ্জামগুলির মধ্যে একটি স্পষ্ট বিজয়ীকে চিহ্নিত করা অবশ্যই খুব তাড়াতাড়ি তবে রিমিক্স অবশ্যই একটি যোগ্য প্রতিযোগীর মতো দেখাচ্ছে৷ সুতরাং, আপনি যদি ইতিমধ্যেই রিমিক্সের অসাধারণতায় আপনার পা না ভিজিয়ে থাকেন, আমি আশা করি এই টিউটোরিয়ালটি আপনাকে শুরু করতে সাহায্য করবে এবং আপনি পরবর্তীতে যা নির্মাণ করবেন তার জন্য এটি বেছে নিতে আপনাকে রাজি করাবে!
পাখির চোখের দৃশ্য
এই পোস্টে, আমি আপনাকে রিমিক্স ব্যবহার করে একটি AMA (আস্ক মি এনিথিং) অ্যাপ তৈরি করে নিয়ে যাব। এই অ্যাপটি তৈরি করতে আমরা যে প্রাথমিক সরঞ্জামগুলি ব্যবহার করব নীচে তার একটি তালিকা রয়েছে৷ পাঠকদের পক্ষে অনুসরণ করা অবশ্যই সহজ হবে যদি তারা কিছু সরঞ্জামের মূল বিষয়গুলির সাথে পরিচিত হয় (অবশ্যই রিমিক্স বাদে) তবে খুব বেশি চিন্তা করবেন না।
- রিমিক্স - প্রাথমিক কাঠামো
- প্রতিক্রিয়া - UI ফ্রেমওয়ার্ক
- প্রিজমা - ডাটাবেস ওআরএম
- PostgreSQL - ডাটাবেস
- TailwindCSS – CSS ফ্রেমওয়ার্ক
এটি একটি দীর্ঘ পোস্ট হতে চলেছে, তাই আমি একাধিক বৈঠকে অনুসরণ করার পরামর্শ দিচ্ছি এবং আপনার পক্ষে সিদ্ধান্ত নেওয়া সহজ করতে পুরো বিষয়টি পড়া একটি সার্থক বিনিয়োগ কিনা, এখানে আমরা কী করব/শিখব তার একটি রূপরেখা রয়েছে পুরো জিনিস, কালানুক্রমিক ক্রমে:
- অ্যাপ স্পেক - আমরা উচ্চতর স্তর থেকে যে অ্যাপটি তৈরি করতে যাচ্ছি তার বৈশিষ্ট্যগুলির রূপরেখা।
- রিমিক্স দিয়ে শুরু করুন - বেশিরভাগই তাদের অফিসিয়াল ডক্স অনুসরণ করা এবং কিছু জিনিস ইনস্টল করা।
- ডাটাবেস স্কিমা - ডাটাবেস স্কিমা সেটআপ করুন যা আমাদের অ্যাপের প্রয়োজনীয় সমস্ত গতিশীল সামগ্রী সমর্থন করতে পারে।
- CRUD - সাধারণ রিমিক্স উপায়ে মৌলিক CRUD অপারেশন।
- UI/UX - জিনিসগুলিকে সুন্দর এবং সুন্দর দেখাতে একটু একটু করে টেলউইন্ড ছিটিয়ে দিন।
আপনি বলতে পারেন, আমাদের কভার করার জন্য অনেক কিছু আছে, তাই, আসুন সরাসরি ঢুকে পড়ি। ওহ, যদিও তার আগে, আপনি যদি আমার মতো অধৈর্য হয়ে থাকেন এবং কোডটি দেখতে চান, তাহলে এখানে পুরো অ্যাপটি গিথুবে রয়েছে: https://github.com/foysalit/remix-ama
অ্যাপ স্পেসিক
যেকোন প্রজেক্টে, আপনি ঠিক কি তৈরি করতে যাচ্ছেন তা যদি আপনি জানেন, তাহলে ল্যান্ডস্কেপ নেভিগেট করা অনেক সহজ হয়ে যায়। আপনার সবসময় সেই স্বাধীনতা নাও থাকতে পারে তবে ভাগ্যক্রমে, আমাদের ক্ষেত্রে, আমরা আমাদের অ্যাপের জন্য প্রয়োজনীয় সমস্ত বৈশিষ্ট্য জানি। প্রযুক্তিগত দৃষ্টিকোণ থেকে আমরা পদ্ধতিগতভাবে সমস্ত বৈশিষ্ট্য তালিকাভুক্ত করার আগে, আসুন একটি সাধারণ পণ্যের দৃষ্টিকোণ থেকে সেগুলিকে দেখি।
এএমএ সেশন
আমাদের অ্যাপের একজন ব্যবহারকারী একাধিক AMA সেশন হোস্ট করতে সক্ষম হওয়া উচিত। যাইহোক, একই দিনে একাধিক সেশন হোস্ট করার কোন মানে হয় না তাই আসুন একটি সেশনের সময়কাল পুরো দিনের মধ্যে সীমাবদ্ধ করি এবং প্রতি ব্যবহারকারী প্রতি দিনে শুধুমাত্র 1টি সেশনের অনুমতি দিন।
প্রশ্ন ও উত্তর
আমাদের অ্যাপের একজন ব্যবহারকারী চলমান AMA সেশনের সময় হোস্টকে একটি প্রশ্ন জিজ্ঞাসা করতে সক্ষম হওয়া উচিত। এক্সক্লুসিভিটি তৈরি করতে, আসুন সেশন শেষ হওয়ার পরে ব্যবহারকারীদের প্রশ্ন জিজ্ঞাসা করা থেকে ব্লক করি। অবশ্যই, অধিবেশনের আয়োজক তাদের সেশনে জিজ্ঞাসা করা প্রশ্নের উত্তর দিতে সক্ষম হওয়া উচিত।
মন্তব্য
প্রথাগত প্রশ্নোত্তরের চেয়ে আরও বেশি ব্যস্ততা তৈরি করতে এবং জিনিসগুলিকে আরও মজাদার করতে, আসুন একটি মন্তব্য থ্রেড বৈশিষ্ট্য যুক্ত করি যা যেকোনো ব্যবহারকারীকে একটি প্রশ্নে একটি মন্তব্য যোগ করতে দেয়৷ এটি ইতিমধ্যে জিজ্ঞাসিত প্রশ্নে আরও প্রসঙ্গ যোগ করতে বা হোস্ট দ্বারা প্রদত্ত উত্তর সম্পর্কে আলোচনা করতে ব্যবহার করা যেতে পারে ইত্যাদি।
এখন আসুন আমরা সেগুলি কীভাবে বাস্তবায়ন করব তা ভেঙে দেওয়া যাক:
প্রমাণীকরণ - ব্যবহারকারীদের একটি AMA সেশন হোস্ট করতে, হোস্টকে একটি প্রশ্ন জিজ্ঞাসা করতে বা একটি থ্রেডে মন্তব্য করতে নিবন্ধন করতে সক্ষম হতে হবে৷ যাইহোক, আসুন একটি অপ্রমাণিত ব্যবহারকারীকে ইতিমধ্যে চলমান সেশন দেখতে বাধা দিই না। প্রমাণীকরণের জন্য, আসুন ইমেল ঠিকানা এবং পাসওয়ার্ড ব্যবহার করি। উপরন্তু, সাইন আপ করার সময়, আসুন ব্যবহারকারীকে অ্যাপের সর্বত্র ব্যবহার করার জন্য তাদের সম্পূর্ণ নাম ইনপুট করতে বলি। প্রমাণীকরণ সম্পর্কিত ডেটা সংরক্ষণের জন্য একটি ব্যবহারকারী সত্তা ব্যবহার করা হবে।
দায়রা - একটি সূচী পৃষ্ঠায় সমস্ত বর্তমান এবং অতীতের সেশনের একটি তালিকা দেখান (প্রমাণিত/অপ্রমাণিত) ব্যবহারকারীদের যা তাদের প্রতিটি সেশনে ক্লিক করতে এবং প্রশ্ন/উত্তর/মন্তব্য ইত্যাদি দেখতে অনুমতি দেবে। সেই দিনের জন্য নয়। আসুন একটি শুরু করার সময় হোস্টকে প্রতিটি সেশনে কিছু প্রসঙ্গ/বিশদ প্রদান করতে বলি। প্রতিটি সেশন একটি সত্তা যা একটি ব্যবহারকারীর অন্তর্গত।
প্রশ্ন - প্রতিটি পৃথক সেশনে হোস্ট ব্যতীত যেকোনো নিবন্ধিত ব্যবহারকারীর কাছ থেকে একাধিক প্রশ্ন থাকতে পারে। প্রশ্ন সত্তা ডাটাবেসে হোস্ট থেকে উত্তর ধারণ করবে এবং লেখক সেশনের হোস্ট কিনা তা নিশ্চিত করতে প্রতিটি উত্তর ইনপুট যাচাই করা হবে। সত্তাটি একটি সেশন এবং একটি ব্যবহারকারীর অন্তর্গত৷ আসুন নিশ্চিত করুন যে একজন ব্যবহারকারী প্রতি সেশনে শুধুমাত্র একটি প্রশ্ন জিজ্ঞাসা করতে পারে যাতে তারা একটি প্রশ্ন জিজ্ঞাসা না করা পর্যন্ত, আসুন প্রতিটি ব্যবহারকারীকে একটি পাঠ্য ইনপুট দেখান। প্রতিটি উত্তর দেওয়া প্রশ্নের অধীনে, আসুন হোস্টকে তাদের উত্তর যোগ করার জন্য একটি পাঠ্য ইনপুট দেখাই।
মন্তব্য - প্রতিটি প্রশ্নের (উত্তর বা না) একাধিক মন্তব্য থাকতে পারে। জটিলতা কমাতে, আপাতত মন্তব্যে থ্রেডিং যোগ করা যাক না। প্রতিটি ব্যবহারকারী একটি প্রশ্নের অধীনে একাধিক মন্তব্য পোস্ট করতে পারে তাই আসুন প্রতিটি প্রশ্নের নীচে সমস্ত ব্যবহারকারীকে মন্তব্য পাঠ্য ইনপুটটি সর্বদা দেখাই৷ UI সরল করার জন্য, আসুন ডিফল্টরূপে অধিবেশন পৃষ্ঠায় প্রশ্ন (এবং উত্তর) তালিকাটি দেখাই এবং একটি সাইডবারে মন্তব্য থ্রেড খুলতে একটি লিঙ্ক যোগ করি।
রিমিক্স দিয়ে শুরু করুন
রিমিক্সের অনেক দুর্দান্ত গুণাবলী রয়েছে কিন্তু ডকুমেন্টেশন সম্ভবত শীর্ষস্থান দখল করে। ভারী উন্নয়নের অধীনে একটি কাঠামোতে অনেকগুলি চলমান টুকরা থাকতে বাধ্য যা রক্ষণাবেক্ষণকারীদের দ্বারা ক্রমাগত বিকশিত হচ্ছে তাই বৈশিষ্ট্যগুলিকে অগ্রাধিকার দেওয়া হলে ডকুমেন্টেশন পিছিয়ে পড়তে বাধ্য। যাইহোক, রিমিক্স দল ডকুমেন্টেশন আপ টু ডেট রাখতে এবং অবিস্মরণীয় পরিবর্তনগুলির ধ্রুবক প্রবাহের সাথে সিঙ্কে রাখার জন্য খুব যত্ন নেয়। সুতরাং, শুরু করার জন্য, অবশ্যই, অফিসিয়াল ডক্স আমাদের প্রবেশের প্রথম পয়েন্ট হবে।
আপনি যদি অন্য ওয়েবসাইটে যেতে এবং পাঠ্যের অন্য দেয়াল পড়তে খুব অলস হন তবে চিন্তা করবেন না। রিমিক্স ইনস্টল করার জন্য আপনাকে যা করতে হবে তা এখানে:
- নিশ্চিত করুন যে আপনার Node.js ডেভেলপমেন্ট env সেটআপ আছে।
- আপনার টার্মিনাল উইন্ডো খুলুন এবং নিম্নলিখিত কমান্ড চালান
npx create-remix@latest
. - সম্পন্ন.
রিমিক্স আপনাকে শুধু একগুচ্ছ টুল দেয় না এবং আপনাকে আপনার জিনিস তৈরি করতে বলে না, তারা উদাহরণ দিয়ে নেতৃত্ব দেয় যার কারণে তাদের ধারণা রয়েছে স্ট্যাক. স্ট্যাকগুলি মূলত টেমপ্লেট/স্টার্টার কিট যা আপনাকে বাক্সের বাইরে একটি সম্পূর্ণ প্রকল্পের ভিত্তি দেয়। আমাদের প্রকল্পের জন্য, আমরা ব্যবহার করব ব্লুজ স্ট্যাক যা আমাদেরকে প্রিজমা, টেইলউইন্ড এবং একটি সম্পূর্ণ মডিউল সহ একটি সম্পূর্ণ কনফিগার করা রিমিক্স প্রকল্প দেয় যা দেখায় কিভাবে একটি CRUD বৈশিষ্ট্য তৈরি করতে সেই টুলগুলি ব্যবহার করতে হয়। আমি সৎভাবে বলতে চাচ্ছি, আমি মনে করি আমার এই পোস্টটি লেখা উচিত নয় যেহেতু টেমপ্লেটটি ইতিমধ্যেই সমস্ত কাজ করেছে৷ ওহ আচ্ছা… আমি এখন খুব গভীরে আছি তাই এটাও শেষ করতে পারে।
আপনাকে যা করতে হবে তা হল কমান্ডটি চালান npx create-remix --template remix-run/blues-stack ama
আপনার টার্মিনালে এবং রিমিক্স নামে একটি নতুন ফোল্ডারে পুরো প্রকল্পটি ড্রপ করবে ama
আপনি কয়েকটি প্রশ্নের উত্তর দেওয়ার পরে।
এখন এর খোলা যাক ama
ফোল্ডার এবং ভিতরের বিষয়বস্তু সঙ্গে নিজেদেরকে একটু পরিচিত. রুটে একগুচ্ছ কনফিগার ফাইল রয়েছে এবং আমরা সেগুলির বেশিরভাগের মধ্যে যাব না। আমরা বেশিরভাগই আগ্রহী Prisma, প্রকাশ্য এবং অ্যাপ্লিকেশন ডিরেক্টরি প্রিজমা ডিরেক্টরিতে আমাদের ডাটাবেস স্কিমা এবং মাইগ্রেশন থাকবে। পাবলিক ডিরেক্টরীতে অ্যাপের প্রয়োজনীয় যেকোন সম্পদ থাকবে যেমন আইকন, ছবি ইত্যাদি। অবশেষে, অ্যাপ ডিরেক্টরিতে ক্লায়েন্ট এবং সার্ভার উভয়ই আমাদের সমস্ত কোড থাকবে। হ্যাঁ, আপনি যে অধিকার পড়া, ক্লায়েন্ট এবং সার্ভার উভয়ই. যদি এটি আপনাকে প্রধান লিগ্যাসি কোডবেস ফ্ল্যাশব্যাক দেয়, অনুগ্রহ করে জেনে রাখুন যে আপনি একা নন৷
আমরা আমাদের নিজস্ব অ্যাপের কোড লেখার মধ্যে ডুব দেওয়ার আগে, আসুন গিটে সবকিছু পরীক্ষা করে দেখি যাতে আমরা রিমিক্স ব্লুজ স্ট্যাক দ্বারা আমাদের জন্য ইতিমধ্যে যা করা হয়েছে তা থেকে আমাদের পরিবর্তনগুলি ট্রেস করতে পারি।
cd ama
git init
git add .
git commit -am ":tada: Remix blues stack app"
এবং পরিশেষে, আসুন অ্যাপটি চালান এবং আমরা কিছু স্পর্শ করার আগে এটি দেখতে কেমন তা পরীক্ষা করে দেখি। README.md ফাইলটিতে ইতিমধ্যেই সমস্ত বিস্তারিত পদক্ষেপ রয়েছে যা আপনাকে এতে সহায়তা করবে এবং যেহেতু এগুলি ঘন ঘন পরিবর্তনের শিকার হয়, তাই আমি সেগুলি এখানে না লিখে ধাপগুলির সাথে লিঙ্ক করতে যাচ্ছি https://github.com/remix-run/blues-stack#development
আপনি যদি ধাপগুলি সঠিকভাবে অনুসরণ করেন তবে অ্যাপটি এখানে অ্যাক্সেসযোগ্য হওয়া উচিত http://localhost:3000
স্ট্যাকটি একটি ডিফল্ট নোট মডিউলের সাথে আসে যা আপনি আপনার ইমেল এবং পাসওয়ার্ড দিয়ে নিবন্ধন করার পরে খেলতে পারেন।
ডাটাবেস স্কিমা
সাধারণত, আমি এর ডাটাবেস স্কিমা থেকে একটি বৈশিষ্ট্য/সত্তার কথা চিন্তা করা শুরু করতে চাই এবং UI পর্যন্ত আমার পথে কাজ করতে চাই যেখানে ডেটা বিভিন্ন উপায়ে ব্যাখ্যা করা, প্রদর্শিত এবং ম্যানিপুলেট করা হয়। একবার আপনি স্কিমা কাজ করে নিলে, দ্রুত সেই বাস্তবায়নের মাধ্যমে সরানো অনেক সহজ হয়ে যায়।
অ্যাপ স্পেসে উপরে আলোচনা করা হয়েছে, আমাদের ডাটাবেসে 3টি সত্তা প্রয়োজন: সেশন, প্রশ্ন এবং মন্তব্য। প্রতিটি নিবন্ধিত ব্যবহারকারীকে সঞ্চয় করার জন্য আমাদের একটি ব্যবহারকারী সত্তার প্রয়োজন কিন্তু রিমিক্সের ব্লুজ স্ট্যাক ইতিমধ্যেই এটি অন্তর্ভুক্ত করে। একটি যোগ করার জন্য আমাদের এটিকে সামান্য পরিবর্তন করতে হবে name
কলাম ফাইলটি ওপেন করা যাক prisma/schema.prisma
এবং ফাইলের শেষে নীচের লাইন যোগ করুন:
model Session { id String @id @default(cuid()) content String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String questions Question[]
} model Question { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt content String answer String? user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade, onUpdate: Cascade) sessionId String comments Comment[]
} model Comment { id String @id @default(cuid()) content String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String question Question @relation(fields: [questionId], references: [id], onDelete: Cascade, onUpdate: Cascade) questionId String
}
এবং তারপর সংজ্ঞা এই লাইন যোগ করুন User
মডেল:
model User { … name String sessions Session[] questions Question[] comments Comment[] …
}
এখন এখানে আনপ্যাক করার জন্য অনেক কিছু আছে কিন্তু এর বেশিরভাগই এই পোস্টের সুযোগের বাইরে। আমাদের প্রয়োজনীয় 3টি নতুন সত্তার জন্য সঠিক কলাম সহ সমস্ত টেবিল তৈরি করার যত্ন নেওয়ার জন্য প্রিজমার জন্য এই স্কিমার সংজ্ঞাটিই আমাদের প্রয়োজন। সংজ্ঞা এবং সিনট্যাক্স কিভাবে কাজ করে আপনার এই লিঙ্কে যাওয়া উচিত https://www.prisma.io/docs/concepts/components/prisma-schema এবং একটু পড়ুন। একটি উচ্চ স্তরের সারাংশ হল:
- একটি সত্তা/সারণী সংজ্ঞা দিয়ে শুরু হয়
model <EntityName> {}
এবং কোঁকড়া ধনুর্বন্ধনীর ভিতরে সত্তার সমস্ত কলাম/বৈশিষ্ট্য এবং অন্যান্য সত্তার সাথে সম্পর্ক চলে যায়। সুতরাং, মন্তব্যের জন্য একটি টেবিল মত দেখাবেmodel Comment {}
- কলাম সংজ্ঞা সাধারণত মত দেখায়
<columnName> <columnType> <default/relationship/other specifiers>
. সুতরাং, যদি আমাদের মন্তব্য সত্তার ব্যবহারকারীর দ্বারা মন্তব্য ইনপুটের বিষয়বস্তু সংরক্ষণ করার জন্য একটি কলামের প্রয়োজন হয় তবে এটি দেখতে হবে
model Comment { content String
}
- 2টি টেবিল/সত্তার মধ্যে সম্পর্কগুলি সাধারণত একটি বিদেশী কী কলামের মাধ্যমে সংজ্ঞায়িত করা হয় তাই অন্যান্য কলামগুলির সাথে এটিও সংজ্ঞায়িত করা হয়। সংজ্ঞা সাধারণত 2 লাইন প্রয়োজন. একটি কলাম যা বিদেশী কী আইডি ধারণ করে এবং অন্যটি সম্পর্কিত সত্তা অ্যাক্সেস করতে ব্যবহৃত নাম উল্লেখ করার জন্য যা সাধারণত দেখায়:
<entity> <entityName> @relation(fields: [<foreignKeyColumnName>], references: [id], onDelete: Cascade, onUpdate: Cascade)
. সুতরাং, মন্তব্য সত্তাকে প্রশ্ন সত্তার সাথে এক-থেকে-অনেক সম্পর্কের সাথে সম্পর্কিত করতে আমাদের এটির মতো সংজ্ঞায়িত করতে হবে
model Comment { content String question Question @relation(fields: [questionId], references: [id], onDelete: Cascade, onUpdate: Cascade) questionId String
}
উপরের আইসবার্গের টিপটিও কভার করে না যা প্রিজমা তাই অনুগ্রহ করে অনুগ্রহ করে, তাদের অফিসিয়াল ডক্স থেকে এটি পড়ুন এবং আপনি এর আসল শক্তি দেখতে পাবেন। এই ব্লগ পোস্টের জন্য, উপরেরটি আপনাকে একটি ধারণা দেবে কেন আমাদের উপরের প্রিজমা স্কিমাটি প্রয়োজন।
আমাদের ডাটাবেসের সাথে সম্পর্কিত একটি শেষ সমন্বয় করতে হবে। সম্পূর্ণ প্রমাণীকরণ সিস্টেমের সাথে, ব্লুজ স্ট্যাকে একটি প্রাথমিক ডেটা সিডারও রয়েছে যা পরীক্ষার উদ্দেশ্যে একটি ডামি ব্যবহারকারীর সাথে আপনার ডাটাবেস তৈরি করে। যেহেতু আমরা একটি নতুন কলাম চালু করেছি name
ব্যবহারকারীর টেবিলে, ব্যবহারকারীর কাছে একটি ডামি নাম যোগ করার জন্য আমাদের সিডারকে সামঞ্জস্য করতে হবে। ফাইলটি খুলুন prisma/seed.js
এবং নীচের মত ব্যবহারকারী সন্নিবেশ কোড সংশোধন করুন:
const user = await prisma.user.create({ data: { Email, name: 'Rachel Remix', password: { create: { hash: hashedPassword, }, }, }, });
এর সাথে, আমরা অবশেষে আমাদের ডাটাবেসের সাথে এই সমস্ত পরিবর্তনগুলি সিঙ্ক করতে প্রস্তুত। যাইহোক, যেহেতু আমাদের ডাটাবেস ইতিমধ্যেই পূর্বে তৈরি করা স্কিমা এবং কিছু বীজযুক্ত ডেটা দিয়ে তৈরি করা হয়েছে এবং তারপর থেকে, আমাদের db পরিবর্তিত হয়েছে আমরা এখনই আমাদের সমস্ত পরিবর্তন সত্যিই সিঙ্ক করতে পারি না। পরিবর্তে, আমাদের মাইগ্রেশন কিছুটা সামঞ্জস্য করতে হবে। প্রিজমা এই ধরনের সমন্বয়ের জন্য কমান্ড প্রদান করে কিন্তু সৌভাগ্যবশত আমাদের বিদ্যমান ডেটা এবং স্কিমা প্রোডাকশনে নেই বা কিছু নেই তাই এই মুহুর্তে, ডিবি নিউক করা এবং আমাদের বর্তমান স্কিমা দিয়ে নতুন করে শুরু করা সহজ। তাহলে আসুন সহজ রুট নিয়ে যাই এবং এই কমান্ডগুলি চালাই:
./node_modules/.bin/prisma migrate reset
./node_modules/.bin/prisma migrate dev
প্রথম কমান্ডটি আমাদের db পুনরায় সেট করে এবং দ্বিতীয়টি বর্তমান স্কিমা সংজ্ঞা ব্যবহার করে সমস্ত টেবিলের সাথে db পুনরায় তৈরি করে এবং বীজযুক্ত ডেটা দিয়ে এটিকে পপুলেট করে।
এখন, চলমান অ্যাপ সার্ভার বন্ধ করা যাক, অ্যাপটিকে পুনরায় সেটআপ করুন এবং এটিকে ব্যাক আপ করুন
npm run setup
npm run dev
ব্যবহারকারী নিবন্ধন আপডেট করুন
যেহেতু আমরা ব্যবহারকারীর টেবিলে একটি নতুন নামের কলাম যুক্ত করেছি, আসুন সাইন আপ করার সময় ব্যবহারকারীদের তাদের নাম পূরণ করতে হবে। এটি আমাদেরকে একটি বড় ধাক্কা না দিয়ে কাজ করার রিমিক্স পদ্ধতিতে একটি সুন্দর এন্ট্রি দেবে যদি আপনি বেশিরভাগই অ্যাপ তৈরির প্রতিক্রিয়ার সাধারণ পদ্ধতির সাথে পরিচিত হন।
ব্যবহারকারী সাইন আপ জন্য কোড পাওয়া যাবে ./app/routes/join.tsx
ফাইল এটি খুলুন এবং ডান নীচে <Form>
নামের জন্য ইনপুট ক্ষেত্র যোগ করতে নিম্নলিখিত কোড উপাদান:
<Form method="post" className="space-y-6" noValidate> <div> <label htmlFor="name" className="block text-sm font-medium text-gray-700" > Full Name </label> <div className="mt-1"> <input ref={nameRef} id="name" required autoFocus={true} name="name" type="text" aria-invalid={actionData?.errors?.name ? true : undefined} aria-describedby="name-error" className="w-full rounded border border-gray-500 px-2 py-1 text-lg" /> {actionData?.errors?.name && ( <div className="pt-1 text-red-700" id="name-error"> {actionData.errors.name} </div> )} </div> </div>
এটি মূলত ইতিমধ্যে বিদ্যমান ইমেল ক্ষেত্রের অনুকরণ করে। এখন, নাম ইনপুট সঠিকভাবে পরিচালনা করা হয়েছে তা নিশ্চিত করতে আমাদের এখানে আরও কিছু জিনিস সামঞ্জস্য করতে হবে। প্রথমে, আসুন নামের ক্ষেত্রে একটি রেফ তৈরি করি এবং যদি নাম ইনপুট পরিচালনার ক্ষেত্রে কোনো ত্রুটি থাকে, আমরা ফর্মের অন্যান্য ক্ষেত্রগুলির মতোই সেই ক্ষেত্রটিকে স্বয়ংক্রিয়ভাবে ফোকাস করতে চাই।
const emailRef = React.useRef<HTMLInputElement>(null); const nameRef = React.useRef<HTMLInputElement>(null); const passwordRef = React.useRef<HTMLInputElement>(null); React.useEffect(() => { if (actionData?.errors?.email) { emailRef.current?.focus(); } else if (actionData?.errors?.password) { passwordRef.current?.focus(); } else if (actionData?.errors?.name) { nameRef.current?.focus(); } }, [actionData]);
এখন কি হয় actionData
? এটি সাবমিট রিকোয়েস্ট থেকে সার্ভার থেকে ফিরে আসা প্রতিক্রিয়া। যেকোন ফর্ম সাবমিট অ্যাকশন ব্রাউজার থেকে সার্ভারে পোস্টের অনুরোধ পাঠাবে এবং রিমিক্স এর মাধ্যমে এটি পরিচালনা করবে action
ফাংশন ঠিক উপাদান উপরে সংজ্ঞায়িত. এই ফাংশনটি একটি অনুরোধ সম্পত্তি সহ একটি বস্তু গ্রহণ করে যা আপনাকে ব্রাউজার থেকে পাঠানো ডেটা অ্যাক্সেস করার জন্য কিছু খুব সহজ পদ্ধতি দেয় এবং আপনি এই ফাংশন থেকে একটি প্রতিক্রিয়া ফেরত দিতে পারেন যা ব্রাউজার কোড সেই অনুযায়ী পরিচালনা করতে পারে। আমাদের ক্ষেত্রে, আমরা জমা দেওয়া ডেটা যাচাই করতে চাই এবং নিশ্চিত করতে চাই যে নামের ক্ষেত্রটি আসলে পূরণ করা হয়েছে। তাই এখানে আমাদের প্রয়োজনীয় পরিবর্তনগুলি রয়েছে action
ফাংশন:
const email = formData.get("email"); const name = formData.get("name"); const password = formData.get("password"); if (typeof name !== "string" || name.length === 0) { return json<ActionData>( { errors: { name: "Name is required" } }, { status: 400 } ); }
যা নিচে ফুটে ওঠে, ফর্ম জমা দেওয়ার অনুরোধ থেকে নাম ইনপুট পুনরুদ্ধার করে এবং নামটি পূরণ না হলে একটি ত্রুটি বার্তা প্রদান করে। যেহেতু রিটার্ন ডেটা টাইপ করা হয় ActionData
টাইপ করুন, আমাদের সংজ্ঞা সামঞ্জস্য করতে হবে এবং নামের সম্পত্তি যোগ করতে হবে:
interface ActionData { errors: { email?: string; name?: string; password?: string; };
}
আমরা শুধুমাত্র ভুল ইনপুট কেসটি পরিচালনা করেছি তাই আসুন এগিয়ে যাই এবং নিশ্চিত করি যে সঠিক ইনপুটের ক্ষেত্রে, লাইনটি আপডেট করার মাধ্যমে ব্যবহারকারীর নাম কলামের বৈশিষ্ট্যে সন্নিবেশিত হয়। const user = await createUser(email, password);
থেকে const user = await createUser(email, password, name);
এবং ফলস্বরূপ, আমাদের এর সংজ্ঞা সামঞ্জস্য করতে হবে createUser
মধ্যে app/models/user.server.ts
ফাইল:
export async function createUser(email: User["email"], password: string, name: string) { const hashedPassword = await bcrypt.hash(password, 10); return prisma.user.create({ data: { email, name, password: { create: { hash: hashedPassword, }, }, }, });
}
এখানে উল্লেখ্য কয়েকটি বিষয়:
- সার্ভার নির্দিষ্ট কোড বিচ্ছিন্ন এবং ক্লায়েন্ট থেকে দূরে রাখতে, আমরা ফাইলের সাথে প্রত্যয় দিতে পারি
.server.ts
. - আমরা খুব সহজে db-এ একটি নতুন সারি সন্নিবেশ করার জন্য একটি খুব অভিব্যক্তিপূর্ণ এবং স্বজ্ঞাত প্রিজমা API ব্যবহার করছি। এটি সাধারণত রূপ নেয়
prisma.<entityName>.<actionName>({})
কোথায়entityName
ছোট অক্ষরে টেবিলের নাম এবংactionName
db অপারেশন যেমন create, update, findOne ইত্যাদি। আমরা শীঘ্রই এগুলোর আরও ব্যবহার দেখতে পাব।
এর সাথে আমরা একটি নতুন নাম ইনপুট যোগ করেছি যা ব্যবহারকারীর আঘাত করার সময় যাচাই করা হবে Create Account
.
গিটে আমাদের পরিবর্তনগুলি পরীক্ষা করার জন্য এটি সম্ভবত একটি ভাল স্টপিং পয়েন্ট তাই আসুন আমাদের কোডটি কমিট করি: git add . && git commit -am “:sparkles: Add name field to the sign up form”
দায়রা
রিমিক্স কীভাবে জিনিসগুলি করে সে সম্পর্কে কিছু অন্তর্দৃষ্টি পেতে এখনও পর্যন্ত আমরা বেশিরভাগই বিদ্যমান কোডগুলিকে এখানে এবং সেখানে সামঞ্জস্য করছি৷ এখন আমরা স্ক্র্যাচ থেকে আমাদের নিজস্ব মডিউল তৈরিতে ডুব দিতে পারি। আমরা প্রথম যে জিনিসটি তৈরি করব তা হল ব্যবহারকারীদের জন্য একটি AMA সেশন হোস্ট করার জন্য প্রাথমিক অ্যাপ স্পেক সংজ্ঞা অনুযায়ী।
রিমিক্সে, ইউআরএল রুটগুলি ফাইল ভিত্তিক। আমি বলতে চাচ্ছি, এটি একটি সম্পূর্ণ নতুন দৃষ্টান্ত উদ্ভাবন করে তাই এটিকে সরলীকরণ করে file based routing
সম্ভবত খুব সঠিক বা ন্যায্য নয় কিন্তু আমরা ধীরে ধীরে এটিতে প্রবেশ করব। সেশন দিয়ে শুরু করতে, আমরা চাই
- একটি তালিকা পৃষ্ঠা যেখানে সমস্ত বর্তমান এবং ঐতিহাসিক সেশন তালিকাভুক্ত করা হয়
- প্রতি সেশনে একটি উত্সর্গীকৃত পৃষ্ঠা যেখানে সমস্ত প্রশ্ন, উত্তর এবং মন্তব্য থ্রেড দেখানো হয়
- যে কোনো লগ ইন করা ব্যবহারকারীর জন্য একটি নতুন অধিবেশন শুরু করার জন্য একটি পৃষ্ঠা৷
তালিকা পাতা দিয়ে শুরু করা যাক. একটি নতুন ফাইল তৈরি করুন app/routes/sessions/index.tsx
এবং এর ভিতরে নিম্নলিখিত কোডটি রাখুন:
import { Link, useLoaderData } from "@remix-run/react";
import { getSessions } from "~/models/session.server";
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Header } from "~/components/shared/header";
import { Button } from "~/components/shared/button"; type LoaderData = { sessions: Awaited<ReturnType<typeof getSessions>>;
}; export const loader: LoaderFunction = async () => { const sessions = await getSessions(); if (!sessions?.length) { throw new Response("No sessions found", { status: 404 }); } return json<LoaderData>({ sessions });
} export function CatchBoundary() { return ( <> <Header /> <div className="mx-auto px-6 md:w-5/6 lg:w-4/5 xl:w-2/3"> <div className="rounded bg-red-100 p-5"> <h4 className="text-lg font-bold">No sessions found</h4> <p className="mb-4">Why don't you start one... could be fun!</p> <Button isLink to="new" className="bg-blue-600 text-white"> Start AMA session! </Button> </div> </div> </> );
} export default function SessionIndexPage() { const data = useLoaderData<LoaderData>(); const dateFormatter = new Intl.DateTimeFormat("en-GB"); return ( <> <Header /> <div className="mx-auto px-6 md:w-5/6 lg:w-4/5 xl:w-2/3"> <div> {data.sessions?.map((session) => ( <div key={`session_list_item_${session.id}`} className="mt-4 p-4 shadow-sm" > <div className="flex flex-row"> <Link className="underline" to={session.id}> {session.user.name} -{" "} {dateFormatter.format(new Date(session.createdAt))} </Link> <span className="px-2">|</span> <div className="flex flex-row"> <img width={18} height={18} alt="Question count icon" src="/icons/question.svg" /> <span className="ml-1">{session._count.questions}</span> </div> </div> <p className="pt-2 text-sm text-gray-700">{session.content}</p> </div> ))} </div> </div> </> );
}
আপনি যদি প্রতিক্রিয়ার সাথে পরিচিত হন তবে বেশিরভাগ অংশে এটি আপনার কাছে পরিচিত দেখা উচিত। যাইহোক, এর টুকরো টুকরো টুকরো টুকরো করা যাক. রিমিক্স ডিফল্ট এক্সপোর্ট করা উপাদান রেন্ডার করবে। উপাদান সংজ্ঞা উপরে, আমরা একটি আছে loader
ফাংশন এটি একটি বিশেষ ফাংশন যা আপনার প্রতি রুট/ফাইল প্রতি মাত্র 1টি থাকতে পারে এবং পৃষ্ঠা লোড হলে, আপনার পৃষ্ঠার প্রয়োজনীয় ডেটা পুনরুদ্ধার করতে রিমিক্স এই ফাংশনটিকে কল করবে। এটি তারপরে ডেটা সহ আপনার কম্পোনেন্টকে হাইড্রেট করবে এবং রেন্ডার করা এইচটিএমএলকে একটি প্রতিক্রিয়া হিসাবে তারের উপর পাঠাবে যা যাদু আচরণ বা রিমিক্সগুলির মধ্যে একটি। এটি নিশ্চিত করে যে ব্যবহারকারীদের লোডিং অবস্থা দেখতে হবে না কারণ আপনার ব্রাউজার JS কোড API অনুরোধ থেকে ডেটা লোড করে। অ্যাকশন ফাংশনের বডি a কে ডাকে getSessions()
ফাংশন যা থেকে আমদানি করা হয় ~/models/session.server
. এখানে, আমরা শুধুমাত্র সার্ভার ফাইলগুলিতে db অপারেশন স্থাপনের ইতিমধ্যে আলোচিত কৌশল অনুসরণ করছি। এর মধ্যে নতুন ফাইল তৈরি করা যাক app/models/session.server.ts
এবং এটিতে নিম্নলিখিত কোডটি রাখুন:
import { prisma } from "~/db.server"; export type { Session, Question, Comment } from "@prisma/client"; export const getSessions = () => { return prisma.session.findMany({ include: { user: true, _count: { select: { questions: true }, }, }, });
};
এটি কেবল সেশন টেবিল থেকে সমস্ত এন্ট্রি এবং তাদের সাথে সম্পর্কিত সমস্ত ব্যবহারকারীর এন্ট্রি নিয়ে আসছে, যেহেতু আমরা UI-তে হোস্টের তথ্য ব্যবহার করব এবং এটি প্রতিটি সেশনের মোট প্রশ্নের সংখ্যাও অন্তর্ভুক্ত করে। এটি খুব স্কেলযোগ্য নয় কারণ আমাদের অ্যাপ বৃদ্ধির সাথে সাথে কয়েক হাজার AMA সেশন হতে পারে এবং সেগুলি পুনরুদ্ধার করা ভালভাবে স্কেল করা যাচ্ছে না। যাইহোক, এই পোস্টের উদ্দেশ্যে, আমরা আপাতত পেজিনেশন এড়িয়ে যাব।
এর আমাদের মধ্যে ফিরে ঝাঁপ দেওয়া যাক sessions/index.tsx
রুট ফাইল। ডাটাবেসে কোনো সেশন না থাকলে, আমরা ব্যবহার করে একটি 404 ত্রুটির প্রতিক্রিয়া ফেরত দিই Response
রিমিক্স থেকে সাহায্যকারী। অন্যথায়, আমরা ব্যবহার করে সেশনের অ্যারে ধারণকারী একটি JSON প্রতিক্রিয়া ফেরত দিই json
রিমিক্স থেকে সাহায্যকারী।
সার্জারির const data = useLoaderData<LoaderData>();
একটি বিশেষ রিমিক্স হুক কল করছে যা আমাদেরকে পাঠানো প্রতিক্রিয়ার ডেটাতে অ্যাক্সেস দেয় action
. আপনি হয়ত ভাবছেন, আমরা কিভাবে ত্রুটি প্রতিক্রিয়া পরিচালনা করছি? এটা নিশ্চিতভাবে শরীরের মধ্যে পরিচালনা করা হচ্ছে না SessionIndexPage
ফাংশন রিমিক্স দীর্ঘ উপলব্ধ ব্যবহার করে ErrorBoundary
ত্রুটি দৃশ্য পরিচালনার জন্য বৈশিষ্ট্য. আমাদের যা করতে হবে তা হল নামক একটি প্রতিক্রিয়া উপাদান রপ্তানি করা CatchBoundary
একটি রুট ফাইল থেকে এবং রুট (ক্লায়েন্ট বা সার্ভার) রেন্ডারিং থেকে নিক্ষিপ্ত কোনো ত্রুটি CatchBoundary
উপাদান রেন্ডার করা হবে। এর উপরে এই বাস্তব দ্রুত সংজ্ঞায়িত করা যাক SessionIndexPage
উপাদান:
export function CatchBoundary() { return ( <> <Header /> <div className="mx-auto px-6 md:w-5/6 lg:w-4/5 xl:w-2/3"> <div className="rounded bg-red-100 p-5"> <h4 className="text-lg font-bold">No sessions found</h4> <p className="mb-4">Why don't you start one... could be fun!</p> <Button isLink to="new" className="bg-blue-600 text-white"> Start AMA session! </Button> </div> </div> </> );
} export default function SessionIndexPage() {
…
এটি কেবল একটি শেয়ার করা হেডার উপাদান এবং একটি নতুন অধিবেশন শুরু করার জন্য একটি লিঙ্ক রেন্ডার করছে৷ এটি একটি ভাগ ব্যবহার করে Button
উপাদান. আসুন এই ভাগ করা উপাদানগুলি তৈরি করি। আমরা তাদের করা হবে app/components/shared/
ডিরেক্টরি এর সঙ্গে শুরু করা যাক app/components/shared/header.tsx
ফাইল:
import { Link } from "@remix-run/react"; export const HeaderText = () => { return ( <h1 className="text-center text-3xl font-cursive tracking-tight sm:text-5xl lg:text-7xl"> <Link to="/sessions" className="block uppercase drop-shadow-md"> AMA </Link> </h1> );
}; export const Header = () => { return ( <div className="flex flex-row justify-between items-center px-6 md:w-5/6 lg:w-4/5 xl:w-2/3 mx-auto py-4"> <HeaderText /> </div> );
};
এটি কিছু টেলওয়াইন্ড স্টাইলিং ছিটিয়ে একটি মৌলিক প্রতিক্রিয়া উপাদান। আমরা ব্যবহার করছি Link
Remix থেকে উপাদান (যা মূলত শুধুমাত্র একটি প্রক্সি Link
প্রতিক্রিয়া-রাউটার থেকে উপাদান) সেশন পৃষ্ঠার তালিকার সাথে লিঙ্ক করতে। এখানে আরেকটি উল্লেখযোগ্য বিষয় হল যে আমরা একটি ব্যবহার করছি font-cursive
শিরোনাম পাঠ্যের উপর স্টাইল করুন যাতে এটিকে কিছুটা লোগোর মতো দেখায়। কার্সিভ ফন্ট স্টাইলটি ডিফল্ট টেইলউইন্ড কনফিগারেশনে অন্তর্ভুক্ত নয় তাই আমাদের নিজেদেরকে কনফিগার করতে হবে। খুলুন tailwind.config.js
প্রকল্পের রুট থেকে ফাইল করুন এবং সামঞ্জস্য করুন theme
নীচের মত সম্পত্তি:
module.exports = { content: ["./app/**/*.{ts,tsx,jsx,js}"], theme: { extend: { fontFamily: { cursive: ["Pinyon Script", "cursive"], }, }, }, plugins: [],
};
লক্ষ্য করুন যে অতিরিক্ত বিট নামের সাথে একটি নতুন ফন্ট ফ্যামিলি যোগ করতে থিমকে প্রসারিত করে cursive
এবং মান হল Pinyon Script
আমি গুগল ফন্ট থেকে এটি বেছে নিয়েছি তবে নির্দ্বিধায় আপনার নিজের ফন্ট বেছে নিন। আপনি যদি টেইলউইন্ডের সাথে খুব বেশি পরিচিত না হন তবে এটি শুধুমাত্র এই ফন্ট ফ্যামিলিটি ব্যবহার করে একটি পাঠ্যে প্রয়োগ করার ক্ষমতা দেয় font-cursive
সাহায্যকারী ক্লাস কিন্তু আমাদের এখনও আমাদের ওয়েবপেজে ফন্টটি লোড করতে হবে। রিমিক্সে বাহ্যিক সম্পদ যোগ করা বেশ সহজ। খোলা app/root.tsx
ফাইল করুন এবং আপডেট করুন links
অ্যারেতে 3টি নতুন বস্তু যুক্ত করার জন্য সংজ্ঞা:
export const links: LinksFunction = () => { return [ { rel: "stylesheet", href: tailwindStylesheetUrl }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Pinyon+Script&display=swap", }, ];
};
উপরের সমস্ত লিঙ্কগুলি গুগল থেকে পুনরুদ্ধার করা হয়েছে ফন্ট পাতা এখানে.
ফিরে আমাদের পদক্ষেপ ট্রেসিং sessions/index.tsx
ফাইল, অন্য শেয়ার্ড কম্পোনেন্টে বোতাম কম্পোনেন্ট আছে। চলুন যে একটি বাস্তব দ্রুত তৈরি করা যাক app/components/shared/button.tsx
:
import React from "react";
import { Link } from "@remix-run/react";
import type { LinkProps } from "@remix-run/react"; export type ButtonProps = { isAction?: boolean; isLink?: boolean;
}; export const Button: React.FC< ButtonProps & (ButtonProps["isLink"] extends true ? LinkProps : React.ButtonHTMLAttributes<HTMLButtonElement>)
> = ({ children, isLink, isAction, className, ...props }) => { let classNames = `${className || ""} px-3 py-2 rounded`; if (isAction) { classNames += " bg-green-300 text-gray-600 text-sm font-semi-bold"; } if (isLink) { return ( <Link className={classNames} {...(props as LinkProps)}> {children} </Link> ); } return ( <button className={classNames} {...props}> {children} </button> );
};
এটি একটি সাধারণ বোতাম উপাদান যা অ্যাপের বিভিন্ন স্থানে লিঙ্ক বা অ্যাকশন বোতামগুলির চেহারা এবং অনুভূতি একত্রিত করতে আমাদের সাহায্য করবে। বোতাম এবং লিঙ্কের জন্য প্রপস গ্রহণ করার সময় উপাদানের প্রকারকে নিরাপদ করার জন্য, আমরা প্রপস এবং রেন্ডারিং-এ কিছু টাইপস্ক্রিপ্ট জাদু প্রয়োগ করি।
অবশেষে, আমরা প্রকৃত পৃষ্ঠা উপাদান কোড নিজেই তাকান. পৃষ্ঠাটি সমস্ত সেশন এন্ট্রির মাধ্যমে ম্যাপ করে এবং সেশনের তারিখ, অধিবেশনের হোস্টের নাম, সেশনের জন্য হোস্ট দ্বারা যোগ করা ভিত্তি/বিশদ বিবরণ এবং সেখানে কতগুলি প্রশ্ন রয়েছে তার মোট গণনা দেখায়। তারিখ রেন্ডার করতে, আমরা বিল্ট ইন ব্রাউজার ব্যবহার করছি Intl মডিউল যা লোকেল ভিত্তিক বিন্যাস সমর্থন করে. আমরা প্রশ্ন সংখ্যার পাশে একটি ছোট এসভিজি আইকন ব্যবহার করছি। আপনি এখানে অ্যাপে ব্যবহৃত সমস্ত সম্পদ খুঁজে পেতে পারেন https://github.com/foysalit/remix-ama/tree/main/public/icons তবে আপনার নিজের আইকনগুলি আপনার পছন্দ মতো ব্যবহার করতে দ্বিধা বোধ করুন৷ সমস্ত পাবলিক সম্পদ যোগ করা প্রয়োজন /public
ফোল্ডার এবং সমস্ত আইকন একসাথে রাখার জন্য, আমরা একটি আইকন ডিরেক্টরি তৈরি করেছি।
উপরের সবগুলির সাথে, আপনি এখন যেতে সক্ষম হবেন http://localhost:3000/sessions url এবং 404 ত্রুটি পৃষ্ঠাটি দেখুন যেহেতু আমরা এখনও কোনো সেশন তৈরি করিনি।
এখন, নতুন সেশন পৃষ্ঠা তৈরি করা যাক যাতে আমরা একটি সেশন হোস্ট করতে পারি এবং তালিকা পৃষ্ঠায় দেখতে পারি। আমরা সেটিকে অন্য পেজে রাখব যাতে ব্যবহারকারীরা সহজেই সেখানে যেতে পারেন /sessions/new
আমাদের অ্যাপে এবং একটি সেশন হোস্ট করা শুরু করুন। একটি নতুন ফাইল তৈরি করুন routes/sessions/new.tsx
নিম্নলিখিত কোড সহ:
import { Form, useActionData, useTransition } from "@remix-run/react";
import { ActionFunction, json, LoaderFunction, redirect,
} from "@remix-run/node";
import { startSessionsForUser } from "~/models/session.server";
import { requireUserId } from "~/session.server";
import { Header } from "~/components/shared/header";
import { Button } from "~/components/shared/button"; export type ActionData = { errors?: { content?: string; alreadyRunning?: string; };
}; export const action: ActionFunction = async ({ request }) => { const userId = await requireUserId(request); const formData = await request.formData(); try { const content = formData.get("content"); if (typeof content !== "string" || content.length < 90) { return json<ActionData>( { errors: { content: "Content is required and must be at least 90 characters.", }, }, { status: 400 } ); } const session = await startSessionsForUser(userId, content); return redirect(`/sessions/${session.id}`); } catch (err: any) { if (err?.message === "already-running-session") { return json<ActionData>( { errors: { alreadyRunning: "You already have a session running." }, }, { status: 400 } ); } return json({ error: err?.message }); }
}; // A simple server-side check for authentication to ensure only logged in users can access this page
export const loader: LoaderFunction = async ({ request }) => { await requireUserId(request); return json({ success: true });
}; export default function SessionNewPage() { const transition = useTransition(); const actionData = useActionData(); return ( <> <Header /> <div className="p-5 bg-gray-50 px-6 md:w-5/6 lg:w-4/5 xl:w-2/3 mx-auto rounded"> <h4 className="font-bold text-lg"> Sure you want to start a new AMA session? </h4> <p className="mb-4"> An AMA session lasts until the end of the day regardless of when you start the session. During the session, any user on the platform can ask you any question. You always have the option to not answer. <br /> <br /> Please add a few lines to give everyone some context for the AMA session before starting. </p> <Form method="post"> <textarea rows={5} autoFocus name="content" className="w-full block rounded p-2" placeholder="Greetings! I am 'X' from 'Y' TV show and I am delighted to be hosting today's AMA session..." /> {actionData?.errors?.content && ( <p className="text-red-500 text-sm">{actionData.errors.content}</p> )} <Button className="px-3 py-2 rounded mt-3" disabled={transition.state === "submitting"} type="submit" isAction > {transition.state === "submitting" ? "Starting..." : "Start Session"} </Button> </Form> </div> {actionData?.errors?.alreadyRunning && ( <div className="mt-4 p-5 bg-red-500 mx-auto min-w-[24rem] max-w-3xl rounded"> <p>{actionData.errors.alreadyRunning}</p> </div> )} </> );
}
স্বাভাবিক ফ্যাশনে, আসুন কোডের এই বড় অংশটি ভেঙে দেওয়া যাক।
- অ্যাকশন - যখন ব্যবহারকারী সেশনের বিবরণ এবং হিটগুলি পূরণ করে
Start Session
আমরা একটি পোস্ট অনুরোধ হিসাবে ফর্ম ডেটা পেতে চাই এবং বর্তমানে লগ ইন করা ব্যবহারকারীর জন্য একটি নতুন সেশন তৈরি করতে চাই৷ সুতরাং, কর্ম দিয়ে শুরু হয়requireUserId(request)
চেক এটি একটি সহায়ক পদ্ধতি যা স্ট্যাকের সাথে আসে এবং অননুমোদিত ব্যবহারকারীদের লগইন পৃষ্ঠায় পুনরায় রুট করে বা অনুমোদিত ব্যবহারকারীর আইডি ফেরত দেয়। তারপর আমরা সেশনের জন্য ব্যবহারকারীর ইনপুট পুনরুদ্ধার করছিcontent
কলাম ব্যবহার করেrequest.formData()
যা আমাদের সমস্ত POST ডেটাতে অ্যাক্সেস দেয়। যদি বিষয়বস্তু পূরণ না হয় বা একটি নির্দিষ্ট দৈর্ঘ্য অতিক্রম করে, আমরা একটি ত্রুটি বার্তা ফেরত দেই। অন্যথায় আমরা সেশন শুরু করি এবং ব্যবহারকারীকে নতুন তৈরি সেশন পৃষ্ঠায় রুট করি। - startSessionsForUser - এটি শুধুমাত্র একটি সার্ভার ফাংশন যা ডাটাবেসে একটি নতুন সেশন এন্ট্রি তৈরি করে। আমাদের এই যোগ করা যাক
models/session.server.ts
ফাইল:
import type { User, Session } from "@prisma/client";
import startOfDay from "date-fns/startOfDay";
import endOfDay from "date-fns/endOfDay"; export const startSessionsForUser = async ( userId: User["id"], content: Session["content"]
) => { const runningSession = await prisma.session.findFirst({ where: { createdAt: { lte: endOfDay(new Date()), gte: startOfDay(new Date()), }, userId, }, }); if (runningSession) { throw new Error("already-running-session"); } return prisma.session.create({ data: { userId, content } });
};
এই ফাংশনটি একটি userId এবং সেশনের বিষয়বস্তু পায়। যদি ব্যবহারকারীর দ্বারা আজকের সীমানার মধ্যে ইতিমধ্যেই একটি সেশন তৈরি করা থাকে, তাহলে এটি একটি ত্রুটি নিক্ষেপ করে, অন্যথায়, এটি একটি নতুন সেশন এন্ট্রি তৈরি করে। তারিখগুলি হেরফের করা জেএস-এ এক ধরণের অদ্ভুত তাই আমি তারিখগুলি পরিচালনা করার জন্য আমার প্রকল্পে একটি লাইব্রেরি ছেড়ে দিতে পছন্দ করি। এই ক্ষেত্রে আমি ব্যবহার করছি date-fns lib কিন্তু নির্দ্বিধায় আপনার পছন্দের lib ব্যবহার করুন।
- লোডার: আমরা চাই শুধুমাত্র অনুমোদিত ব্যবহারকারীরা এই পৃষ্ঠাটি দেখুক যাতে লোডার সহজভাবে চালায়
requireUserId()
ফাংশন যা অননুমোদিত ব্যবহারকারীদের লগআউট করবে এবং সেশন তৈরি ফর্ম দেখতে তাদের বাধা দেবে। - রূপান্তর - রিমিক্স একটি খুব দরকারী সঙ্গে আসে
useTransition()
হুক যা আপনাকে একটি পৃষ্ঠার বিভিন্ন রাজ্যে অ্যাক্সেস দেয়। আপনি একটি পৃষ্ঠা থেকে একটি ফর্ম জমা দেওয়ার সাথে সাথে সার্ভারে ডেটা পাঠান এবং প্রতিক্রিয়ার জন্য অপেক্ষা করুন,transition.state
পরিবর্তন হবেsubmitting
সেই সময়কাল জুড়ে। এটি ব্যবহার করে, ব্যবহারকারীদের ভুলবশত একাধিক সেশন তৈরি করার চেষ্টা থেকে বিরত রাখতে আমরা জমা বোতামটি নিষ্ক্রিয় করছি। - ত্রুটি পরিচালনা - ব্যবহারকারীরা একটি সেশন শুরু করার চেষ্টা করার সাথে সাথে, আমরা বিষয়বস্তু ক্ষেত্রের জন্য বৈধতা ত্রুটি ফিরে পাই বা ইতিমধ্যে একটি চলমান সেশন থাকলে আমরা একটি নির্দিষ্ট ত্রুটি পাই, আমরা এর থেকে ডেটা অ্যাক্সেস করে ত্রুটি বার্তার UI প্রদর্শনের মাধ্যমে উভয়ই পরিচালনা করছি
useActionData()
. - ফর্ম উপাদান – The
Form
রিমিক্স থেকে কম্পোনেন্ট হল ব্রাউজারের ফর্ম কম্পোনেন্টের উপরে একটি ছোট সিনট্যাকটিক চিনি। এটি একটি ফর্মের সমস্ত ডিফল্ট আচরণ বজায় রাখে। আপনি এখানে আরও গভীরভাবে এটি পড়তে পারেন: https://remix.run/docs/en/v1/guides/data-writes#plain-html-forms
আপনি উপরের সমস্ত ধাপ অনুসরণ করে থাকলে, খুলুন http://localhost:3000/sessions/new আপনার ব্রাউজারে এবং আপনি উপরের মত একটি পৃষ্ঠা দেখতে হবে. যাইহোক, আপনি যদি ইনপুট ক্ষেত্রটি পূরণ করেন এবং স্টার্ট সেশনে আঘাত করেন, এটি আপনাকে একটি 404 পাওয়া যায়নি এমন পৃষ্ঠায় নিয়ে যাবে কিন্তু এর মানে এই নয় যে বোতামটি কাজ করেনি। আপনি ম্যানুয়ালি ফিরে যেতে পারেন http://localhost:3000/sessions এবং তালিকা পৃষ্ঠায় নিজের দ্বারা সদ্য নির্মিত সেশন দেখুন। এটার মতো কিছু:
প্রশ্ন ও উত্তর
সেশন তালিকা এবং পৃষ্ঠাগুলি ভালভাবে কাজ করার সাথে, আমরা এখন প্রতি সেশনে প্রশ্নোত্তর তৈরি করতে পারি। প্রতিটি সেশন এর মাধ্যমে অ্যাক্সেসযোগ্য হওয়া উচিত sessions/:sessionId
যেখানে url :sessionId
একটি পরিবর্তনশীল যা সেশনের আইডি দ্বারা প্রতিস্থাপিত হবে। রিমিক্সে একটি রুট ফাইলে ডায়নামিক রুট প্যারাম ম্যাপ করার জন্য, আমাদের ফাইলের নাম দিয়ে শুরু করতে হবে $
প্যারামিটারের নামের দ্বারা প্রত্যয়িত চিহ্ন। সুতরাং, আমাদের ক্ষেত্রে, আসুন একটি নতুন ফাইল তৈরি করি routes/sessions/$sessionId.tsx
নিম্নলিখিত কোড সহ:
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useCatch, useLoaderData, Outlet, useParams,
} from "@remix-run/react";
import invariant from "tiny-invariant"; import { addAnswerToQuestion, addQuestionToSession, getSession,
} from "~/models/session.server";
import { getUserId, requireUserId } from "~/session.server";
import { Button } from "~/components/shared/button";
import { QuestionAnswer } from "~/components/sessions/question-answer";
import { Header } from "~/components/shared/header"; type ActionData = { errors?: { title?: string; body?: string; };
}; type LoaderData = { session: Awaited<ReturnType<typeof getSession>>; currentUserId?: string;
}; export type OutletContext = LoaderData; export const loader: LoaderFunction = async ({ request, params }) => { invariant(params.sessionId, "sessionId not found"); const session = await getSession(params.sessionId); if (!session) { throw new Response("Not Found", { status: 404 }); } const currentUserId = await getUserId(request); return json<LoaderData>({ session, currentUserId });
}; export const action: ActionFunction = async ({ request, params }) => { const userId = await requireUserId(request); invariant(params.sessionId, "sessionId not found"); const formData = await request.formData(); const questionId = formData.get("answer_to_question"); if (typeof questionId === "string") { const answer = formData.get("answer"); if (typeof answer !== "string" || answer?.trim()?.length < 3) { return json<ActionData>( { errors: { title: "Answer is required" } }, { status: 400 } ); } await addAnswerToQuestion({ id: questionId, userId, answer }); return redirect(`/sessions/${params.sessionId}/questions/${questionId}`); } const content = formData.get("content"); if (typeof content !== "string" || content?.trim()?.length < 3) { return json<ActionData>( { errors: { title: "Question is required" } }, { status: 400 } ); } const question = await addQuestionToSession({ userId, sessionId: params.sessionId, content, }); return redirect(`/sessions/${params.sessionId}/questions/${question.id}`);
}; export default function SessionDetailsPage() { const params = useParams(); const data = useLoaderData() as LoaderData; const dateFormatter = new Intl.DateTimeFormat("en-GB"); return ( <> <Header /> <div className="mx-auto flex w-full flex-row justify-between px-6 md:w-5/6 lg:w-4/5 xl:w-2/3"> <div className={params.questionId ? "w-1/2" : "w-full"}> <h3 className="flex flex-row items-center justify-between"> <span className="text-2xl font-bold"> {data.session?.user.name} </span> <span> {dateFormatter.format( new Date(data.session?.createdAt || Date.now()) )} </span> </h3> <p className="py-6">{data.session?.content}</p> {data.currentUserId !== data.session?.userId && ( <div className="mb-4 rounded bg-gray-100 p-3"> <Form method="post"> <div> <label htmlFor="question" className="block"> <div className="mb-2 flex flex-row items-center"> <img alt="Question logo" src="/icons/question.svg" width={45} height={45} /> <span className="ml-2 leading-4"> Ask your question <br /> <i className="text-xs text-gray-800"> Please be concise and expressive. No explicit content allowed! </i> </span> </div> <textarea rows={5} name="content" className="block w-full rounded p-2" /> </label> </div> <div className="mt-2 flex justify-end"> <Button type="submit" isAction> Ask Question </Button> </div> </Form> </div> )} {!!data.session?.questions?.length && ( <ul> {data.session.questions.map((q) => ( <QuestionAnswer question={q} key={`question_${q.id}`} canAnswer={data.currentUserId === data.session?.userId} isSelected={params.questionId === q.id} /> ))} </ul> )} </div> <Outlet context={data} /> </div> </> );
} export function ErrorBoundary({ error }: { error: Error }) { console.error(error); return <div>An unexpected error occurred: {error.message}</div>;
} export function CatchBoundary() { const caught = useCatch(); if (caught.status === 404) { return <div>Session not found</div>; } throw new Error(`Unexpected caught response with status: ${caught.status}`);
}
এটির সাথে, আমরা ইতিমধ্যেই আলোচনা করেছি এমন কিছু ধারণার মধ্য দিয়ে দ্রুত স্কিম করব এবং নতুন বিটগুলিতে আরও ফোকাস করব:
- লোডার: সেশন এন্ট্রি এবং বর্তমান ব্যবহারকারীর আইডি প্রদান করে। এটি একটি কল আহ্বান করে
invariant
যেটি একটি বহিরাগত লাইব্রেরি যা সহজে চেক করার জন্য একটি ভেরিয়েবল সত্য কিনা এবং না হলে ত্রুটি নিক্ষেপ করে। - getSession: একমাত্র যুক্তি হিসাবে sessionId গ্রহণ করে। আমাদের এটা বাস্তবায়ন করা যাক
models/session.server.ts
ফাইল:
export const getSession = (id: Session["id"]) => prisma.session.findFirst({ where: { id }, include: { questions: { include: { user: true, }, }, user: true, }, });
বিজ্ঞপ্তি কীভাবে এটি একটি অধিবেশনের সাথে সম্পর্কিত সমস্ত প্রশ্ন এবং সেইসাথে সেই প্রশ্নগুলি জিজ্ঞাসা করা ব্যবহারকারীদের অন্তর্ভুক্ত করে৷
- অ্যাকশন: কে দেখছে তার উপর ভিত্তি করে এই পৃষ্ঠাটি 2টি জিনিস করতে পারে৷ সেশনের হোস্ট যেকোনো প্রশ্নের উত্তর দিতে পারে কিন্তু কোনো প্রশ্ন করতে পারে না। অন্য সব ব্যবহারকারী শুধুমাত্র বিপরীত করতে পারেন. সুতরাং কর্ম উভয় কর্ম এবং উপায় আমরা উভয় মধ্যে পার্থক্য হ্যান্ডেল করা প্রয়োজন মাধ্যমে হয়
formData.get("answer_to_question")
ইনপুট. ক্লায়েন্টের দিক থেকে, আমরা তখনই এটি পাঠাব যখন হোস্ট একটি প্রশ্নের উত্তর জমা দিচ্ছে। বিজ্ঞপ্তি কিভাবে আমরা ব্যবহারকারীকে রিডাইরেক্ট করছি/sessions/${params.sessionId}/questions/${questionId}
উভয় পদক্ষেপের ক্ষেত্রে? এটা নেস্টেড রাউটিং আমাদের এন্ট্রি. এটি আপনার মাথার পিছনে পরে রাখুন। - addAnswerToQuestion: এই হেল্পার প্রশ্নটির আইডি এবং উত্তর ইনপুট ধারণ করে এমন একটি বস্তুকে একটি যুক্তি হিসাবে গ্রহণ করে একটি প্রশ্নের হোস্টের উত্তর যোগ করে। এর বাস্তবায়ন করা যাক
models/session.server.ts
:
import type { User, Session, Question } from "@prisma/client"; export const addAnswerToQuestion = async ({ id, userId, answer,
}: Pick<Question, "id" | "userId" | "answer">) => { const existingQuestion = await prisma.question.findFirst({ where: { id }, include: { session: true }, }); if (!existingQuestion) { throw new Error("question-not-found"); } if (existingQuestion.session.userId !== userId) { throw new Error("not-session-author"); } return prisma.question.update({ where: { id }, data: { answer } });
};
লক্ষ্য করুন যে ব্যবহারকারী অনুরোধটি করছেন কিনা তা বাস্তবিকই সেশনের হোস্ট কিনা এবং তা না হলে একটি নির্দিষ্ট ত্রুটি ছুড়ে দেয়।
- addQuestionToSession: এটি ব্যবহারকারীর এবং সেশনের আইডি এবং প্রশ্ন ইনপুট সমন্বিত একটি অবজেক্ট আর্গুমেন্ট গ্রহণ করে একটি সেশনে যেকোন নন-হোস্ট ব্যবহারকারীর প্রশ্ন যোগ করে। এইভাবে এটি বাস্তবায়িত হয়
models/session.server.ts
:
export const addQuestionToSession = async ({ userId, sessionId, content,
}: Pick<Question, "userId" | "sessionId" | "content">) => { const existingQuestion = await prisma.question.findFirst({ where: { userId, sessionId, content, }, }); if (existingQuestion) { throw new Error("already-asked"); } const isSessionHost = await prisma.session.findFirst({ where: { userId, id: sessionId, }, }); if (isSessionHost) { throw new Error("host-can-not-ask-questions"); } return prisma.question.create({ data: { sessionId, userId, content } });
};
লক্ষ্য করুন কিভাবে আমরা একজন ব্যবহারকারীকে প্রতি সেশনে একাধিকবার একই প্রশ্ন পোস্ট করতে বাধা দিচ্ছি?
- ইউজ প্যারামস হুক: এই হুক রাউটারে প্রতিক্রিয়া করার আরেকটি প্রক্সি যা আমাদের ক্ষেত্রে সেশনআইডির মতো যেকোনো রুট প্যারামিটারে অ্যাক্সেস দেয়।
- প্রশ্ন ফর্ম: সমস্ত নন-হোস্ট, প্রমাণীকৃত ব্যবহারকারীদের কাছে, আমরা পূর্বে পোস্ট করা প্রশ্নের তালিকার উপরে প্রতিটি সেশনে একটি প্রশ্ন ইনপুট ফর্ম দেখাই।
- প্রশ্নউত্তর উপাদান: কোডের একটি বড় অংশ ভাগ করে নেওয়ার যোগ্য এবং বিচ্ছিন্ন রাখতে, আমরা একটি ভাগ করা উপাদান ফাইলে একটি একক প্রশ্ন রাখি। কেন আমরা একটু পরে দেখব কিন্তু প্রথমে এই উপাদানটির বাস্তবায়ন দেখি। একটি নতুন ফাইল তৈরি করুন
app/components/sessions/question-answer.tsx
এবং সেখানে নিম্নলিখিত কোড রাখুন:
import { Form, Link } from "@remix-run/react";
import React from "react"; import type { Question } from "~/models/session.server";
import type { User } from "~/models/user.server";
import { Button } from "~/components/shared/button"; export const QuestionAnswer: React.FC<{ question: Question & { user: User }; isSelected?: boolean; as?: React.ElementType; canAnswer: boolean; hideCommentsLink?: boolean;
}> = ({ question, hideCommentsLink, isSelected, as: Component = "li", canAnswer, ...rest
}) => { const dateFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "short", }); return ( <Component className={`mb-4 rounded p-2 ${isSelected ? "bg-gray-50" : ""}`} {...rest} > <div className="flex flex-row"> <div className="max-w-40 mr-2"> <img width={50} height={50} alt="Question icon" src="/icons/question.svg" /> </div> <p> <span className="font-semi-bold text-xs text-gray-500"> {question.user?.name} at{" "} {dateFormatter.format(new Date(question.createdAt))} {!hideCommentsLink && ( <> {" "} |{" "} <Link className="underline" to={`questions/${question.id}`}> Comments </Link> </> )} </span> <br /> {question.content} </p> </div> {question.answer ? ( <div className="mt-2 pl-10"> <div className="flex flex-row p-2 shadow-sm"> <img width={50} height={50} alt="Question icon" src="/icons/answer.svg" /> <p> <span className="font-semi-bold text-xs text-gray-500"> {dateFormatter.format(new Date(question.updatedAt))} </span> <br /> {question.answer} </p> </div> </div> ) : ( canAnswer && ( <div className="mt-4 px-4"> <Form method="post"> <textarea rows={5} name="answer" className="block w-full rounded p-2" /> <div className="mt-2 flex justify-end"> <Button name="answer_to_question" value={question.id} isAction> Answer </Button> </div> </Form> </div> ) )} </Component> );
};
লক্ষ্য করুন যে এই উপাদানটি এটির ভিতরে একটি ফর্ম এম্বেড করে যার অর্থ প্রতিটি প্রশ্ন হোস্টের জন্য এই ফর্মটিকে রেন্ডার করবে যাতে তারা এখনও উত্তর দেয়নি এমন প্রশ্নের উত্তর যোগ করার একটি সহজ উপায় দেয় এবং ফর্মের জমা বোতামটি রয়েছে name="answer_to_question" value={question.id}
প্রপস যা আমাদেরকে ব্যাকএন্ড (অ্যাকশন) সংকেত দিতে সাহায্য করে যে এই ফর্ম জমাটি হোস্ট দ্বারা উত্তর ইনপুট হিসাবে মোকাবেলা করতে হবে।
আপনি হয়তো লক্ষ্য করেছেন যে প্রতিটি প্রশ্ন এর সাথে লিঙ্ক করে to={
প্রশ্ন/${question.id}}
যা আমাদের নেস্টেড রাউটিং বিষয়ে নিয়ে আসে। এখন যে কটাক্ষপাত করা যাক.
নেস্টেড রাউটিং
একটি ঐতিহ্যগত প্রতিক্রিয়া অ্যাপে, আপনি একটি পৃষ্ঠাকে একাধিক উপাদানে বিভক্ত করবেন এবং উপাদানগুলি অভ্যন্তরীণভাবে তাদের নিজস্ব ডেটা লোড করবে বা একটি বিশ্বব্যাপী ডেটা স্টোর দ্বারা খাওয়ানো হবে যা এতে ডেটা প্রেরণ করে। রিমিক্সে, আপনি নেস্টেড রাউটিং এর মাধ্যমে এটি করতে পারেন যেখানে একটি পৃষ্ঠা অন্য একটি পৃষ্ঠা এম্বেড করতে পারে যার মধ্যে ডেটা লোডার, অ্যাকশন, এরর বাউন্ডার ইত্যাদির মতো নিজস্ব জীবনচক্র রয়েছে৷ . আমরা একটি সেশনে প্রশ্ন প্রতি একটি মন্তব্য থ্রেড দেখানোর জন্য এটি ব্যবহার করতে যাচ্ছি।
এই সুবিধার জন্য, আমরা একটি যোগ <Outlet context={data.session} />
অধিবেশন বিবরণ পৃষ্ঠায় উপাদান. Outlet
নেস্টেড পৃষ্ঠার বিষয়বস্তুর জন্য ধারক এবং এটি আমাদের অভিভাবক স্তরে একটি চাইল্ড পৃষ্ঠার জন্য লেআউট তৈরি করার ক্ষমতা দেয়৷ যখন ব্যবহারকারী একটি নেস্টেড রুটে যায়, এটি নেস্টেড পৃষ্ঠার রুটের সর্বনিম্ন স্তর দ্বারা রেন্ডার করা html দ্বারা প্রতিস্থাপিত হবে৷
এখন, মন্তব্য থ্রেড অ্যাক্সেস করতে, আমরা ব্যবহারকারীদের রাউটিং করছি session/:sessionId/questions/:questionId
রুট যাতে ফাইল সিস্টেমের সাথে মেলে, আমাদের ভিতরে একটি নতুন ডিরেক্টরি তৈরি করতে হবে routes/sessions/$sessionId/questions
এবং নামে একটি ফাইল তৈরি করুন $questionId.tsx
এর ভিতরে আমরা এখন নামের একটি ফাইল আছে লক্ষ্য করুন $sessionId.tx
এবং একটি ডিরেক্টরি নামে $sessionId
. এটি বিভ্রান্তিকর হতে পারে তবে ডিজাইন করা হয়েছে। এটি রিমিক্সকে ব্যবহার করতে বলে sessionId.tsxfileastheparentpageandrenderanynestedroutesfromthe'sessionId.tsx ফাইলটি মূল পৃষ্ঠা হিসাবে এবং ` থেকে যেকোনো নেস্টেড রুট রেন্ডার করুনসেশন আইডিdirectory. Now let’s put in the following code in the
$questionId.tsx` ফাইল:
import type { LoaderFunction, ActionFunction } from "@remix-run/node"; // or "@remix-run/cloudflare"
import { Form, Link, useLoaderData, useOutletContext, useParams, useTransition,
} from "@remix-run/react";
import type { Comment } from "~/models/session.server";
import { addCommentToAnswer, getCommentsForQuestion,
} from "~/models/session.server";
import invariant from "tiny-invariant";
import { json, redirect } from "@remix-run/node"; import type { OutletContext } from "../../$sessionId";
import { requireUserId } from "~/session.server";
import type { User } from "~/models/user.server";
import { QuestionAnswer } from "~/components/sessions/question-answer";
import { Button } from "~/components/shared/button";
import React, { useEffect, useRef } from "react"; type LoaderData = { comments: Awaited<ReturnType<typeof getCommentsForQuestion>>;
}; type ActionData = { errors?: { title?: string; body?: string; };
}; export const loader: LoaderFunction = async ({ params }) => { invariant(params.questionId); const data: LoaderData = { comments: await getCommentsForQuestion(params.questionId), }; return json(data);
}; export const action: ActionFunction = async ({ request, params }) => { const userId = await requireUserId(request); invariant(params.sessionId, "sessionId not found"); invariant(params.questionId, "questionId not found"); const formData = await request.formData(); const content = formData.get("content"); if (typeof content !== "string" || content?.trim()?.length < 3) { return json<ActionData>( { errors: { title: "Comment is required" } }, { status: 400 } ); } await addCommentToAnswer({ userId, content, questionId: params.questionId, }); return redirect( `/sessions/${params.sessionId}/questions/${params.questionId}` );
}; export default function SessionQuestion() { const params = useParams(); const commentFormRef = useRef<HTMLFormElement>(null); const transition = useTransition(); const outletData = useOutletContext<OutletContext>(); const data = useLoaderData(); const question = outletData?.questions.find( (q) => q.id === params.questionId ); const isCommenting = transition.state === "submitting"; useEffect(() => { if (!isCommenting) { commentFormRef?.current?.reset(); } }, [isCommenting]); if (!question) return null; const dateFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "short", }); return ( <div className="w-1/2"> <div className="pl-8"> <Link to={`/sessions/${params.sessionId}`} className="bg-gray-500 rounded-sm px-2 py-1 text-white flex flex-row justify-between" > <span>Thread</span> <span>✕</span> </Link> <QuestionAnswer question={question} as="div" hideCommentsLink /> <div className="bg-gray-100 p-3 mb-4 rounded"> <Form method="post" ref={commentFormRef}> <label htmlFor="comment" className="block"> <div className="flex flex-row mb-2 items-center"> <img alt="Question logo" src="/icons/comment.svg" width={45} height={45} /> <span className="ml-2 leading-4"> Add a comment <br /> <i className="text-xs text-gray-800"> Please be polite. No explicit content allowed! </i> </span> </div> <textarea rows={5} className="w-full block rounded p-2" name="content" /> </label> <div className="mt-2 flex justify-end"> <Button type="submit" isAction> Comment </Button> </div> </Form> </div> <ul> {data.comments?.map((comment: Comment & { user: User }) => ( <li key={`comment_${comment.id}`} className="mt-4"> <div className="flex flex-row"> <div> <img width={40} height={40} alt="Question icon" className="mr-2" src="/icons/comment.svg" /> </div> <p> <span className="font-semi-bold text-xs text-gray-500"> {comment.user?.name} at{" "} {dateFormatter.format(new Date(comment.createdAt))} </span> <br /> <span className="text-gray-800 text-sm">{comment.content}</span> </p> </div> </li> ))} </ul> </div> </div> );
}
এখানে, আমরা যে ব্যবহার করছি question-answer.tsx
একই UI উপাদান প্রদর্শন করার জন্য উপাদান যা আমরা সেশনের অধীনে দেখাই কিন্তু এই ক্ষেত্রে মন্তব্য থ্রেডের শীর্ষে, পাঠকদের মন্তব্যের প্রসঙ্গ দিতে। আমরা এটির ভিতরে একটি ফর্মও রাখছি যার মাধ্যমে যে কোনও প্রমাণীকৃত ব্যবহারকারী একটি মন্তব্য পোস্ট করতে পারেন। আসুন আমরা লোডারে যে 2টি নতুন সার্ভার ফাংশন ব্যবহার করছি তা পরীক্ষা করে দেখি এবং তারপর থেকে এই পৃষ্ঠাটির জন্য অ্যাকশন models/session.server.ts
:
import type { User, Session, Question, Comment } from "@prisma/client"; export const addCommentToAnswer = async ({ questionId, userId, content,
}: Pick<Comment, "questionId" | "userId" | "content">) => { return prisma.comment.create({ data: { questionId, userId, content } });
}; export const getCommentsForQuestion = async (questionId: string) => { return prisma.comment.findMany({ where: { questionId }, include: { user: true }, });
};
এই উপাদানের মধ্যে কয়েকটি উল্লেখযোগ্য বিষয় হল:
- useOutletContext হুক: এটি আমাদেরকে এর মাধ্যমে চাইল্ড পেজে পাস করা সমস্ত প্রপস অ্যাক্সেস দেয়
<Outlet … />
মূল পৃষ্ঠায় উপাদান। সুতরাং, এখানে, আমাদের ভিতরে সমস্ত প্রশ্ন সহ পুরো অধিবেশনে অ্যাক্সেস রয়েছে এবং থ্রেডের একক প্রশ্নের জন্য অনুসন্ধান করার পরিবর্তে, আমরা কেবল ইতিমধ্যে পাস করা ডেটা থেকে এটি বাছাই করছি। - মন্তব্য লোড করা হচ্ছে: আমরা একটি প্রশ্নের জন্য সমস্ত মন্তব্য পেজিনেশন ছাড়াই লোড করছি, যা কোনো প্রোডাকশন অ্যাপের জন্য একটি দুর্দান্ত ধারণা নয়।
শেষ করি
আপনি যদি পূর্ববর্তী সমস্ত পদক্ষেপগুলি অনুসরণ করে থাকেন তবে একটি ছদ্মবেশী উইন্ডোতে অ্যাপটি খুলুন এবং একটি নতুন অ্যাকাউন্ট তৈরি করুন৷ তারপর আপনি যদি পূর্বে তৈরি করা সেশনে ক্লিক করেন, তাহলে আপনাকে একটি প্রশ্ন জিজ্ঞাসা করার জন্য একটি ইনপুট ক্ষেত্র দেখতে হবে:
এখন আপনি যদি একটি প্রশ্ন টাইপ করেন এবং সেই নতুন অ্যাকাউন্ট থেকে পোস্ট করেন, তাহলে আপনার এরকম কিছু দেখতে হবে:
যা আপনার মন্তব্য দেখায়, ডানদিকে একটি থ্রেড হিসাবে মন্তব্যটি খোলে এবং আপনাকে বা অন্য কোন ব্যবহারকারীকে থ্রেডে একটি মন্তব্য যোগ করতে দেয়।
অবশেষে, আপনি যদি অন্য ব্রাউজার উইন্ডোতে ফিরে যান যেখানে আপনি সেশনের হোস্ট হিসাবে লগ ইন করেছেন এবং সেশন পৃষ্ঠাটি রিফ্রেশ করেন, তাহলে আপনার উত্তর পোস্ট করার জন্য নীচে একটি ইনপুট সহ মন্তব্যটি দেখতে হবে:
এরপর কি?
আপনি এখানে অবধি অনুসরণ করে একটি আশ্চর্যজনক কাজ করেছেন তাই দয়া করে নিজেকে সাধুবাদ জানান! আপনি যদি আমার মতো হন এবং কখনই চকচকে নতুন JS জিনিসগুলি পর্যাপ্ত পরিমাণে পেতে না পারেন, আপনি হয়তো ভাবছেন: "এটি দুর্দান্ত তবে এটি কি এমন কিছু যা আমি একজন ব্যবহারকারী হিসাবে ব্যবহার করব?" এবং যদি আপনি নিজের প্রতি সত্য হন তবে উত্তরটি একটি বড় মোটা হবে NO
. তাই আমি আপনাকে কয়েকটি ধারনা দিয়ে দেব যা এটিকে দ্রুত খেলনা অ্যাপটিকে একটি উত্পাদন-প্রস্তুত অ্যাপে পরিণত করতে পারে যা বাস্তব জগতে কিছুটা আকর্ষণ পেতে পারে:
- রিয়েল টাইম ডেটা সিঙ্ক - AMA সেশনগুলি সব সময় সম্পর্কে। অন্তত ভাল বেশী হয়. তাদের হোস্ট করা লোকেদের কাছে ঘুরে দাঁড়ানোর এবং নতুন মন্তব্য/প্রশ্ন ইত্যাদি দেখার জন্য প্রতি 10 সেকেন্ডে রিফ্রেশ করার সময় নেই৷ তাই সেগুলিকে রিয়েলটাইমে সিঙ্ক করা উচিত এবং হোস্টের কাছে হাইলাইট করা উচিত৷ অংশগ্রহণকারীদের জন্য একই.
- পেজিনেশন - পোস্ট জুড়ে উল্লিখিত হিসাবে, আমরা ডেটা লোডিংয়ে কিছু কোণ কেটেছি যা অবশ্যই বাস্তব বিশ্বের অ্যাপে স্কেল করবে না। সমস্ত প্রশ্নের সাথে পৃষ্ঠা সংখ্যা যোগ করাও একটি ভাল শেখার অভিজ্ঞতা হবে।
- সেশন টাইমার এবং ভবিষ্যত সেশন: যেহেতু এই অ্যাপে সেশনগুলি প্রতিদিন টাইম-বক্স করা হয়, সেশন কখন শেষ হয় তার জন্য একটি টাইমার দেখানো অভিজ্ঞতায় রোমাঞ্চের একটি উপাদান যোগ করতে পারে। আরেকটি ঘাতক বৈশিষ্ট্য হোস্টদের ভবিষ্যতের জন্য সময়সূচী সেশনের অনুমতি দেবে এবং হোম পেজে আসন্ন সেশনকে আরও হাইলাইট করে দেখানোর মাধ্যমে এর চারপাশে কিছু হাইপ তৈরি করবে।
Resources
- এসইও চালিত বিষয়বস্তু এবং পিআর বিতরণ। আজই পরিবর্ধিত পান।
- প্লেটোব্লকচেন। Web3 মেটাভার্স ইন্টেলিজেন্স। জ্ঞান প্রসারিত. এখানে প্রবেশ করুন.
- উত্স: https://www.codementor.io/foysalit/build-a-fullstack-ama-app-with-remix-prisma-postgresql-1vsbmepsp3
- 1
- a
- ক্ষমতা
- সক্ষম
- সম্পর্কে
- উপরে
- প্রবেশ
- প্রবেশযোগ্য
- অ্যাক্সেস করা
- অনুযায়ী
- তদনুসারে
- হিসাব
- সঠিক
- কর্ম
- স্টক
- প্রকৃতপক্ষে
- যোগ
- অতিরিক্ত
- উপরন্তু
- ঠিকানা
- যোগ করে
- সমন্বয়
- পর
- বয়সের
- এগিয়ে
- সব
- অনুমতি
- একা
- এর পাশাপাশি
- ইতিমধ্যে
- সর্বদা
- আবুল মাল আবদুল
- এএমএ সেশন
- আশ্চর্যজনক
- মধ্যে
- এবং
- অন্য
- উত্তর
- উত্তর
- API
- অ্যাপ্লিকেশন
- প্রয়োগ করা
- অ্যাপস
- যুক্তি
- কাছাকাছি
- বিন্যাস
- সম্পদ
- সম্পদ
- প্রচেষ্টা
- প্রমাণীকরণ
- অনুমোদিত
- প্রমাণীকরণ
- লেখক
- গাড়ী
- সহজলভ্য
- অপেক্ষায় রয়েছেন
- পিছনে
- সাহায্যপ্রাপ্ত
- ব্যাক-এন্ড
- ভিত্তি
- ভিত্তি
- মৌলিক
- মূলত
- মূলতত্ব
- কারণ
- হয়ে
- আগে
- পিছনে
- হচ্ছে
- নিচে
- মধ্যে
- বিশাল
- বিট
- বাধা
- রোধক
- ব্লগ
- শরীর
- সীমান্ত
- পাদ
- সীমানা
- বক্স
- বিরতি
- আনে
- ব্রাউজার
- নির্মাণ করা
- ভবন
- নির্মিত
- গুচ্ছ
- বোতাম
- কল
- কলিং
- কল
- যত্ন
- নির্ঝর
- কেস
- দঙ্গল
- ধরা
- কিছু
- অবশ্যই
- পরিবর্তন
- পরিবর্তন
- অক্ষর
- চেক
- পরীক্ষণ
- চেক
- শিশু
- শিশু
- বেছে নিন
- শ্রেণী
- পরিষ্কার
- মক্কেল
- কোড
- কোডবেস
- স্তম্ভ
- কলাম
- এর COM
- মন্তব্য
- মন্তব্য
- সমর্পণ করা
- সম্প্রদায়
- প্রতিযোগিতা
- সম্পূর্ণ
- জটিলতা
- উপাদান
- উপাদান
- ধারণা
- ধারণা
- বিভ্রান্তিকর
- অতএব
- কনসোল
- ধ্রুব
- প্রতিনিয়ত
- আধার
- ধারণ
- বিষয়বস্তু
- প্রসঙ্গ
- সন্তুষ্ট
- কোণে
- পারা
- দম্পতি
- পথ
- আবরণ
- সৃষ্টি
- নির্মিত
- সৃষ্টি
- সৃজনী
- সিএসএস
- বর্তমান
- এখন
- কাটা
- উপাত্ত
- ডেটাবেস
- তারিখ
- তারিখগুলি
- DATETIME
- দিন
- নিবেদিত
- গভীর
- ডিফল্ট
- স্পষ্টভাবে
- খুশি
- গভীরতা
- পরিকল্পিত
- বিশদ
- বিস্তারিত
- দেব
- বিকাশকারী
- ডেভেলপারদের
- উন্নয়ন
- DID
- ভেদ করা
- ডিরেক্টরি
- আলোচনা
- আলোচনা
- প্রদর্শন
- ডকুমেন্টেশন
- না
- করছেন
- Dont
- নিচে
- ড্রপ
- বাতিল
- সময়
- প্রগতিশীল
- প্রতি
- গোড়ার দিকে
- সহজ
- সহজে
- পারেন
- ইমেইল
- প্রান্ত
- প্রবৃত্তি
- যথেষ্ট
- নিশ্চিত করা
- নিশ্চিত
- সমগ্র
- সত্ত্বা
- সত্তা
- প্রবেশ
- ভুল
- ত্রুটি
- মূলত
- ইত্যাদি
- থার (eth)
- এমন কি
- সবাই
- সব
- বিবর্তিত
- ঠিক
- উদাহরণ
- ছাড়া
- বিদ্যমান
- অভিজ্ঞতা
- রপ্তানি
- রপ্তানির
- ভাবপূর্ণ
- প্রসারিত করা
- বহিরাগত
- চোখ
- সহজতর করা
- ন্যায্য
- পতন
- পরিচিত
- অভ্যস্ত করান
- পরিবার
- ফ্যাশন
- দ্রুত
- চর্বি
- বৈশিষ্ট্য
- বৈশিষ্ট্য
- প্রতিপালিত
- ফুট
- কয়েক
- ক্ষেত্র
- ক্ষেত্রসমূহ
- ফাইল
- নথি পত্র
- পূরণ করা
- ভরা
- পরিশেষে
- আবিষ্কার
- প্রথম
- প্রথম দেখা
- কেন্দ্রবিন্দু
- অনুসরণ করা
- অনুসৃত
- অনুসরণ
- ফন্ট
- বিদেশী
- ফর্ম
- পাওয়া
- ফ্রেমওয়ার্ক
- বিনামূল্যে
- ঘন
- তাজা
- বন্ধুত্বপূর্ণ
- থেকে
- সম্পূর্ণ
- সম্পূর্ণরূপে
- মজা
- ক্রিয়া
- ক্রিয়াকলাপ
- ভবিষ্যৎ
- লাভ করা
- সাধারণ
- পাওয়া
- git
- GitHub
- দাও
- দেয়
- দান
- বিশ্বব্যাপী
- Go
- Goes
- চালু
- ভাল
- গুগল
- গুগল ফন্ট
- মহান
- ভিত্তি
- বৃদ্ধি
- হাতল
- হ্যান্ডলিং
- কুশলী
- খাটান
- কাটা
- মাথা
- সুস্থ
- সাহায্য
- সাহায্য
- এখানে
- উচ্চ
- ঊর্ধ্বতন
- হাইলাইট করা
- ঐতিহাসিক
- আঘাত
- হিট
- হোম
- আশা
- নিমন্ত্রণকর্তা
- হোস্টিং
- ঘর
- কিভাবে
- কিভাবে
- যাহোক
- এইচটিএমএল
- HTTPS দ্বারা
- শত শত
- প্রতারণা
- আইকন
- ধারণা
- ধারনা
- সনাক্ত করা
- চিত্র
- বাস্তবায়ন
- বাস্তবায়ন
- বাস্তবায়িত
- আমদানি
- in
- অন্তর্ভুক্ত করা
- অন্তর্ভুক্ত
- অন্তর্ভুক্ত
- সুদ্ধ
- অবিশ্বাস্যভাবে
- সূচক
- স্বতন্ত্র
- তথ্য
- প্রারম্ভিক
- ইনপুট
- সূক্ষ্মদৃষ্টি
- ইনস্টল
- ইনস্টল করার
- পরিবর্তে
- আগ্রহী
- উপস্থাপিত
- স্বজ্ঞাত
- বিনিয়োগ
- পূজা
- ভিন্ন
- IT
- নিজেই
- কাজ
- JSON
- ঝাঁপ
- রাখা
- চাবি
- রকম
- জানা
- লেবেল
- ভূদৃশ্য
- বড়
- গত
- গত বছর
- বিন্যাস
- নেতৃত্ব
- শিক্ষা
- ত্যাগ
- উত্তরাধিকার
- লম্বা
- যাক
- উচ্চতা
- LG
- স্বাধীনতা
- লাইব্রেরি
- লাইন
- লাইন
- LINK
- লিঙ্ক
- তালিকা
- সামান্য
- বোঝা
- লোডার
- বোঝাই
- লোড
- লোগো
- দীর্ঘ
- দেখুন
- মত চেহারা
- সৌন্দর্য
- অনেক
- সর্বনিম্ন স্তর
- জাদু
- রক্ষণাবেক্ষণ
- মুখ্য
- করা
- মেকিং
- কাজে ব্যবহৃত
- হেরফের
- ম্যানুয়ালি
- অনেক
- মানচিত্র
- মানচিত্র
- ম্যাচ
- মানে
- উল্লিখিত
- বার্তা
- পদ্ধতি
- পদ্ধতি
- হতে পারে
- মাইগ্রেট
- অভিপ্রয়াণ
- মডেল
- মডিউল
- অধিক
- সেতু
- পদক্ষেপ
- চলন্ত
- মোজিলা
- বহু
- নাম
- নামে
- নেভিগেট করুন
- প্রয়োজন
- চাহিদা
- নতুন
- পরবর্তী
- পরবর্তী.js
- নোড
- node.js
- সাধারণ
- স্মরণীয়
- লক্ষণীয়
- সংখ্যা
- লক্ষ্য
- বস্তু
- ঘটেছে
- কর্মকর্তা
- ONE
- খোলা
- প্রর্দশিত
- অপারেশন
- অপারেশনস
- বিপরীত
- পছন্দ
- ক্রম
- অন্যান্য
- অন্যভাবে
- রূপরেখা
- বাহিরে
- নিজের
- পত্রাঙ্কন
- দৃষ্টান্ত
- স্থিতিমাপ
- অংশ
- অংশগ্রহণকারীদের
- গৃহীত
- পাস
- পাসওয়ার্ড
- গত
- সম্প্রদায়
- পরিপ্রেক্ষিত
- বাছাই
- টুকরা
- টুকরা
- জায়গা
- স্থাপন
- মাচা
- Plato
- প্লেটো ডেটা ইন্টেলিজেন্স
- প্লেটোডাটা
- খেলা
- দয়া করে
- প্লাগ-ইন
- বিন্দু
- দৃশ্যের পয়েন্ট
- পোস্ট
- পোস্ট
- পোস্টগ্রেস্কল
- ক্ষমতা
- ক্ষমতাশালী
- পছন্দ করা
- পছন্দের
- চমত্কার
- প্রতিরোধ
- আগে
- পূর্বে
- প্রাথমিক
- অগ্রাধিকারের
- প্রি্ম্
- সম্ভবত
- সমস্যা
- পণ্য
- উত্পাদনের
- প্রকল্প
- সম্পত্তি
- প্রদান
- প্রদত্ত
- উপলব্ধ
- প্রক্সি
- প্রকাশ্য
- প্রকাশিত
- উদ্দেশ্য
- উদ্দেশ্য
- ধাক্কা
- করা
- স্থাপন
- প্রশ্ন ও উত্তর
- গুণাবলী
- প্রশ্ন
- প্রশ্ন
- দ্রুত
- দ্রুত
- প্রতিক্রিয়া
- পড়া
- পাঠক
- পাঠকদের
- পড়া
- প্রস্তুত
- বাস্তব
- বাস্তব জগতে
- প্রকৃত সময়
- গ্রহণ করা
- পায়
- সুপারিশ করা
- পুনর্নির্দেশ
- হ্রাস করা
- রেফারেন্স
- তথাপি
- খাতা
- নিবন্ধভুক্ত
- নিবন্ধনের
- সংশ্লিষ্ট
- সম্পর্ক
- সম্পর্ক
- অপেক্ষাকৃতভাবে
- বিশ্বাসযোগ্যতা
- বিশ্বাসযোগ্য
- রিমিক্স
- অনুবাদ
- প্রতিস্থাপিত
- অনুরোধ
- অনুরোধ
- প্রয়োজনীয়
- প্রয়োজন
- Resources
- প্রতিক্রিয়া
- বিশ্রাম
- সীমাবদ্ধ করা
- প্রত্যাবর্তন
- আয়
- বিপ্লব হয়েছে
- শিকড়
- বৃত্তাকার
- রুট
- রাউটার
- যাত্রাপথ
- সারিটি
- চালান
- দৌড়
- নিরাপদ
- হেতু
- একই
- মাপযোগ্য
- স্কেল
- তফসিল
- সুযোগ
- দ্বিতীয়
- এইজন্য
- অনুভূতি
- সেশন
- সেশন
- সেটআপ
- ভাগ
- শিফটিং
- সংক্ষিপ্ত
- উচিত
- প্রদর্শনী
- বেড়াবে
- শো
- চিহ্ন
- সংকেত
- স্বাক্ষর
- সহজ
- সহজতর করা
- সরলীকরণ
- কেবল
- থেকে
- একক
- ধীরে ধীরে
- ছোট
- So
- সমাধান
- কিছু
- কিছু
- এসপিএ
- স্থান
- প্রশিক্ষণ
- নির্দিষ্ট
- স্পীড
- ঘূর্ণন
- বিভক্ত করা
- অকুস্থল
- কর্তিত
- গাদা
- স্ট্যাক
- মান
- শুরু
- শুরু
- শুরু হচ্ছে
- শুরু
- রাষ্ট্র
- যুক্তরাষ্ট্র
- অবস্থা
- প্রারম্ভিক ব্যবহারের নির্দেশাবলী
- এখনো
- থামুন
- বাঁধন
- দোকান
- কৌশল
- প্রবাহ
- শৈলী
- নমন
- জমা
- পেশ
- সাফল্য
- এমন
- সংক্ষিপ্তসার
- সুপার
- সমর্থন
- সমর্থন
- করা SVG
- বাক্য গঠন
- পদ্ধতি
- টেবিল
- Tailwind
- গ্রহণ করা
- লাগে
- গ্রহণ
- টীম
- কারিগরী
- বলে
- টেমপ্লেট
- প্রান্তিক
- পরীক্ষামূলক
- সার্জারির
- অধিকার
- আড়াআড়ি
- তাদের
- বিষয়
- জিনিস
- কিছু
- চিন্তা
- হাজার হাজার
- দ্বারা
- সর্বত্র
- নিক্ষেপ
- সময়
- সময়জ্ঞান
- ডগা
- শিরনাম
- থেকে
- আজ
- একসঙ্গে
- অত্যধিক
- সরঞ্জাম
- শীর্ষ
- বিষয়
- মোট
- স্পর্শ
- চিহ্ন
- আকর্ষণ
- ঐতিহ্যগত
- রূপান্তর
- সত্য
- অভিভাবকসংবঁধীয়
- tv
- টাইপরাইটারে মুদ্রি
- ui
- অধীনে
- অপ্রত্যাশিত
- আসন্ন
- আপডেট
- আপডেট
- URL টি
- us
- ব্যবহার
- ব্যবহারকারী
- ব্যবহারকারী
- সাধারণত
- ux
- যাচাই করুন
- যাচাই
- বৈধতা
- মূল্য
- বিভিন্ন
- মাধ্যমে
- চেক
- মতামত
- অপেক্ষা করুন
- উপায়
- ওয়েব
- ওয়েব ডেভেলপমেন্ট
- ওয়েবসাইট
- কি
- যে
- যখন
- হু
- ইচ্ছা
- টেলিগ্রাম
- মধ্যে
- ছাড়া
- ভাবছি
- হয়া যাই ?
- কাজ করছে
- কাজ
- বিশ্ব
- উপযুক্ত
- would
- লেখা
- X
- বছর
- আপনার
- নিজেকে
- zephyrnet