دليل شامل: بناء تطبيق أفلام تفاعلي باستخدام React وواجهة OMDB API

دقائق القراءة: 20

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

ماذا سنتعلم ونبني في هذا المشروع؟

  • استخدام واجهة API حقيقية للبحث عن الأفلام بشكل ديناميكي أثناء الكتابة.
  • إنشاء تأثير تمرير أفقي شبيه بـ Netflix لعرض قوائم الأفلام.
  • إضافة الأفلام إلى قائمة المفضلة وإزالتها منها.
  • حفظ قائمة المفضلة في الذاكرة المحلية للمتصفح (Local Storage) لضمان بقائها حتى بعد تحديث التطبيق.

إليك نظرة سريعة على ما سنبنيه:

معاينة لتطبيق الأفلام النهائي مع ميزات البحث والتمرير الأفقي والمفضلة.

إذا كنت تفضل المتابعة عبر الفيديو، يتوفر شرح مصور للمشروع. وفي حال واجهت أي صعوبة، يمكنك دائمًا الحصول على الكود النهائي للمشروع على GitHub.

الحصول على مفتاح API من OMDB

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

خطوات الحصول على مفتاح API

  1. توجه إلى صفحة http://www.omdbapi.com/apikey.aspx.
  2. املأ النموذج المطلوب بالمعلومات اللازمة.
  3. ستتلقى رسالة بريد إلكتروني تحتوي على مفتاح API الخاص بك ورابط التفعيل، كما هو موضح في الصورة:

مثال لرسالة البريد الإلكتروني التي تحتوي على مفتاح OMDB API ورابط التفعيل.

انقر على رابط التفعيل (المُظلل باللون الأخضر) وسيصبح مفتاحك جاهزًا للاستخدام! تهانينا!

استكشاف واجهة API باستخدام Postman

هذه الخطوة اختيارية، لذا إذا كنت متحمسًا للانتقال مباشرة إلى برمجة React، فلا تتردد في تخطي هذا القسم. سنستخدم أداة Postman (يمكنك تنزيلها من هنا إذا لم تكن لديك) لتجربة واجهة API.

  1. شغل Postman والصق عنوان URL الخاص بـ OMDB API الذي تلقيته في بريدك الإلكتروني (المُظلل باللون الأصفر في الصورة أعلاه).
  2. انقر على زر "Send"، وستحصل على استجابة بصيغة JSON في قسم "body"، كما يلي:

استجابة JSON من OMDB API عند استدعاء بسيط باستخدام Postman.

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

البحث عن الأفلام حسب العنوان

حتى الآن، استخدمنا واجهة API لجلب فيلم واحد، لكن ما نريده هو البحث عن "Titles containing a search term" (عناوين تحتوي على مصطلح بحث). للقيام بذلك، سنغير معامل الاستعلام i إلى s، كما يلي:

تغيير معامل الاستعلام من 'i' إلى 's' في Postman للبحث عن الأفلام.

سيؤدي هذا إلى استعلام واجهة API عن جميع الأفلام التي تحتوي على "star wars" في عنوانها. انقر على "Send" مرة أخرى، وهذه المرة ستلاحظ أن الاستجابة مختلفة:

استجابة OMDB API التي تحتوي على مصفوفة من نتائج البحث عن الأفلام.

لاحظ كيف نتلقى مصفوفة من العناصر (array of items). يحتوي كل عنصر في المصفوفة على بعض التفاصيل حول الفيلم (العنوان، السنة، وما إلى ذلك). سنأخذ صورة الملصق (Poster) لكل كائن ونعرضها في تطبيقنا.

إعداد مشروع React جديد

بعد أن انتهينا من استكشاف واجهة API، يمكننا الآن الانتقال إلى الجزء الممتع – إنشاء مشروع React. سنستخدم أداة create-react-app لإنشاء المشروع بسرعة.

إنشاء مشروع React

افتح الطرفية (terminal) واكتب الأمر التالي:

npx create-react-app movie-app

بعد انتهاء العملية، سنضيف مكتبة Bootstrap لمساعدتنا في تنظيم العناصر بشكل جيد دون الحاجة إلى الكثير من شيفرة CSS الخاصة بنا.

تثبيت Bootstrap

قم بتشغيل الأوامر التالية في الطرفية:

cd movie-app
npm install bootstrap

تشغيل التطبيق

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

npm start

إدارة حالة الأفلام في React

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

تعديل مكون App.js

افتح ملف App.js، احذف كل ما فيه، واستبدله بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';

const App = () => {
  const [movies, setMovies] = useState([
    {
      "Title": "Star Wars: Episode IV - A New Hope",
      "Year": "1977",
      "imdbID": "tt0076759",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BNzVlY2MwMjktM2E4OS00Y2Y3LWE3ZjctYzhkZGM3YzA1ZWM2XkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SX300.jpg"
    },
    {
      "Title": "Star Wars: Episode V - The Empire Strikes Back",
      "Year": "1980",
      "imdbID": "tt0080684",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BYmU1NDRjNDgtMzhiMi00NjZmLTg5NGItZDNiZjU5NTU4OTE0XkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SX300.jpg"
    },
    {
      "Title": "Star Wars: Episode VI - Return of the Jedi",
      "Year": "1983",
      "imdbID": "tt0086190",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BOWZlMjFiYzgtMTUzNC00Y2IzLTk1NTMtZmNhMTczNTk0ODk1XkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg"
    }
  ]);

  return (
    
); }; export default App;

في هذه الخطوة، نقوم بإنشاء كائن حالة (state object) للاحتفاظ بقائمة الأفلام. ستأتي هذه القائمة من واجهة API لاحقًا، لكن في الوقت الحالي، نقوم بتضمين بعض البيانات يدويًا – وهي مأخوذة من الاستجابة التي حصلنا عليها في Postman. كما قمنا بإضافة CSS من Bootstrap وبعض التنسيقات الأساسية.

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

إنشاء مكون MovieList لعرض الأفلام

مرحباً بأول مكون لنا! سنقوم بإنشاء مكون MovieList لعرض قائمة الأفلام التي تعود في طلب البحث.

  1. أنشئ مجلدًا جديدًا باسم components داخل مجلد src.
  2. أنشئ ملفًا جديدًا داخل مجلد components باسم MovieList.js.

هيكل المجلدات الذي يوضح مكان ملف MovieList.js داخل مجلد components.

افتح ملف MovieList.js وأضف ما يلي:

import React from 'react';

const MovieList = (props) => {
  return (
    <>
      {props.movies.map((movie, index) => (
        
movie
))} > ); }; export default MovieList;

في هذا المكون:

  • سنقوم بتمرير قائمة من الأفلام كـ props.
  • سنستخدم دالة map للتكرار على هذه الأفلام.
  • لكل فيلم، سنعرض صورة باستخدام عنوان URL الخاص بـ Poster كمصدر للصورة.

عرض مكون MovieList في التطبيق

خطوة واحدة أخرى قبل أن نرى الأشياء تعمل في المتصفح – هل أنت متحمس؟ ارجع إلى ملف App.js وقم بتحديثه بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';

const App = () => {
  const [movies, setMovies] = useState([
    {
      Title: 'Star Wars: Episode IV - A New Hope',
      Year: '1977',
      imdbID: 'tt0076759',
      Type: 'movie',
      Poster: 'https://m.media-amazon.com/images/M/MV5BNzVlY2MwMjktM2E4OS00Y2Y3LWE3ZjctYzhkZGM3YzA1ZWM2XkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SX300.jpg',
    },
    {
      Title: 'Star Wars: Episode V - The Empire Strikes Back',
      Year: '1980',
      imdbID: 'tt0080684',
      Type: 'movie',
      Poster: 'https://m.media-amazon.com/images/M/MV5BYmU1NDRjNDgtMzhiMi00NjZmLTg5NGItZDNiZjU5NTU4OTE0XkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SX300.jpg',
    },
    {
      Title: 'Star Wars: Episode VI - Return of the Jedi',
      Year: '1983',
      imdbID: 'tt0086190',
      Type: 'movie',
      Poster: 'https://m.media-amazon.com/images/M/MV5BOWZlMjFiYzgtMTUzNC00Y2IzLTk1NTMtZmNhMTczNTk0ODk1XkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg',
    },
  ]);

  return (
    
); }; export default App;

في هذا التحديث:

  • نقوم باستيراد مكون MovieList (السطر 4 في محرر الأكواد الخاص بك).
  • نقوم بعرض MovieList وتمرير الأفلام التي قمنا بتخزينها في الحالة (state) كـ props (السطر 37 في محرر الأكواد الخاص بك).

احفظ وشغل التطبيق، ثم إذا ذهبت إلى المتصفح، يجب أن ترى ما يلي:

شاشة المتصفح تعرض ملصقات الأفلام المضافة إلى حالة التطبيق.

هذه الأفلام مأخوذة من كائن حالة الأفلام (movies state object) في App.js. لدينا الآن بعض ملصقات الأفلام. رائع!

دمج استدعاء واجهة API لجلب الأفلام

الآن بعد أن عرفنا أن تطبيقنا قادر على عرض الأفلام التي نتلقاها من واجهة API، يمكننا إضافة منطق لتقديم طلب لجلب الأفلام وعرضها على الشاشة. قم بتحديث ملف App.js بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';

const App = () => {
  const [movies, setMovies] = useState([]);

  const getMovieRequest = async () => {
    const url = `http://www.omdbapi.com/?s=star wars&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  useEffect(() => {
    getMovieRequest();
  }, []);

  return (
    
); }; export default App;

في هذا التحديث الهام:

  • نقوم بإزالة الأفلام المضمنة يدويًا من قيمة حالة الأفلام لدينا (السطر 7 في محرر الأكواد الخاص بك).
  • أضفنا دالة تسمى getMovieRequest تستدعي واجهة API. تستخدم هذه الدالة واجهة Fetch API (السطر 9 في محرر الأكواد الخاص بك).
  • نقوم بتضمين قيمة بحث ثابتة في الوقت الحالي – لاحقًا سنضيف حقل إدخال يمكن للمستخدم كتابة قيمة البحث فيه (السطر 10 في محرر الأكواد الخاص بك).
  • إذا حصلنا على أي أفلام في البحث، فسنقوم بتعيينها لحالة الأفلام لدينا.
  • نحن نستخدم خطاف useEffect لضمان أن استدعاء واجهة API يحدث فقط عند تحميل التطبيق لأول مرة (السطر 20 في محرر الأكواد الخاص بك).

نظرًا لأننا نمرر بالفعل قيمة حالة movies إلى مكون MovieList كـ props، فإن هذا يعمل تلقائيًا دون الحاجة إلى تغيير JSX الخاص بنا. كم هو رائع!

تطبيق التمرير الأفقي والخلفية الداكنة

بينما نحن هنا، سنضيف تأثير التمرير الأفقي الأنيق وخلفية داكنة – على غرار Netflix. اذهب إلى ملف App.css واحذف كل ما فيه. ثم أضف ما يلي:

body {
  background: #141414;
  color: #ffffff;
}

.movie-app > .row {
  overflow-x: auto;
  flex-wrap: nowrap;
}

هذا كل شيء! حاول التمرير أفقيًا عبر الأفلام في المتصفح.

إضافة عنوان ومربع بحث تفاعلي

حتى الآن، كنا نستخدم قيم بحث ثابتة، ولكن هذا قد لا يرضي غير محبي أفلام Star Wars. بعد ذلك، سنضيف عنوانًا ومربع بحث يسمح للمستخدم بالبحث عن أي شيء يريده.

إضافة مكون العنوان MovieListHeading

أضف مكونًا جديدًا إلى مجلد components باسم MovieListHeading.js. افتحه وأضف ما يلي:

import React from 'react';

const MovieListHeading = (props) => {
  return (
    

{props.heading}

); }; export default MovieListHeading;

يقبل هذا المكون خاصية heading التي يتم عرضها داخل عمود Bootstrap. هذا يسمح لنا بإعادة استخدام هذا المكون لاحقًا.

إضافة مكون مربع البحث SearchBox

أضف مكونًا جديدًا إلى مجلد components باسم SearchBox.js. افتحه وأضف ما يلي:

import React from 'react';

const SearchBox = (props) => {
  return (
    
props.setSearchValue(event.target.value)} placeholder='Type to search...' >
); }; export default SearchBox;

يقوم هذا المكون بعرض حقل إدخال (input):

  • يأخذ قيمة من props.
  • عندما يكتب المستخدم، يستدعي دالة تقوم بتحديث القيمة. هذه الدالة أيضًا مأخوذة من props.

ربط مربع البحث بحالة التطبيق

الآن بعد أن أصبح لدينا مكونات جديدة، نحتاج إلى معرفة ما كتبه المستخدم حتى نتمكن من إرساله إلى واجهة API. قم بتحديث ملف App.js بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [searchValue, setSearchValue] = useState('');

  const getMovieRequest = async () => {
    const url = `http://www.omdbapi.com/?s=star wars&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  useEffect(() => {
    getMovieRequest();
  }, []);

  return (
    
); }; export default App;

في هذا التحديث:

  • نضيف قيمة حالة جديدة لتخزين ما يكتبه المستخدم (السطر 10 في محرر الأكواد الخاص بك).
  • نقوم باستيراد مكوناتنا الجديدة (الأسطر 5/6 في محرر الأكواد الخاص بك).
  • نضيف صفًا جديدًا (row) يحتوي على مكوني MovieListHeading و SearchBox (السطر 29 في محرر الأكواد الخاص بك).
  • ونقوم بتمرير قيمة searchValue ودالة setSearchValue إلى مكون SearchBox (السطر 31).

من خلال حفظ حالة الإدخال في App.js، يصبح من السهل علينا تمرير قيمة البحث إلى دالة getMovieRequest.

تحديث نتائج البحث ديناميكيًا مع تغير المدخلات

الآن بعد أن عرفنا ما كتبه المستخدم، نحتاج إلى استدعاء واجهة API بهذه القيمة. قم بتحديث ملف App.js بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [searchValue, setSearchValue] = useState('');

  const getMovieRequest = async (searchValue) => {
    const url = `http://www.omdbapi.com/?s=${searchValue}&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  useEffect(() => {
    getMovieRequest(searchValue);
  }, [searchValue]);

  return (
    
); }; export default App;

في هذا التعديل الجوهري:

  • نقوم بتحديث دالة getMovieRequest لقبول معامل: searchValue (السطر 12 في محرر الأكواد الخاص بك).
  • نمرر هذه القيمة إلى الطلب باستخدام قالب السلسلة النصية (template string) (السطر 13 في محرر الأكواد الخاص بك).
  • نقوم بتحديث خطاف useEffect للتشغيل كلما تغيرت قيمة searchValue (السطر 25 في محرر الأكواد الخاص بك).
  • عندما يعمل خطاف useEffect، فإنه يمرر searchValue الجديدة إلى دالة getMovieRequest الخاصة بنا (السطر 24 في محرر الأكواد الخاص بك).

هذا يقوم باستدعاء واجهة API ويحدث الحالة إذا حصلنا على نتائج كالمعتاد. جرب هذا في المتصفح – وسترى النتائج تتحدث في الوقت الفعلي.

ميزة إضافة الأفلام إلى المفضلة

القدرة على البحث عن الأفلام أمر جميل، ولكن كيف سنتذكر مشاهدة كل هذه الأفلام الرائعة؟ بإضافتها إلى المفضلة بالطبع! سنضيف تأثير "zooming" لطيفًا يعرض زر "Add to Favourites" (إضافة إلى المفضلة) عندما يحوم المستخدم فوق الملصق، كما ترى أدناه. سنضيف أيضًا منطقًا لإضافة/عرض أي أفلام مفضلة يختارها المستخدم:

تأثير التكبير والتراكب الذي يظهر زر 'إضافة إلى المفضلة' عند تمرير الماوس فوق ملصق الفيلم.

إضافة التراكب (Overlay) وتأثير التكبير

سنبدأ بإضافة التراكب وتأثير التكبير. اذهب إلى ملف MovieList.js وقم بتحديثه بما يلي:

import React from 'react';

const MovieList = (props) => {
  return (
    <>
      {props.movies.map((movie, index) => (
        
movie
Add to Favourites
))} > ); }; export default MovieList;

في هذا التحديث:

  • نضيف فئة جديدة إلى العنصر الأب div: image-container. سيسمح لنا هذا بإضافة تأثير التكبير (السطر 7 في محرر الأكواد الخاص بك).
  • نضيف عنصر div جديدًا سيكون هو التراكب (overlay). سنخفي هذا العنصر في البداية ونظهره عندما يحوم المستخدم فوقه (السطر 9 في محرر الأكواد الخاص بك).
  • نضيف بعض النصوص (السطر 10 في محرر الأكواد الخاص بك).

الآن إذا قمت بحفظ وتشغيل هذا، فلن يحدث شيء. نحتاج إلى إضافة بعض شيفرة CSS لحدوث السحر. اذهب إلى ملف App.css وأضف ما يلي إلى الملف:

.image-container {
  position: relative;
  transition: transform 0.2s;
}

.image-container:hover {
  cursor: pointer;
  transform: scale(1.1);
}

.image-container:hover .overlay {
  opacity: 1;
}

.overlay {
  position: absolute;
  background: rgba(0, 0, 0, 0.8);
  width: 100%;
  transition: 0.5s ease;
  opacity: 0;
  bottom: 0;
  font-size: 20px;
  padding: 20px;
  text-align: center;
}

في هذه الأنماط:

  • نضيف تأثير انتقال (transition effect) إلى حاوية الصورة لتكبيرها (scale) عندما يحوم المستخدم فوقها (مما يمنحنا تأثير التكبير).
  • نضيف بعض الأنماط إلى التراكب الذي يكون مخفيًا في البداية.
  • نزيد الشفافية (أي نظهر التراكب) عندما يحوم المستخدم فوقه.

إذا قمت بتشغيل هذا في المتصفح، يمكنك رؤية الصورة "تتكبر" ويظهر التراكب عندما تحوم فوقها. أنيق!

إنشاء مكون “إضافة إلى المفضلة” (AddToFavourites)

بعد ذلك، سنقوم بإنشاء مكون "Add to Favourites" الذي نمرره إلى MovieList والذي سنقوم بعد ذلك بعرضه في التراكب. أنشئ ملفًا جديدًا في مجلد components باسم AddToFavourites.js. أضف ما يلي:

import React from 'react';

const AddFavourite = () => {
  return (
    <>
      Add to Favourites
      
        
      
    >
  );
};

export default AddFavourite;

سيعرض هذا المكون النص "Add to Favourites" وأيقونة "Heart" (مأخوذة من www.icons.getbootstrap.com). بعد ذلك، سنقوم باستيراد هذا المكون إلى App.js وتمريره إلى مكون MovieList الخاص بنا. قم بتحديث App.js بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';
import AddFavourites from './components/AddFavourites';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [searchValue, setSearchValue] = useState('');

  const getMovieRequest = async (searchValue) => {
    const url = `http://www.omdbapi.com/?s=${searchValue}&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  useEffect(() => {
    getMovieRequest(searchValue);
  }, [searchValue]);

  return (
    
); }; export default App;

في هذا التحديث:

  • نقوم باستيراد مكون AddFavourites الجديد (السطر 7 في محرر الأكواد الخاص بك).
  • نمرر هذا المكون كخاصية (favouriteComponent) إلى مكون MovieList الخاص بنا (السطر 35).

الآن بعد أن أصبح مكون MovieList الخاص بنا يقبل هذا المكون كخاصية، يمكننا عرضه في التراكب لدينا. تذكر أن مكونات React هي مجرد دوال – لذا يمكننا تمريرها كما نفعل مع الدوال العادية! افتح ملف MovieList.js وقم بتحديثه بما يلي:

import React from 'react';

const MovieList = (props) => {
  const FavouriteComponent = props.favouriteComponent;

  return (
    <>
      {props.movies.map((movie, index) => (
        
movie
))} > ); }; export default MovieList;

في هذا التعديل:

  • نأخذ favouriteComponent من props ونقوم بتعيينه لمتغير. هذا يسمح لنا باستخدامه كمكون React (السطر 4 في محرر الأكواد الخاص بك).
  • نقوم بعرض favouriteComponent الخاص بنا في التراكب (السطر 15 في محرر الأكواد الخاص بك).

يتيح لنا هذا النهج تخصيص ما يتم عرضه في التراكب. يمكننا تمرير أي مكون React وسيقوم مكون MovieList بعرضه. هذا يجعل مكون MovieList الخاص بنا قابلًا لإعادة الاستخدام.

حفظ الأفلام المفضلة في حالة التطبيق

لدينا الآن مكون المفضلة في مكانه، وعلى الرغم من أنه يبدو جميلًا، إلا أنه لا يفعل شيئًا بعد. عندما ينقر المستخدم على مكون "Add to Favourites"، نريد أن نأخذ الفيلم الذي نقر عليه ونحفظه في كائن حالة جديد يسمى favourites. ثم سنقوم بعرض هذه القائمة في واجهة المستخدم. افتح ملف App.js وقم بتحديثه بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';
import AddFavourites from './components/AddFavourites';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [searchValue, setSearchValue] = useState('');
  const [favourites, setFavourites] = useState([]);

  const getMovieRequest = async (searchValue) => {
    const url = `http://www.omdbapi.com/?s=${searchValue}&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  const addFavouriteMovie = (movie) => {
    const newFavouriteList = [...favourites, movie];
    setFavourites(newFavouriteList);
  };

  useEffect(() => {
    getMovieRequest(searchValue);
  }, [searchValue]);

  return (
    
); }; export default App;

في هذا التحديث:

  • نضيف كائن حالة جديدًا للاحتفاظ بقائمة المفضلة لدينا. سنضيف أي فيلم ينقر عليه المستخدم إلى هذه المصفوفة (السطر 12 في محرر الأكواد الخاص بك).
  • نقوم بإنشاء دالة تسمى addFavouriteMovie، والتي تقبل فيلمًا (movie). تأخذ هذه الدالة مصفوفة المفضلة الحالية، وتنسخها، وتضيف الفيلم الجديد إليها، وتحفظ كل شيء مرة أخرى في الحالة (السطر 25 في محرر الأكواد الخاص بك).
  • نمرر هذه الدالة كخاصية (handleFavouritesClick) إلى مكون MovieList الخاص بنا (السطر 44 في محرر الأكواد الخاص بك).

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

import React from 'react';

const MovieList = (props) => {
  const FavouriteComponent = props.favouriteComponent;

  return (
    <>
      {props.movies.map((movie, index) => (
        
movie
props.handleFavouritesClick(movie)} className='overlay d-flex align-items-center justify-content-center'>
))} > ); }; export default MovieList;

كل ما نقوم به هنا هو أخذ دالة handleFavouritesClick من props وإضافتها إلى خاصية onClick في التراكب. نمرر الفيلم الحالي الذي تتكرر عليه دالة map إلى دالة handleFavouritesClick. الآن إذا قمت بتشغيل التطبيق، انقر على "Add to Favourites" لأي فيلم، وافتح أدوات مطوري React (في Chrome) سترى أنه يتم تحديثه في الحالة. لسوء الحظ، لا يتم تسمية الخطافات بنفس ما أطلقنا عليها، ولكن يمكننا تخمين أي منها هو:

أدوات مطوري React تعرض تحديث حالة المفضلة بعد إضافة فيلم.

عرض قائمة الأفلام المفضلة

تبدو الأمور جيدة حتى الآن. لدينا القدرة على حفظ الأشياء في المفضلة، لكننا لم نعرضها بعد. سنقوم بإعادة استخدام مكون MovieList لعرض مفضلاتنا. كم هو أنيق! افتح ملف App.js وقم بتحديثه بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';
import AddFavourites from './components/AddFavourites';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [searchValue, setSearchValue] = useState('');
  const [favourites, setFavourites] = useState([]);

  const getMovieRequest = async (searchValue) => {
    const url = `http://www.omdbapi.com/?s=${searchValue}&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  const addFavouriteMovie = (movie) => {
    const newFavouriteList = [...favourites, movie];
    setFavourites(newFavouriteList);
  };

  useEffect(() => {
    getMovieRequest(searchValue);
  }, [searchValue]);

  return (
    
); }; export default App;

في هذا التحديث:

  • نضيف صفًا جديدًا وداخله، نضيف عنوانًا جديدًا باستخدام مكون MovieListHeading (السطر 47 في محرر الأكواد الخاص بك).
  • نضيف صفًا جديدًا أسفل ذلك، ونعرض مفضلاتنا باستخدام مكون MovieList (السطر 51).

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

تطبيق الأفلام يعرض قسمين: الأفلام و المفضلة، مع إمكانية إضافة الأفلام.

إزالة الأفلام من قائمة المفضلة

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

  • إنشاء مكون إزالة (remove component) نمرره إلى MovieList الخاص بنا، والذي يتم عرضه في التراكب.
  • إنشاء دالة تسمى clicked لإزالة الفيلم الذي تم النقر عليه من الحالة (state).
  • تمرير دالة للتعامل مع حدث onClick عندما ينقر المستخدم على مكون الإزالة.

إنشاء مكون “إزالة من المفضلة” (RemoveFavourites)

أنشئ ملفًا جديدًا في مجلد components باسم RemoveFavourites.js وأضف ما يلي:

import React from 'react';

const RemoveFavourites = () => {
  return (
    <>
      Remove from favourites
      
        
        
      
    >
  );
};

export default RemoveFavourites;

في هذا المكون:

  • نضيف النص "Remove from favourites" (السطر 6 في محرر الأكواد الخاص بك).
  • نضيف أيقونة "remove" التي نحصل عليها من icons.getbootstrap.com (السطر 7 في محرر الأكواد الخاص بك).

إزالة المفضلة من حالة التطبيق

على غرار ما فعلناه من قبل، نحتاج إلى كتابة دالة تزيل فيلمًا محددًا من حالة المفضلة لدينا. افتح ملف App.js وقم بتحديثه بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';
import AddFavourites from './components/AddFavourites';
import RemoveFavourites from './components/RemoveFavourites';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [searchValue, setSearchValue] = useState('');
  const [favourites, setFavourites] = useState([]);

  const getMovieRequest = async (searchValue) => {
    const url = `http://www.omdbapi.com/?s=${searchValue}&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  const addFavouriteMovie = (movie) => {
    const newFavouriteList = [...favourites, movie];
    setFavourites(newFavouriteList);
  };

  const removeFavouriteMovie = (movie) => {
    const newFavouriteList = favourites.filter(
      (favourite) => favourite.imdbID !== movie.imdbID
    );
    setFavourites(newFavouriteList);
  };

  useEffect(() => {
    getMovieRequest(searchValue);
  }, [searchValue]);

  return (
    
); }; export default App;

في هذا التحديث:

  • نقوم باستيراد مكون RemoveFavourites الخاص بنا (السطر 8 في محرر الأكواد الخاص بك).
  • نقوم بإنشاء دالة تسمى removeFavouriteMovie لإزالة فيلم معين من حالة المفضلة لدينا (السطر 31 في محرر الأكواد الخاص بك).
  • لإزالة الفيلم، نستخدم دالة filter. تعيد هذه الدالة نسخة جديدة من مصفوفة المفضلة لا تتضمن الفيلم الذي نرغب في إزالته.
  • نمرر مكون RemoveFavourites الخاص بنا ودالة removeFavouriteMovie إلى مكون MovieList الخاص بنا (السطر 60 في محرر الأكواد الخاص بك).

نظرًا لأننا قمنا بالفعل بترميز مكون MovieList الخاص بنا لقبول مكون للعرض ودالة للاستدعاء، فإن كل شيء يعمل بسلاسة! الآن إذا قمت بتشغيل هذا في متصفحك، يجب أن تكون قادرًا على إزالة المفضلة:

تطبيق الأفلام يوضح كيفية إزالة فيلم من قائمة المفضلة.

تخزين وإدارة المفضلة في الذاكرة المحلية (Local Storage)

آخر شيء سنقوم به هو الحفظ في الذاكرة المحلية (local storage). يتيح لنا هذا الاحتفاظ بأفلامنا المفضلة عند إعادة تحميل الصفحة أو إذا فتحنا التطبيق في نافذة مختلفة. سنحفظ مفضلاتنا في الذاكرة المحلية عندما يتغير شيء ما، وسنسترجع مفضلاتنا من الذاكرة المحلية عند تحميل التطبيق، ونحفظ هذا في حالة المفضلة لدينا. افتح ملف App.js للمرة الأخيرة وقم بتحديثه بما يلي:

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import MovieList from './components/MovieList';
import MovieListHeading from './components/MovieListHeading';
import SearchBox from './components/SearchBox';
import AddFavourites from './components/AddFavourites';
import RemoveFavourites from './components/RemoveFavourites';

const App = () => {
  const [movies, setMovies] = useState([]);
  const [favourites, setFavourites] = useState([]);
  const [searchValue, setSearchValue] = useState('');

  const getMovieRequest = async (searchValue) => {
    const url = `http://www.omdbapi.com/?s=${searchValue}&apikey=263d22d8`;
    const response = await fetch(url);
    const responseJson = await response.json();

    if (responseJson.Search) {
      setMovies(responseJson.Search);
    }
  };

  useEffect(() => {
    getMovieRequest(searchValue);
  }, [searchValue]);

  useEffect(() => {
    const movieFavourites = JSON.parse(
      localStorage.getItem('react-movie-app-favourites')
    );

    if (movieFavourites) {
        setFavourites(movieFavourites);
    }
  }, []);

  const saveToLocalStorage = (items) => {
    localStorage.setItem('react-movie-app-favourites', JSON.stringify(items));
  };

  const addFavouriteMovie = (movie) => {
    const newFavouriteList = [...favourites, movie];
    setFavourites(newFavouriteList);
    saveToLocalStorage(newFavouriteList);
  };

  const removeFavouriteMovie = (movie) => {
    const newFavouriteList = favourites.filter(
      (favourite) => favourite.imdbID !== movie.imdbID
    );
    setFavourites(newFavouriteList);
    saveToLocalStorage(newFavouriteList);
  };

  return (
    
); }; export default App;

في هذا التحديث الأخير:

  • نضيف دالة تسمى saveToLocalStorage. تأخذ هذه الدالة قائمة من العناصر، وتحفظها في الذاكرة المحلية باستخدام مفتاح. في هذه الحالة، المفتاح هو react-movie-app-favourites. (السطر 38 في محرر الأكواد الخاص بك).
  • نقوم بالحفظ في الذاكرة المحلية عندما نضيف فيلمًا مفضلاً (السطر 45 في محرر الأكواد الخاص بك).
  • نقوم بالحفظ في الذاكرة المحلية عندما نزيل فيلمًا مفضلاً (السطر 54 في محرر الأكواد الخاص بك).
  • نستخدم خطاف useEffect لاسترداد المفضلة من الذاكرة المحلية عند تحميل التطبيق، ونقوم بتعيين هذا للحالة (السطر 30 في محرر الأكواد الخاص بك).

شغل هذا في متصفحك ويجب أن تكون قادرًا على حفظ الأفلام المفضلة – حتى لو أغلقت المتصفح!

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

الخلاصة التقنية

لقد قمنا في هذا الدليل ببناء تطبيق أفلام تفاعلي متكامل باستخدام React و OMDB API، مما يمثل مشروعًا ممتازًا لإثراء محفظة أعمال أي مطور ويب. استعرضنا مفاهيم أساسية في React مثل إدارة الحالة (useState)، استخدام التأثيرات الجانبية (useEffect)، تمرير الخصائص (props)، وإنشاء مكونات قابلة لإعادة الاستخدام. كما تعلمنا كيفية التفاعل مع واجهات API الخارجية لجلب البيانات ديناميكيًا، وتطبيق تأثيرات CSS متقدمة مثل التمرير الأفقي وتأثير التكبير. الأهم من ذلك، قمنا بدمج ميزة تخزين البيانات في الذاكرة المحلية للمتصفح (Local Storage) لضمان استمرارية قائمة المفضلة، مما يعزز تجربة المستخدم بشكل كبير. يبرز هذا المشروع قوة React في بناء تطبيقات ويب حديثة وفعالة، ويقدم أساسًا متينًا للمطورين الطموحين.

شكرًا لك على القراءة، والأهم من ذلك – تهانينا على وصولك إلى النهاية! إذا استمتعت بهذا المقال، فتأكد من زيارة reactbeginnerprojects.com. ستجد مجموعة من المشاريع المجانية التي يمكنك تجربتها لتعلم المهارات الأساسية التي ستحتاجها للحصول على وظيفة كمطور React وتعزيز محفظة أعمالك.

كريس بلاكلي

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *