كيفية إعداد مشروع Ruby on Rails v7 مع واجهة مستخدم React على Ubuntu 20.04

اختار الكاتب مؤسسة الحدود الإلكترونية لتلقي تبرع كجزء من برنامج كتابة من أجل التبرعات.

المقدمة

روبي على القضبان هو إطار عمل شهير لتطبيقات الويب من الخادم. يشغل العديد من التطبيقات الشهيرة الموجودة على الويب اليوم، مثل GitHub، Basecamp، SoundCloud، Airbnb، و Twitch. بفضل تركيزه على تجربة المبرمج والمجتمع المتحمس المبني حوله، سيوفر لك روبي على القضبان الأدوات التي تحتاجها لبناء وصيانة تطبيق الويب الحديث الخاص بك.

رياكت هو مكتبة JavaScript تستخدم لإنشاء واجهات المستخدم الأمامية. بدعم من فيسبوك، إنها واحدة من أكثر المكتبات الأمامية شيوعًا المستخدمة على الويب اليوم. تقدم رياكت ميزات مثل نموذج الكائن الظاهري (DOM)، هندسة المكونات، و إدارة الحالة، مما يجعل عملية تطوير الواجهة الأمامية أكثر تنظيمًا وفعالية.

باقتراب واجهة الويب الأمامية نحو إطارات عمل منفصلة عن كود الخادم، ستمكنك دمج أناقة Rails مع كفاءة React من بناء تطبيقات قوية وحديثة تستفيد من التوجهات الحالية. باستخدام React لعرض المكونات من داخل عرض Rails (بدلاً من محرك القوالب Rails)، سيستفيد تطبيقك من أحدث التطورات في جافا سكريبت وتطوير الواجهة الأمامية بينما يستغل تعبيرية Ruby on Rails.

في هذا البرنامج التعليمي، ستقوم بإنشاء تطبيق Ruby on Rails يخزن وصفاتك المفضلة ثم يعرضها بواجهة أمامية React. عند الانتهاء، ستتمكن من إنشاء، عرض، وحذف الوصفات باستخدام واجهة React مُنسّقة بـ
Bootstrap:

المتطلبات المسبقة

لمتابعة هذا البرنامج التعليمي، تحتاج:

ملاحظة: إصدار Rails 7 ليس متوافقًا مع الإصدارات السابقة. إذا كنت تستخدم إصدار Rails 5 ، يرجى زيارة البرنامج التعليمي لـ كيفية إعداد مشروع Ruby on Rails v5 مع واجهة أمامية React على Ubuntu 18.04.

الخطوة 1 — إنشاء تطبيق Rails جديد

سوف تقوم ببناء تطبيق الوصفات الخاص بك على إطار تطبيقات Rails في هذه الخطوة. أولاً، ستقوم بإنشاء تطبيق Rails جديد، الذي سيتم تكوينه للعمل مع React.

توفر Rails عدة نصوص تسمى مولدات تقوم بإنشاء كل ما يلزم لبناء تطبيق ويب حديث. لمراجعة قائمة كاملة من هذه الأوامر وما تفعله، قم بتشغيل الأمر التالي في الطرفية الخاصة بك:

  1. rails -h

تعتبر هذه الأمر ستنتج قائمة شاملة من الخيارات، مما يتيح لك تحديد معلمات تطبيقك. أحد الأوامر المدرجة هو أمر new، الذي يقوم بإنشاء تطبيق Rails جديد.

الآن، ستقوم بإنشاء تطبيق Rails جديد باستخدام مولد new. قم بتشغيل الأمر التالي في الطرفية الخاصة بك:

  1. rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T

الأمر السابق يقوم بإنشاء تطبيق Rails جديد في دليل يحمل اسم rails_react_recipe، ويقوم بتثبيت التبعيات اللازمة للروبي والجافا سكريبت، ويكون تكوين Webpack. العلامات المرتبطة بهذا الأمر الأمر new تتضمن ما يلي:

  • تحديد العلامة -d المحرك المفضل لقاعدة البيانات، والذي هو في هذه الحالة PostgreSQL.
  • تحديد العلامة -j نهج الجافا سكريبت للتطبيق. يقدم Rails بعض الطرق المختلفة للتعامل مع رمز جافا سكريبت في تطبيقات Rails. الخيار esbuild الذي يتم تمريره إلى العلامة -j يوجه Rails لتهيئة esbuild كمجمع جافا سكريبت المفضل.
  • تحديد العلامة -c معالج CSS للتطبيق. Bootstrap هو الخيار المفضل في هذه الحالة.
  • توجيه العلامة -T Rails لتخطي إنشاء ملفات الاختبار لأنك لن تكتب اختبارات لهذا البرنامج التعليمي. يُقترح هذا الأمر أيضًا إذا كنت ترغب في استخدام أداة اختبار Ruby مختلفة عن تلك التي يوفرها Rails.

بمجرد انتهاء الأمر، انتقل إلى الدليل rails_react_recipe، وهو الدليل الرئيسي لتطبيقك:

  1. cd rails_react_recipe

ثم، قم بسرد محتويات الدليل:

  1. ls

Output
Gemfile README.md bin db node_modules storage yarn.lock Gemfile.lock Rakefile config lib package.json tmp Procfile.dev app config.ru log public vendor

تحتوي هذه الدليل الأساسي على عدة ملفات ومجلدات تم إنشاؤها تلقائيًا والتي تشكل هيكل تطبيق Rails، بما في ذلك ملف package.json الذي يحتوي على الاعتماديات الخاصة بتطبيق React.

الآن بعد أن قمت بإنشاء تطبيق Rails جديد بنجاح، ستقوم بربطه بقاعدة بيانات في الخطوة التالية.

الخطوة 2 – إعداد قاعدة البيانات

قبل تشغيل تطبيق Rails الجديد الخاص بك، يجب عليك أولاً توصيله بقاعدة بيانات. في هذه الخطوة، ستقوم بتوصيل تطبيق Rails الذي تم إنشاؤه حديثًا بقاعدة بيانات PostgreSQL بحيث يمكن تخزين بيانات الوصفة واسترجاعها حسب الحاجة.

يحتوي ملف database.yml الموجود في config/database.yml على تفاصيل قاعدة البيانات مثل أسماء قواعد البيانات لبيئات التطوير المختلفة. يُحدد Rails اسم قاعدة البيانات لبيئات التطوير المختلفة عن طريق إضافة شرطة سفلية (_) تليها اسم البيئة. في هذا البرنامج التعليمي، ستستخدم قيم تكوين قاعدة البيانات الافتراضية، لكن يمكنك تغيير قيم تكوينك إذا لزم الأمر.

ملاحظة: في هذه النقطة، يمكنك تعديل config/database.yml لتحديد الدور الذي ترغب في أن يستخدم Rails لإنشاء قاعدة البيانات الخاصة بك. خلال الخطوات الأولية، قمت بإنشاء دور محمي بكلمة مرور في الدورة التعليمية كيفية استخدام PostgreSQL مع تطبيق Ruby on Rails الخاص بك. إذا لم تقم بتحديد المستخدم بعد، يمكنك الآن اتباع التعليمات في الخطوة 4 — تكوين وإنشاء قاعدة البيانات الخاصة بك في نفس دورة الأولية.

تقدم Rails العديد من الأوامر التي تجعل تطوير تطبيقات الويب سهلة، بما في ذلك الأوامر للعمل مع قواعد البيانات مثل create، drop، و reset. لإنشاء قاعدة بيانات لتطبيقك، قم بتشغيل الأمر التالي في الطرفية الخاصة بك:

  1. rails db:create

يقوم هذا الأمر بإنشاء قاعدة بيانات development و test، مما يؤدي إلى الناتج التالي:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

الآن بمجرد أن يكون التطبيق متصلاً بقاعدة بيانات، قم بتشغيل التطبيق عبر تشغيل الأمر التالي:

  1. bin/dev

توفر Rails نصًا بديلاً bin/dev يبدأ تطبيق Rails عن طريق تنفيذ الأوامر في ملف Procfile.dev في دليل التطبيق الجذر باستخدام الجوهرة Foreman.

بمجرد تشغيل هذا الأمر، سيختفي موجه الأوامر الخاصة بك، وسيظهر في مكانه الناتج التالي:

Output
started with pid 70099 started with pid 70100 started with pid 70101 yarn run v1.22.10 yarn run v1.22.10 $ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch $ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch => Booting Puma => Rails 7.0.4 application starting in development => Run `bin/rails server --help` for more startup options [watch] build finished, watching for changes... Puma starting in single mode... * Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 70099 * Listening on http://127.0.0.1:3000 * Listening on http://[::1]:3000 Use Ctrl-C to stop Sass is watching for changes. Press Ctrl-C to stop.

للوصول إلى تطبيقك، افتح نافذة متصفح وانتقل إلى http://localhost:3000. سيتم تحميل صفحة الترحيب الافتراضية لـ Rails، مما يعني أنك قمت بتكوين تطبيق Rails الخاص بك بشكل صحيح:

لإيقاف خادم الويب، اضغط CTRL+C في الطرفية حيث يعمل الخادم. ستحصل على رسالة وداع من Puma:

Output
^C SIGINT received, starting shutdown - Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting sending SIGTERM to all processes terminated by SIGINT terminated by SIGINT exited with code 0

ثم سيعود سطر الأوامر الخاص بك.

لقد نجحت في تكوين قاعدة بيانات لتطبيق وصفات الطعام الخاص بك. في الخطوة التالية، ستقوم بتثبيت الاعتماديات الجافا سكريبت التي تحتاجها لتجميع واجهة الواجهة الأمامية لك.

الخطوة 3 — تثبيت اعتماديات الواجهة الأمامية

في هذه الخطوة، ستقوم بتثبيت الاعتماديات الجافا سكريبت اللازمة على الواجهة الأمامية لتطبيق وصفات الطعام الخاص بك. وتشمل:

  • React لبناء واجهات المستخدم.
  • React DOM لتمكين React من التفاعل مع DOM المتصفح.
  • React Router للتعامل مع التنقل في تطبيق React.

قم بتشغيل الأمر التالي لتثبيت هذه الحزم باستخدام مدير الحزم Yarn:

  1. yarn add react react-dom react-router-dom

هذا الأمر يستخدم Yarn لتثبيت الحزم المحددة ويضيفها إلى ملف package.json. للتحقق من ذلك، قم بفتح ملف package.json الموجود في الدليل الرئيسي للمشروع:

  1. nano package.json

سيتم سرد الحزم المثبتة تحت مفتاح dependencies:

~/rails_react_recipe/package.json
{
  "name": "app",
  "private": "true",
  "dependencies": {
    "@hotwired/stimulus": "^3.1.0",
    "@hotwired/turbo-rails": "^7.1.3",
    "@popperjs/core": "^2.11.6",
    "bootstrap": "^5.2.1",
    "bootstrap-icons": "^1.9.1",
    "esbuild": "^0.15.7",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.3.0",
    "sass": "^1.54.9"
  },
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}

أغلق الملف بالضغط على CTRL+X.

لقد قمت بتثبيت بعض الاعتماديات الأمامية لتطبيقك. فيما بعد، ستقوم بإعداد صفحة رئيسية لتطبيق وصفات الطعام الخاص بك.

الخطوة 4 — إعداد الصفحة الرئيسية

بعد تثبيت الاعتماديات المطلوبة، ستقوم الآن بإنشاء صفحة رئيسية للتطبيق لتكون الصفحة الرئيسية عندما يزور المستخدمون التطبيق لأول مرة.

تتبع Rails نمط التصميم المعماري Model-View-Controller للتطبيقات. في نمط MVC، يكمن هدف المتحكم في استقبال الطلبات المحددة وتمريرها إلى النموذج أو العرض المناسب. يعرض التطبيق حاليًا صفحة ترحيب Rails عند تحميل عنوان URL الأساسي في المتصفح. لتغيير هذا، ستقوم بإنشاء متحكم وعرض للصفحة الرئيسية ثم تطابقه مع مسار.

يوفر Rails مولد controller لإنشاء متحكم. يستقبل مولد controller اسم المتحكم والإجراء المطابق. لمزيد من المعلومات حول هذا، يمكنك مراجعة وثائق Rails.

سيتم تسمية المتحكم في هذا البرنامج التعليمي بـ Homepage. قم بتشغيل الأمر التالي لإنشاء متحكم Homepage مع إجراء index:

  1. rails g controller Homepage index

ملاحظة:
على نظام Linux، قد ينتج عن الخطأ FATAL: Listen error: unable to monitor directories for changes. قيود النظام على عدد الملفات التي يمكن للجهاز رصدها للتغييرات قد تكون السبب. قم بتشغيل الأمر التالي لإصلاحه:

  1. echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

سيزيد هذا الأمر بشكل دائم عدد الدلائل التي يمكنك مراقبتها باستخدام Listen إلى 524288. يمكنك تغيير هذا مرة أخرى عن طريق تشغيل نفس الأمر واستبدال 524288 بالرقم المطلوب.

سيؤدي تشغيل أمر controller إلى إنشاء الملفات التالية:

  • A homepage_controller.rb file for receiving all homepage-related requests. This file contains the index action you specified in the command.
  • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
  • ملف index.html.erb كصفحة عرض لأي شيء متعلق بالصفحة الرئيسية.

بالإضافة إلى هذه الصفحات الجديدة التي تم إنشاؤها بتشغيل أمر Rails، يقوم Rails أيضًا بتحديث ملف التوجيهات الخاص بك الموجود في config/routes.rb، ويضيف طريقًا get لصفحة البداية، والتي ستقوم بتعديلها كطريقة رئيسية.

A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb in your favorite editor:

  1. nano config/routes.rb

في هذا الملف، قم بتعويض get 'homepage/index' بـ root 'homepage#index' حتى يتطابق الملف مع ما يلي:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # للحصول على تفاصيل حول لغة التوجيه المتاحة في هذا الملف، انظر إلى http://guides.rubyonrails.org/routing.html
end

تقوم هذه التعديلات بتوجيه Rails لمطابقة الطلبات الموجهة إلى جذر التطبيق إلى إجراء index في تحكم Homepage، الذي بدوره يقوم بعرض محتوى ملف index.html.erb الذي يقع في app/views/homepage/index.html.erb على المتصفح.

احفظ وأغلق الملف.

للتحقق من أن هذا يعمل، قم بتشغيل تطبيقك:

  1. bin/dev

عند فتح التطبيق في المتصفح أو تحديثه، سيتم تحميل صفحة هبوط جديدة لتطبيقك:

بمجرد التحقق من أن تطبيقك يعمل، اضغط على CTRL+C لإيقاف الخادم.

بعد ذلك، افتح ملف ~/rails_react_recipe/app/views/homepage/index.html.erb:

  1. nano ~/rails_react_recipe/app/views/homepage/index.html.erb

قم بإزالة الكود داخل الملف، ثم احفظ الملف كفارغ. من خلال القيام بذلك، تضمن أن محتويات index.html.erb لا تتداخل مع عملية تقديم React لواجهة الأمام الخاصة بك.

الآن بعد أن قمت بتكوين صفحة البداية لتطبيقك، يمكنك الانتقال إلى القسم التالي، حيث ستقوم بتكوين واجهة التطبيق الأمامية باستخدام React.

الخطوة 5 — تكوين React كواجهة أمامية لـ Rails الخاصة بك

في هذه الخطوة، ستقوم بتكوين Rails لاستخدام React على الواجهة الأمامية للتطبيق بدلاً من محرك القوالب الخاص به. تكوين هذا الجديد سيسمح لك بإنشاء صفحة رئيسية أكثر جاذبية بصريًا باستخدام React.

بفضل الخيار esbuild المحدد عند توليد تطبيق Rails، فإن معظم الإعدادات المطلوبة للسماح للجافا سكريبت بالعمل بسلاسة مع Rails موجودة بالفعل. كل ما تبقى هو تحميل نقطة الدخول إلى تطبيق React في نقطة الدخول لملفات الجافا سكريبت في esbuild. للقيام بذلك، ابدأ بإنشاء دليل للمكونات في الدليل app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/components

سيحتوي دليل components على المكون للصفحة الرئيسية، إلى جانب المكونات الأخرى لتطبيق React، بما في ذلك ملف الدخول إلى تطبيق React.

ثم، افتح ملف application.js الموجود في app/javascript/application.js:

  1. nano ~/rails_react_recipe/app/javascript/application.js

أضف السطر المبرز في الملف:

~/rails_react_recipe/app/javascript/application.js
// نقطة الدخول لنص البناء في package.json الخاص بك
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"

سيقوم السطر المضاف إلى ملف application.js باستيراد الكود في ملف الدخول index.jsx، مما يجعله متاحًا لـ esbuild للتجميع. باستيراد الدليل /components إلى نقطة الدخول الخاصة بجافا سكريبت لتطبيق Rails، يمكنك إنشاء مكون React لصفحتك الرئيسية. ستحتوي الصفحة الرئيسية على بعض النصوص وزر دعوة للعمل لعرض جميع الوصفات.

احفظ وأغلق الملف.

ثم، أنشئ ملف Home.jsx في دليل components:

  1. nano ~/rails_react_recipe/app/javascript/components/Home.jsx

أضف الكود التالي إلى الملف:

~/rails_react_recipe/app/javascript/components/Home.jsx
import React from "react";
import { Link } from "react-router-dom";

export default () => (
  <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
    <div className="jumbotron jumbotron-fluid bg-transparent">
      <div className="container secondary-color">
        <h1 className="display-4">Food Recipes</h1>
        <p className="lead">
          A curated list of recipes for the best homemade meal and delicacies.
        </p>
        <hr className="my-4" />
        <Link
          to="/recipes"
          className="btn btn-lg custom-button"
          role="button"
        >
          View Recipes
        </Link>
      </div>
    </div>
  </div>
);

بهذا الكود، تقوم باستيراد مكتبة React ومكون Link من React Router. يقوم مكون Link بإنشاء رابط للتنقل من صفحة إلى أخرى. بعد ذلك، تقوم بإنشاء وتصدير مكون وظيفي يحتوي على بعض لغة Markup لصفحتك الرئيسية، مزودة بتنسيق باستخدام فئات Bootstrap.

احفظ وأغلق الملف.

بعد تحديد مكون Home، ستقوم الآن بإعداد التوجيه باستخدام React Router. قم بإنشاء دليل routes في دليل app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/routes

سيحتوي دليل routes على بعض المسارات مع مكوناتها المقابلة. عند تحميل أي مسار محدد، سيقوم بتقديم مكونه المقابل إلى المتصفح.

في دليل routes، قم بإنشاء ملف index.jsx:

  1. nano ~/rails_react_recipe/app/javascript/routes/index.jsx

أضف الكود التالي إليه:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";

export default (
  <Router>
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  </Router>
);

في ملف مسار index.jsx هذا، تقوم بإستيراد الوحدات التالية: وحدة React التي تسمح لك باستخدام React، بالإضافة إلى وحدات BrowserRouter، Routes، و Route من React Router، والتي تساعدك معًا على التنقل من مسار إلى آخر. أخيرًا، تقوم بإستيراد مكون Home الخاص بك، الذي سيتم تقديمه عندما يتطابق الطلب مع المسار الجذري (/). عندما ترغب في إضافة صفحات إضافية إلى تطبيقك، يمكنك إعلان مسار في هذا الملف وتطابقه مع المكون الذي ترغب في تقديمه لتلك الصفحة.

احفظ وأغلق الملف.

لقد قمت الآن بإعداد التوجيه باستخدام React Router. لكي يكون React على علم بالمسارات المتاحة واستخدامها، يجب أن تكون المسارات متاحة في نقطة الدخول إلى التطبيق. لتحقيق ذلك، ستقوم بتقديم المسارات الخاصة بك في مكون سيتم تقديمه بواسطة React في ملف الدخول الخاص بالتطبيق.

قم بإنشاء ملف App.jsx في دليل app/javascript/components:

  1. nano ~/rails_react_recipe/app/javascript/components/App.jsx

أضف الكود التالي إلى ملف App.jsx:

~/rails_react_recipe/app/javascript/components/App.jsx
import React from "react";
import Routes from "../routes";

export default props => <>{Routes}</>;

في ملف App.jsx، استورد React وملفات المسارات التي أنشأتها للتو. ثم قم بتصدير مكون لتقديم المسارات ضمن قطع. سيتم تقديم هذا المكون في نقطة دخول التطبيق، مما يجعل المسارات متاحة في كل مرة يتم تحميل التطبيق.

احفظ وأغلق الملف.

الآن بعد أن قمت بإعداد ملف App.jsx، يمكنك تقديمه في ملف الدخول الخاص بك. قم بإنشاء ملف index.jsx في الدليل components:

  1. nano ~/rails_react_recipe/app/javascript/components/index.jsx

أضف الكود التالي إلى ملف index.js:

~/rails_react_recipe/app/javascript/components/index.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

document.addEventListener("turbo:load", () => {
  const root = createRoot(
    document.body.appendChild(document.createElement("div"))
  );
  root.render(<App />);
});

في سطور الاستيراد، استورد مكتبة React ووظيفة createRoot من ReactDOM، ومكون App الخاص بك. باستخدام وظيفة createRoot من ReactDOM، قم بإنشاء عنصر جذري كعنصر div يتم إضافته إلى الصفحة، وقم بتقديم مكون App الخاص بك فيه. عند تحميل التطبيق، ستقوم React بتقديم محتوى مكون App داخل عنصر div على الصفحة.

احفظ وأغلق الملف.

أخيرًا، ستضيف بعض أنماط CSS إلى صفحة البداية الخاصة بك.

Translated text:

افتح ملف application.bootstrap.scss في دليل ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss الخاص بك:

  1. nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss

ثم، استبدل محتويات ملف application.bootstrap.scss بالكود التالي:

~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';

.bg_primary-color {
  background-color: #FFFFFF;
}
.primary-color {
  background-color: #FFFFFF;
}
.bg_secondary-color {
  background-color: #293241;
}
.secondary-color {
  color: #293241;
}
.custom-button.btn {
  background-color: #293241;
  color: #FFF;
  border: none;
}
.hero {
  width: 100vw;
  height: 50vh;
}
.hero img {
  object-fit: cover;
  object-position: top;
  height: 100%;
  width: 100%;
}
.overlay {
  height: 100%;
  width: 100%;
  opacity: 0.4;
}

قم بتعيين بعض الألوان المخصصة للصفحة. سيقوم قسم .hero بإنشاء الهيكل الأساسي لصورة hero، أو صورة كبيرة على الصفحة الرئيسية لموقع الويب الخاص بك، والتي ستضيفها لاحقًا. بالإضافة إلى ذلك، custom-button.btn ينسق الزر الذي سيستخدمه المستخدم للدخول إلى التطبيق.

مع أنماط CSS الخاصة بك في مكانها، قم بحفظ وإغلاق الملف.

ثم، أعد تشغيل خادم الويب الخاص بتطبيقك:

  1. bin/dev

ثم أعد تحميل التطبيق في متصفحك. ستظهر صفحة رئيسية جديدة تمامًا:

قم بإيقاف خادم الويب باستخدام CTRL+C.

لقد قمت بتكوين تطبيقك لاستخدام React كواجهة أمامية في هذه الخطوة. في الخطوة التالية، ستقوم بإنشاء نماذج وتحكمات تمكنك من إنشاء وقراءة وتحديث وحذف الوصفات.

الخطوة 6 — إنشاء تحكم الوصفة والنموذج

الآن بعد إعداد واجهة المستخدم الأمامية باستخدام React لتطبيقك، ستقوم بإنشاء نموذج ومتحكم للوصفة. سيمثل نموذج الوصفة جدول قاعدة البيانات الذي يحتوي على معلومات حول وصفات المستخدم، بينما سيستقبل ويتعامل المتحكم مع الطلبات لإنشاء وقراءة وتحديث أو حذف الوصفات. عندما يطلب المستخدم وصفة، يستقبل المتحكم للوصفة هذا الطلب ويمرره إلى نموذج الوصفة، الذي يسترد البيانات المطلوبة من قاعدة البيانات. ثم يقوم النموذج بإرجاع بيانات الوصفة كاستجابة للمتحكم. وأخيرًا، يتم عرض هذه المعلومات في المتصفح.

ابدأ بإنشاء نموذج “الوصفة” باستخدام أمر “توليد النموذج” المقدم من قبل Rails وحدد اسم النموذج مع أعمدته وأنواع البيانات. قم بتشغيل الأمر التالي:

  1. rails generate model Recipe name:string ingredients:text instruction:text image:string

يوجّه الأمر السابق Rails لإنشاء نموذج “الوصفة” جنبًا إلى جنب مع عمود “الاسم” من نوع “سلسلة”، وعمود “المكونات” و”التعليمات” من نوع “نص”، وعمود “الصورة” من نوع “سلسلة”. في هذا البرنامج التعليمي، تم أطلاق اسم النموذج “الوصفة”، لأن النماذج في Rails تستخدم اسمًا مفردًا بينما تستخدم جداول قاعدة البيانات المقابلة أسماء جمعية.

تقوم عملية توليد النموذج بإنشاء ملفين وتطبع النتيجة التالية:

Output
invoke active_record create db/migrate/20221017220817_create_recipes.rb create app/models/recipe.rb

الملفين التي تم إنشاؤهما هما:

  • A recipe.rb file that holds all the model-related logic.
  • A 20221017220817_create_recipes.rb file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.

في الخطوة التالية، ستقوم بتعديل ملف نموذج الوصفة للتأكد من أنه يتم حفظ البيانات الصحيحة فقط في قاعدة البيانات. يمكنك تحقيق ذلك عن طريق إضافة بعض التحققات إلى قاعدة البيانات في نموذجك.

افتح ملف نموذج الوصفة الخاص بك الموجود في “app/models/recipe.rb”:

  1. nano ~/rails_react_recipe/app/models/recipe.rb

أضف السطور المظللة التالية من الشيفرة إلى الملف:

~/rails_react_recipe/app/models/recipe.rb
class Recipe < ApplicationRecord
  validates :name, presence: true
  validates :ingredients, presence: true
  validates :instruction, presence: true
end

في هذه الشيفرة، تقوم بإضافة التحقق من النموذج، الذي يتحقق من وجود الحقول name، ingredients، و instruction. بدون هذه الحقول الثلاثة، يعتبر الوصفة غير صالحة ولن تُحفظ في قاعدة البيانات.

احفظ وأغلق الملف.

لإنشاء جدول recipes في قاعدة بياناتك باستخدام Rails، يجب عليك تشغيل عملية migration، وهو الطريقة لإجراء تغييرات على قاعدة بياناتك بشكل برمجي. لضمان أن العملية تعمل مع القاعدة التي أنشأتها، يجب عليك إجراء تغييرات على الملف 20221017220817_create_recipes.rb.

افتح هذا الملف في محرر النصوص:

  1. nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb

أضف المواد المظللة حتى يطابق ملفك الملف التالي:

db/migrate/20221017220817_create_recipes.rb
class CreateRecipes < ActiveRecord::Migration[5.2]
  def change
    create_table :recipes do |t|
      t.string :name, null: false
      t.text :ingredients, null: false
      t.text :instruction, null: false
      t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'

      t.timestamps
    end
  end
end

يحتوي هذا الملف الذي تم تغييره على فئة Ruby تحتوي على طريقة change وأمر لإنشاء جدول يسمى recipes مع الأعمدة وأنواع البيانات الخاصة بها. كما تقوم بتحديث 20221017220817_create_recipes.rb بشرط NOT NULL على الأعمدة name، ingredients، و instruction عن طريق إضافة null: false، مما يضمن أن تكون لهذه الأعمدة قيمة قبل تغيير قاعدة البيانات. وأخيرًا، تضيف عنوان URL افتراضي لعمود الصورة؛ يمكن أن يكون هذا عنوان URL آخر إذا كنت ترغب في استخدام صورة مختلفة.

مع هذه التغييرات، احفظ واخرج من الملف. الآن، أنت جاهز لتشغيل عملية التحرك وإنشاء جدولك. في الطرفية، قم بتشغيل الأمر التالي:

  1. rails db:migrate

تستخدم أمر ترحيل قاعدة البيانات لتشغيل التعليمات في ملف الترحيل الخاص بك. بمجرد أن ينجح الأمر في التشغيل بنجاح، ستتلقى إخراجًا مشابهًا لما يلي:

Output
== 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

بعد وضع نموذج الوصفة الخاص بك في المكان، ستقوم بإنشاء تحكم الوصفات الخاص بك لإضافة المنطق لإنشاء وقراءة وحذف الوصفات. قم بتشغيل الأمر التالي:

  1. rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper

في هذا الأمر، ستقوم بإنشاء تحكم “الوصفات” في دليل “api/v1” مع إجراءات “index” و “create” و “show” و “destroy”. سيتم التعامل مع إجراء “index” لجلب جميع وصفاتك؛ سيكون إجراء “create” مسؤولًا عن إنشاء وصفات جديدة؛ سيسترد إجراء “show” وصفة واحدة، وسيحتوي إجراء “destroy” على المنطق لحذف وصفة.

كما تمرر بعض العلامات لجعل التحكم أكثر خفة، بما في ذلك:

  • --skip-template-engine، الذي يوجه Rails إلى تخطي إنشاء ملفات عرض Rails لأن React يتعامل مع احتياجات واجهة المستخدم الخاصة بك.
  • --no-helper، الذي يوجه Rails إلى تخطي إنشاء ملف مساعد لتحكمك.

يقوم تشغيل الأمر أيضًا بتحديث ملف الطرق الخاص بك بطريقة لكل إجراء في تحكم “الوصفات”.

عندما يتم تشغيل الأمر، سيقوم بطباعة إخراج مثل هذا:

Output
create app/controllers/api/v1/recipes_controller.rb route namespace :api do namespace :v1 do get 'recipes/index' get 'recipes/create' get 'recipes/show' get 'recipes/destroy' end end

لاستخدام هذه الطرق، ستقوم بإجراء تغييرات على ملف “config/routes.rb” الخاص بك. افتح ملف “routes.rb” في محرر النص الخاص بك:

  1. nano ~/rails_react_recipe/config/routes.rb

قم بتحديث هذا الملف ليبدو مثل الكود التالي، عن طريق تغيير أو إضافة الأسطر المظللة:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      get 'recipes/index'
      post 'recipes/create'
      get '/show/:id', to: 'recipes#show'
      delete '/destroy/:id', to: 'recipes#destroy'
    end
  end
  root 'homepage#index'
  get '/*path' => 'homepage#index'
  # حدد مسارات تطبيقك وفقًا لـ DSL في https://guides.rubyonrails.org/routing.html

   # يحدد مسار الجذر ("/")
  # root "articles#index"
end

في هذا الملف تعديل الفعل HTTP لمسارات create و destroy بحيث يمكن أن يقوم بـ post و delete للبيانات. كما تعدل مسارات show و destroy بإضافة معلمة :id إلى المسار. :id سيحمل رقم التعريف للوصفة التي تريد قراءتها أو حذفها.

أضف مسارًا يلتقط كل الطلبات باستخدام get '/*path' التي لا تتطابق مع المسارات الحالية إلى index لوحدة تحكم homepage. سيتعامل توجيه الواجهة الأمامية مع الطلبات غير المتعلقة بإنشاء أو قراءة أو حذف الوصفات.

احفظ واخرج من الملف.

لتقييم قائمة المسارات المتاحة في تطبيقك ، قم بتشغيل الأمر التالي:

  1. rails routes

تعرض تشغيل هذا الأمر قائمة طويلة من أنماط URI وأفعال الأفعال أو وحدات التحكم المطابقة لمشروعك.

فيما يلي ، ستقوم بإضافة المنطق للحصول على جميع الوصفات دفعة واحدة. تستخدم Rails مكتبة ActiveRecord للتعامل مع المهام المتعلقة بقاعدة البيانات مثل هذه. يربط ActiveRecord بين الفئات وجداول قاعدة البيانات العلائقية ويوفر واجهة برمجة تطبيق غنية للعمل معها.

للحصول على جميع الوصفات ، ستستخدم ActiveRecord لاستعلام جدول الوصفات واسترداد جميع الوصفات في قاعدة البيانات.

افتح ملف recipes_controller.rb باستخدام الأمر التالي:

  1. nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

أضف الأسطر المحددة إلى متحكم الوصفات:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
  end

  def show
  end

  def destroy
  end
end

في إجراء index الخاص بك ، تستخدم أسلوب all في ActiveRecord للحصول على جميع الوصفات في قاعدة البيانات الخاصة بك. باستخدام أسلوب order ، ترتبها في ترتيب تنازلي حسب تاريخ إنشائها ، مما سيضع أحدث الوصفات أولاً. وأخيرًا ، ترسل قائمتك من الوصفات كاستجابة JSON باستخدام render.

بعد ذلك ، ستقوم بإضافة المنطق لإنشاء وصفات جديدة. كما هو الحال مع جلب جميع الوصفات ، ستعتمد على ActiveRecord للتحقق وحفظ تفاصيل الوصفة المقدمة. قم بتحديث متحكم الوصفات الخاص بك بالأسطر المحددة التالية من الكود:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
    recipe = Recipe.create!(recipe_params)
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def show
  end

  def destroy
  end

  private

  def recipe_params
    params.permit(:name, :image, :ingredients, :instruction)
  end
end

في الفعل create، يتم استخدام طريقة create في ActiveRecord لإنشاء وصفة جديدة. يمكن للطريقة create تخصيص جميع معلمات المتحكم المقدمة إلى النموذج دفعة واحدة. تجعل هذه الطريقة عملية إنشاء السجلات سهلة، ولكنها تفتح الباب أمام احتمال استخدام خبيث. يمكن تجنب الاستخدام الخبيث باستخدام ميزة strong parameters المقدمة من Rails. بهذه الطريقة، لا يمكن تخصيص المعلمات ما لم يتم السماح بها. يتم تمرير معلمة recipe_params إلى طريقة create في الشيفرة الخاصة بك. recipe_params هي طريقة private حيث تسمح لمعلمات المتحكم بمنع دخول محتوى خاطئ أو خبيث إلى قاعدة البيانات الخاصة بك. في هذه الحالة، تسمح بمعلمات name، image، ingredients، و instruction لاستخدام صحيح للطريقة create.

يمكن لمتحكم الوصفة الآن قراءة وإنشاء وصفات. كل ما تبقى هو منطق القراءة والحذف لوصفة واحدة. قم بتحديث متحكم الوصفات الخاص بك بالشيفرة المظللة:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  before_action :set_recipe, only: %i[show destroy]

  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
    recipe = Recipe.create!(recipe_params)
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def show
    render json: @recipe
  end

  def destroy
    @recipe&.destroy
    render json: { message: 'Recipe deleted!' }
  end

  private

  def recipe_params
    params.permit(:name, :image, :ingredients, :instruction)
  end

  def set_recipe
    @recipe = Recipe.find(params[:id])
  end
end

في الأسطر الجديدة من الكود، أنت تُنشئ طريقة خاصة set_recipe يتم استدعاؤها بواسطة before_action فقط عندما تتطابق الإجراءات show و delete مع طلب. تستخدم طريقة set_recipe طريقة find من ActiveRecord للعثور على وصفة طعام تطابق id المقدم في params وتخصيصها لمتغير مثيل @recipe. في إجراء show، تُرجع كائن @recipe المُعد بواسطة طريقة set_recipe كاستجابة JSON.

في إجراء destroy، قمت بشيء مشابه باستخدام عامل التشغيل الآمن &. من Ruby، الذي يتجنب أخطاء nil عند استدعاء طريقة. هذه الإضافة تتيح لك حذف وصفة طعام فقط إذا كانت موجودة، ثم إرسال رسالة كاستجابة.

بعد إجراء هذه التغييرات على recipes_controller.rb، احفظ الملف وأغلقه.

في هذه الخطوة، قمت بإنشاء نموذج ومتحكم لوصفات الطعام الخاصة بك. لقد كتبت كل المنطق اللازم للعمل مع وصفات الطعام في الخلفية. في القسم التالي، ستقوم بإنشاء مكونات لعرض وصفات الطعام.

الخطوة 7 — عرض الوصفات

في هذا القسم، ستقوم بإنشاء مكونات لعرض الوصفات. ستقوم بإنشاء صفحتين: واحدة لعرض جميع الوصفات الموجودة وأخرى لعرض وصفات فردية.

ستبدأ بإنشاء صفحة لعرض جميع الوصفات. قبل إنشاء الصفحة ، تحتاج إلى وصفات للعمل معها ، حيث أن قاعدة البيانات الخاصة بك فارغة حاليًا. يوفر Rails طريقة لإنشاء بيانات بداية لتطبيقك.

قم بفتح ملف البذور المسمى seeds.rb للتحرير:

  1. nano ~/rails_react_recipe/db/seeds.rb

قم بتعويض محتويات الملف الأصلية بالشفرة التالية:

~/rails_react_recipe/db/seeds.rb
9.times do |i|
  Recipe.create(
    name: "Recipe #{i + 1}",
    ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
    instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
  )
end

في هذه الشفرة ، يتم استخدام حلقة تُعلِّم Rails إنشاء تسعة وصفات بأقسام للأسماء والمكونات والتعليمات. احفظ وأغلق الملف.

لزراعة قاعدة البيانات بهذه البيانات ، قم بتشغيل الأمر التالي في الطرفية الخاصة بك:

  1. rails db:seed

بتشغيل هذا الأمر ، ستتم إضافة تسعة وصفات إلى قاعدة البيانات الخاصة بك. يمكنك الآن جلبها وتقديمها على واجهة المستخدم.

سيقوم المكون لعرض جميع الوصفات بإرسال طلب HTTP إلى إجراء index في RecipesController للحصول على قائمة بجميع الوصفات. سيتم عرض هذه الوصفات في بطاقات على الصفحة.

قم بإنشاء ملف Recipes.jsx في الدليل app/javascript/components:

  1. nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

بمجرد فتح الملف ، استورد وحدات React و useState و useEffect و Link و useNavigate عن طريق إضافة الأسطر التالية:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

بعد ذلك ، أضف الأسطر المميزة لإنشاء وتصدير مكون React وظيفي يسمى Recipes:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

const Recipes = () => {
  const navigate = useNavigate();
  const [recipes, setRecipes] = useState([]);
};

export default Recipes;

داخل مكون Recipe، ستقوم واجهة توجيه React Router باستدعاء خطاف useNavigate. خطاف useState في React سيقوم بتهيئة حالة recipes، وهي مصفوفة فارغة ([])، ووظيفة setRecipes لتحديث حالة recipes.

بعد ذلك، في خطاف useEffect، ستقوم بعمل طلب HTTP لاسترجاع جميع الوصفات الخاصة بك. للقيام بذلك، أضف الأسطر المحددة:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

const Recipes = () => {
  const navigate = useNavigate();
  const [recipes, setRecipes] = useState([]);

  useEffect(() => {
    const url = "/api/v1/recipes/index";
    fetch(url)
      .then((res) => {
        if (res.ok) {
          return res.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((res) => setRecipes(res))
      .catch(() => navigate("/"));
  }, []);
};

export default Recipes;

في خطاف useEffect الخاص بك، ستقوم بالاتصال HTTP لاسترجاع جميع الوصفات باستخدام API الاسترجاع. إذا كانت الاستجابة ناجحة، يقوم التطبيق بحفظ مصفوفة الوصفات في حالة recipes. إذا حدث خطأ، فسيقوم بتوجيه المستخدم إلى الصفحة الرئيسية.

أخيرًا، قم بإرجاع علامات الترميز للعناصر التي سيتم تقييمها وعرضها على صفحة المتصفح عند تقديم المكون. في هذه الحالة، سيقوم المكون بعرض بطاقة للوصفات من حالة recipes. أضف الأسطر المحددة إلى Recipes.jsx:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

const Recipes = () => {
  const navigate = useNavigate();
  const [recipes, setRecipes] = useState([]);

  useEffect(() => {
    const url = "/api/v1/recipes/index";
    fetch(url)
      .then((res) => {
        if (res.ok) {
          return res.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((res) => setRecipes(res))
      .catch(() => navigate("/"));
  }, []);

  const allRecipes = recipes.map((recipe, index) => (
    <div key={index} className="col-md-6 col-lg-4">
      <div className="card mb-4">
        <img
          src={recipe.image}
          className="card-img-top"
          alt={`${recipe.name} image`}
        />
        <div className="card-body">
          <h5 className="card-title">{recipe.name}</h5>
          <Link to={`/recipe/${recipe.id}`} className="btn custom-button">
            View Recipe
          </Link>
        </div>
      </div>
    </div>
  ));
  const noRecipe = (
    <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
      <h4>
        No recipes yet. Why not <Link to="/new_recipe">create one</Link>
      </h4>
    </div>
  );

  return (
    <>
      <section className="jumbotron jumbotron-fluid text-center">
        <div className="container py-5">
          <h1 className="display-4">Recipes for every occasion</h1>
          <p className="lead text-muted">
            We’ve pulled together our most popular recipes, our latest
            additions, and our editor’s picks, so there’s sure to be something
            tempting for you to try.
          </p>
        </div>
      </section>
      <div className="py-5">
        <main className="container">
          <div className="text-end mb-3">
            <Link to="/recipe" className="btn custom-button">
              Create New Recipe
            </Link>
          </div>
          <div className="row">
            {recipes.length > 0 ? allRecipes : noRecipe}
          </div>
          <Link to="/" className="btn btn-link">
            Home
          </Link>
        </main>
      </div>
    </>
  );
};

export default Recipes;

احفظ واخرج من Recipes.jsx.

الآن بمجرد أن قمت بإنشاء مكون لعرض جميع الوصفات، ستقوم بإنشاء مسار له. افتح ملف المسار الأمامي app/javascript/routes/index.jsx:

  1. nano app/javascript/routes/index.jsx

أضف الأسطر المحددة إلى الملف:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" element={<Recipes />} />
    </Routes>
  </Router>
);

احفظ واخرج من الملف.

في هذه النقطة ، من الجيد التحقق من أن الكود يعمل كما هو متوقع. كما فعلت سابقًا ، استخدم الأمر التالي لبدء الخادم الخاص بك:

  1. bin/dev

ثم افتح التطبيق في المتصفح الخاص بك. اضغط على زر “عرض الوصفة” في الصفحة الرئيسية للوصول إلى صفحة العرض التي تحتوي على وصفات البذور الخاصة بك:

استخدم CTRL+C في الطرفية الخاصة بك لإيقاف الخادم والعودة إلى سطر الأوامر.

الآن بما أنه يمكنك عرض جميع الوصفات في التطبيق الخاص بك ، حان الوقت لإنشاء مكون ثاني لعرض الوصفات الفردية. أنشئ ملف Recipe.jsx في الدليل app/javascript/components:

  1. nano app/javascript/components/Recipe.jsx

كما هو الحال مع مكون Recipes ، قم بتوريد وحدات React و useState و useEffect و Link و useNavigate و useParam عن طريق إضافة الأسطر التالية:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

بعد ذلك ، أضف الأسطر المضللة لإنشاء وتصدير مكون React الوظيفي المسمى Recipe:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });
};

export default Recipe;

مثل مكون Recipes ، تقوم بتهيئة التنقل في React Router بواسطة الخطاف useNavigate. ستقوم الحالة recipe ودالة setRecipe بتحديث الحالة باستخدام خطاف useState. بالإضافة إلى ذلك ، تقوم باستدعاء الخطاف useParams ، الذي يعيد كائنًا يحتوي على مفاتيح / قيم المعلمات URL.

للعثور على وصفة محددة ، يحتاج التطبيق الخاص بك إلى معرف الوصفة ، مما يعني أن مكون Recipe يتوقع وجود id param في عنوان URL. يمكنك الوصول إليها عبر كائن params الذي يحتوي على قيمة إرجاع خطاف useParams.

ثم، قم بتعريف خطاف useEffect حيث ستصل إلى معلمة id من كائن params. بمجرد الحصول على معلمة id للوصفة، ستقوم بطلب HTTP لاسترجاع الوصفة. أضف الأسطر المظللة إلى ملفك:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);
};

export default Recipe;

في خطاف useEffect، تستخدم قيمة params.id لإجراء طلب HTTP GET لاسترجاع الوصفة التي تمتلك الـ id ثم لحفظها في حالة المكون باستخدام دالة setRecipe. التطبيق يعيد توجيه المستخدم إلى صفحة الوصفات إذا لم تكن الوصفة موجودة.

بعد ذلك، أضف دالة addHtmlEntities التي ستُستخدم لاستبدال كيانات الأحرف بكيانات HTML في المكون. ستأخذ دالة addHtmlEntities سلسلة نصية وستقوم بتبديل جميع الأقواس المفتوحة والمغلقة المهربة بكياناتها HTML. ستساعدك هذه الدالة في تحويل أي حرف مهرب تم حفظه في تعليمات وصفتك. أضف الأسطر المظللة:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };
};

export default Recipe;

أخيرًا، قم بإرجاع العلامات لعرض الوصفة في حالة المكون على الصفحة عن طريق إضافة الأسطر المظللة:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

  const ingredientList = () => {
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);
  
  return (
    <div className="">
      <div className="hero position-relative d-flex align-items-center justify-content-center">
        <img
          src={recipe.image}
          alt={`${recipe.name} image`}
          className="img-fluid position-absolute"
        />
        <div className="overlay bg-dark position-absolute" />
        <h1 className="display-4 position-relative text-white">
          {recipe.name}
        </h1>
      </div>
      <div className="container py-5">
        <div className="row">
          <div className="col-sm-12 col-lg-3">
            <ul className="list-group">
              <h5 className="mb-2">Ingredients</h5>
              {ingredientList()}
            </ul>
          </div>
          <div className="col-sm-12 col-lg-7">
            <h5 className="mb-2">Preparation Instructions</h5>
            <div
              dangerouslySetInnerHTML={{
                __html: `${recipeInstruction}`,
              }}
            />
          </div>
          <div className="col-sm-12 col-lg-2">
            <button
              type="button"
              className="btn btn-danger"
            >
              Delete Recipe
            </button>
          </div>
        </div>
        <Link to="/recipes" className="btn btn-link">
          Back to recipes
        </Link>
      </div>
    </div>
  );
};

export default Recipe;

باستخدام وظيفة ingredientList ، يمكنك تقسيم مكونات الوصفة المفصولة بالفواصل إلى مصفوفة وتطبيق دالة الخريطة عليها لإنشاء قائمة من المكونات. إذا لم تكن هناك مكونات، يعرض التطبيق رسالة تقول لا توجد مكونات متاحة. كما يتم استبدال جميع الأقواس المفتوحة والمغلقة في تعليمات الوصفة عن طريق تمريرها عبر دالة addHtmlEntities. وأخيرًا، يعرض الكود صورة الوصفة كصورة بطل، ويضيف زر حذف الوصفة بجانب تعليمات الوصفة، ويضيف زرًا يربط بالصفحة الرئيسية للوصفات.

ملاحظة: يعتبر استخدام خاصية dangerouslySetInnerHTML في React خطيرًا حيث يعرض تطبيقك لهجمات كتابة النصوص عبر المواقع. يمكن تقليل هذا المخاطر عن طريق التأكد من استبدال الأحرف الخاصة المُدخلة عند إنشاء الوصفات باستخدام دالة stripHtmlEntities المعرفة في مكون NewRecipe.

احفظ وأغلق الملف.

لعرض مكون Recipe على صفحة ما، ستقوم بإضافته إلى ملف التوجيهات. افتح ملف التوجيهات للتحرير:

  1. nano app/javascript/routes/index.jsx

أضف الأسطر المشار إليها باللون المميز إلى الملف:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" element={<Recipe />} />
    </Routes>
  </Router>
);

تقوم باستيراد مكون Recipe في هذا الملف التوجيهي وإضافة مسار. المسار لديه :id كمتغير تعريف يتم استبداله بـid الوصفة التي ترغب في عرضها.

احفظ وأغلق الملف.

استخدم النصيب bin/dev لبدء الخادم مرة أخرى، ثم قم بزيارة http://localhost:3000 في متصفحك. انقر فوق زر عرض الوصفات للانتقال إلى صفحة الوصفات. في صفحة الوصفات، قم بالوصول إلى أي وصفة عن طريق النقر فوق زر عرض الوصفة. ستُستقبل بصفحة ممتلئة بالبيانات من قاعدة البيانات الخاصة بك:

يمكنك إيقاف الخادم باستخدام CTRL+C.

في هذه الخطوة، قمت بإضافة تسع وصفات إلى قاعدة بياناتك وأنشأت مكونات لعرض هذه الوصفات، سواء بشكل فردي أو كمجموعة. في الخطوة التالية، ستقوم بإضافة مكون لإنشاء الوصفات.

الخطوة 8 — إنشاء الوصفات

الخطوة التالية للحصول على تطبيق وصفات الطعام القابل للاستخدام هي القدرة على إنشاء وصفات جديدة. في هذه الخطوة، ستقوم بإنشاء مكون لهذه الميزة. سيحتوي المكون على نموذج لجمع تفاصيل الوصفة المطلوبة من المستخدم ثم إرسال طلب إلى إجراء create في تحكم Recipe لحفظ بيانات الوصفة.

أنشئ ملف NewRecipe.jsx في الدليل app/javascript/components:

  1. nano app/javascript/components/NewRecipe.jsx

في الملف الجديد، قم بإستيراد وحدات React، useState، Link، و useNavigate التي استخدمتها في مكونات أخرى:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

ثم، أنشئ وصدر مكون NewRecipe وظيفيًا عن طريق إضافة الأسطر المظللة:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");
};

export default NewRecipe;

كما هو الحال مع المكونات السابقة، يتم تهيئة توجيه مسار React بواسطة خطاف useNavigate، ثم يتم استخدام خطاف useState لتهيئة حالة name، ingredients، و instruction، مع وظائف التحديث الخاصة بها على التوالي. هذه هي الحقول التي ستحتاج إليها لإنشاء وصفة صالحة.

بعد ذلك، قم بإنشاء وظيفة stripHtmlEntities التي ستحول الأحرف الخاصة (مثل <) إلى قيمها المهربة/المشفرة (مثل &lt;) على التوالي. للقيام بذلك، أضف السطور المظللة إلى مكون NewRecipe:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");

  const stripHtmlEntities = (str) => {
    return String(str)
      .replace(/\n/g, "<br> <br>")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  };
};

export default NewRecipe;

في وظيفة stripHtmlEntities، قم بتبديل الأحرف < و > بقيمها المهربة. بهذه الطريقة، لن تقوم بتخزين HTML الخام في قاعدة البيانات الخاصة بك.

بعد ذلك، أضف السطور المظللة لإضافة وظائف onChange و onSubmit إلى مكون NewRecipe للتعامل مع تحرير وتقديم النموذج:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");

  const stripHtmlEntities = (str) => {
    return String(str)
      .replace(/\n/g, "<br> <br>")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  };

  const onChange = (event, setFunction) => {
    setFunction(event.target.value);
  };

  const onSubmit = (event) => {
    event.preventDefault();
    const url = "/api/v1/recipes/create";

    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
      return;

    const body = {
      name,
      ingredients,
      instruction: stripHtmlEntities(instruction),
    };

    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "POST",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => navigate(`/recipe/${response.id}`))
      .catch((error) => console.log(error.message));
  };
};

export default NewRecipe;

تقوم وظيفة onChange بقبول إدخال المستخدم event ووظيفة تعيين الحالة، ثم تحدث الحالة بقيمة إدخال المستخدم. في وظيفة onSubmit، يتم التحقق من عدم وجود أي إدخالات مطلوبة فارغة. ثم يتم بناء كائن يحتوي على المعاملات المطلوبة لإنشاء وصفة جديدة. باستخدام وظيفة stripHtmlEntities، يتم استبدال الأحرف < و > في تعليمات الوصفة بقيمتها المهربة واستبدال كل حرف جديد بوسم فاصل، مما يحافظ على تنسيق النص الذي أدخله المستخدم. أخيرًا، يتم إجراء طلب POST HTTP لإنشاء وصفة جديدة وإعادة التوجيه إلى صفحتها في حالة استجابة ناجحة.

لحماية ضد هجمات Cross-Site Request Forgery (CSRF)، يقوم Rails بإرفاق رمز أمان CSRF بمستند HTML. يُطلب هذا الرمز في كل مرة يتم فيها إجراء طلب غير GET. باستخدام الثابت token في الشيفرة السابقة، يتحقق التطبيق الخاص بك من الرمز على الخادم ويُلقي استثناءً إذا لم يتطابق الرمز الأماني مع المتوقع. في وظيفة onSubmit، يسترد التطبيق الرمز CSRF المضمن في مستند HTML الخاص بك بواسطة Rails ومن ثم يقوم بإجراء طلب HTTP بسلسلة JSON. إذا تم إنشاء الوصفة بنجاح، يقوم التطبيق بإعادة توجيه المستخدم إلى صفحة الوصفة حيث يمكنه مشاهدة الوصفة التي تم إنشاؤها حديثًا.

أخيرًا، قم بإرجاع العلامة التي تقوم بتقديم نموذج للمستخدم لإدخال تفاصيل الوصفة التي يرغب المستخدم في إنشائها. أضف السطور المظللة:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");

  const stripHtmlEntities = (str) => {
    return String(str)
      .replace(/\n/g, "<br> <br>")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  };

  const onChange = (event, setFunction) => {
    setFunction(event.target.value);
  };

  const onSubmit = (event) => {
    event.preventDefault();
    const url = "/api/v1/recipes/create";

    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
      return;

    const body = {
      name,
      ingredients,
      instruction: stripHtmlEntities(instruction),
    };

    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "POST",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => navigate(`/recipe/${response.id}`))
      .catch((error) => console.log(error.message));
  };

  return (
    <div className="container mt-5">
      <div className="row">
        <div className="col-sm-12 col-lg-6 offset-lg-3">
          <h1 className="font-weight-normal mb-5">
            Add a new recipe to our awesome recipe collection.
          </h1>
          <form onSubmit={onSubmit}>
            <div className="form-group">
              <label htmlFor="recipeName">Recipe name</label>
              <input
                type="text"
                name="name"
                id="recipeName"
                className="form-control"
                required
                onChange={(event) => onChange(event, setName)}
              />
            </div>
            <div className="form-group">
              <label htmlFor="recipeIngredients">Ingredients</label>
              <input
                type="text"
                name="ingredients"
                id="recipeIngredients"
                className="form-control"
                required
                onChange={(event) => onChange(event, setIngredients)}
              />
              <small id="ingredientsHelp" className="form-text text-muted">
                Separate each ingredient with a comma.
              </small>
            </div>
            <label htmlFor="instruction">Preparation Instructions</label>
            <textarea
              className="form-control"
              id="instruction"
              name="instruction"
              rows="5"
              required
              onChange={(event) => onChange(event, setInstruction)}
            />
            <button type="submit" className="btn custom-button mt-3">
              Create Recipe
            </button>
            <Link to="/recipes" className="btn btn-link mt-3">
              Back to recipes
            </Link>
          </form>
        </div>
      </div>
    </div>
  );
};

export default NewRecipe;

العلامة المُرجَعِيّة المُعادة تتضمن نموذج يحتوي على ثلاث حقول إدخال؛ واحدة لكلٍ من recipeName، recipeIngredients، و instruction. كل حقل إدخال له معالِج حدث onChange يُستدعى الدالة onChange. كما يتم تعليق معالِج حدث onSubmit على زر الإرسال ويُستدعى الدالة onSubmit التي تُرسل بيانات النموذج.

احفظ وأغلق الملف.

للوصول إلى هذا المكون في المتصفح، قم بتحديث ملف التوجيه الخاص بك بمساره:

  1. nano app/javascript/routes/index.jsx

قم بتحديث ملف التوجيه الخاص بك لتضمين هذه الأسطر المظللة:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" exact component={Recipe} />
      <Route path="/recipe" element={<NewRecipe />} />
    </Routes>
  </Router>
);

مع وجود المسار، احفظ وأغلق ملفك.

أعد تشغيل خادم التطوير الخاص بك واذهب إلى http://localhost:3000 في متصفحك. انتقل إلى صفحة الوصفات وانقر فوق زر إنشاء وصفة جديدة. ستجد صفحة تحتوي على نموذج لإضافة الوصفات إلى قاعدة البيانات الخاصة بك:

أدخل تفاصيل الوصفة المطلوبة وانقر فوق زر إنشاء وصفة. ستظهر الوصفة التي تم إنشاؤها حديثًا على الصفحة. عند الانتهاء، أغلق الخادم.

في هذه الخطوة، قمت بإضافة القدرة على إنشاء وصفات إلى تطبيق وصفات الطعام الخاص بك. في الخطوة التالية، ستقوم بإضافة الوظيفة لحذف الوصفات.

الخطوة 9 — حذف الوصفات

في هذا القسم، ستقوم بتعديل مكون الوصفة لتضمين خيار لحذف الوصفات. عند النقر فوق زر الحذف على صفحة الوصفة، ستقوم التطبيق بإرسال طلب لحذف وصفة من قاعدة البيانات.

أولاً، افتح ملف Recipe.jsx الخاص بك للتعديل:

  1. nano app/javascript/components/Recipe.jsx

في مكون Recipe، أضف وظيفة deleteRecipe مع الأسطر المميزة:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

  const deleteRecipe = () => {
    const url = `/api/v1/destroy/${params.id}`;
    const token = document.querySelector('meta[name="csrf-token"]').content;

    fetch(url, {
      method: "DELETE",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(() => navigate("/recipes"))
      .catch((error) => console.log(error.message));
  };

  const ingredientList = () => {
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

  return (
    <div className="">
...

في وظيفة deleteRecipe، تحصل على id للوصفة التي سيتم حذفها، ثم قم ببناء عنوان URL الخاص بك واحصل على رمز CSRF. بعد ذلك، قم بإرسال طلب DELETE إلى وحدة التحكم Recipes لحذف الوصفة. يقوم التطبيق بتوجيه المستخدم إلى صفحة الوصفات إذا تم حذف الوصفة بنجاح.

لتشغيل الكود في وظيفة deleteRecipe كلما تم النقر على زر الحذف، قم بتمريره كمعالج حدث النقر إلى الزر. أضف حدث onClick إلى عنصر زر الحذف في المكون:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
...
return (
    <div className="">
      <div className="hero position-relative d-flex align-items-center justify-content-center">
        <img
          src={recipe.image}
          alt={`${recipe.name} image`}
          className="img-fluid position-absolute"
        />
        <div className="overlay bg-dark position-absolute" />
        <h1 className="display-4 position-relative text-white">
          {recipe.name}
        </h1>
      </div>
      <div className="container py-5">
        <div className="row">
          <div className="col-sm-12 col-lg-3">
            <ul className="list-group">
              <h5 className="mb-2">Ingredients</h5>
              {ingredientList()}
            </ul>
          </div>
          <div className="col-sm-12 col-lg-7">
            <h5 className="mb-2">Preparation Instructions</h5>
            <div
              dangerouslySetInnerHTML={{
                __html: `${recipeInstruction}`,
              }}
            />
          </div>
          <div className="col-sm-12 col-lg-2">
            <button
              type="button"
              className="btn btn-danger"
              onClick={deleteRecipe}
            >
              Delete Recipe
            </button>
          </div>
        </div>
        <Link to="/recipes" className="btn btn-link">
          Back to recipes
        </Link>
      </div>
    </div>
  );
...

في هذه النقطة في البرنامج التعليمي، يجب أن يتطابق ملف Recipe.jsx الخاص بك مع هذا الملف:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

  const deleteRecipe = () => {
    const url = `/api/v1/destroy/${params.id}`;
    const token = document.querySelector('meta[name="csrf-token"]').content;

    fetch(url, {
      method: "DELETE",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(() => navigate("/recipes"))
      .catch((error) => console.log(error.message));
  };

  const ingredientList = () => {
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

  return (
    <div className="">
      <div className="hero position-relative d-flex align-items-center justify-content-center">
        <img
          src={recipe.image}
          alt={`${recipe.name} image`}
          className="img-fluid position-absolute"
        />
        <div className="overlay bg-dark position-absolute" />
        <h1 className="display-4 position-relative text-white">
          {recipe.name}
        </h1>
      </div>
      <div className="container py-5">
        <div className="row">
          <div className="col-sm-12 col-lg-3">
            <ul className="list-group">
              <h5 className="mb-2">Ingredients</h5>
              {ingredientList()}
            </ul>
          </div>
          <div className="col-sm-12 col-lg-7">
            <h5 className="mb-2">Preparation Instructions</h5>
            <div
              dangerouslySetInnerHTML={{
                __html: `${recipeInstruction}`,
              }}
            />
          </div>
          <div className="col-sm-12 col-lg-2">
            <button
              type="button"
              className="btn btn-danger"
              onClick={deleteRecipe}
            >
              Delete Recipe
            </button>
          </div>
        </div>
        <Link to="/recipes" className="btn btn-link">
          Back to recipes
        </Link>
      </div>
    </div>
  );
};

export default Recipe;

احفظ وأغلق الملف.

أعد تشغيل خادم التطبيق وانتقل إلى الصفحة الرئيسية. انقر فوق زر عرض الوصفات للوصول إلى جميع الوصفات الموجودة، ثم افتح أي وصفة معينة وانقر فوق زر حذف الوصفة على الصفحة لحذف المقالة. سيتم توجيهك إلى صفحة الوصفات، ولن تكون الوصفة المحذوفة موجودة بعد الآن.

بعد عمل الزر للحذف، لديك الآن تطبيق وصفة متكامل بشكل كامل!

الختام

في هذا الدرس، قمت بإنشاء تطبيق لوصفات الطعام باستخدام Ruby on Rails وواجهة مستخدم React، باستخدام PostgreSQL كقاعدة بيانات و Bootstrap للتنسيق. إذا كنت ترغب في مواصلة البناء باستخدام Ruby on Rails، فكر في متابعة دروسنا تأمين الاتصالات في تطبيق Rails ثلاثي المستويات باستخدام أنفاق SSH أو زيارة سلسلة كيفية البرمجة بلغة Ruby لتحديث مهاراتك في Ruby. للغوص بعمق في React، جرب كيفية عرض البيانات من API DigitalOcean باستخدام React.

Source:
https://www.digitalocean.com/community/tutorials/how-to-set-up-a-ruby-on-rails-v7-project-with-a-react-frontend-on-ubuntu-20-04