إنشاء تطبيق ويب باستخدام React و Redux و Sanity.io

نشرت: 2022-03-10
ملخص سريع ↬ يعد Headless CMS طريقة قوية وسهلة لإدارة المحتوى والوصول إلى واجهة برمجة التطبيقات. بنيت على React ، Sanity.io هي أداة سلسة لإدارة المحتوى المرنة. يمكن استخدامه لبناء تطبيقات بسيطة إلى معقدة من الألف إلى الياء. في هذه المقالة ، يشرح Ifeanyi كيفية إنشاء تطبيق قائمة بسيط باستخدام Sanity.io و React. ستتم إدارة الدول العالمية باستخدام Redux وسيتم تصميم التطبيق بمكونات مصممة.

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

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

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

هناك الكثير من CMS مقطوعة الرأس ، وبعض أشهرها تشمل Strapi ، و Contentful ، و Contentstack ، و Sanity ، و Butter CMS ، و Prismic ، و Storyblok ، و Directus ، وما إلى ذلك. على سبيل المثال ، CMS مثل Sanity و Strapi و Contentful و Storyblok مجانية للمشاريع الصغيرة.

تعتمد أنظمة CMS مقطوعة الرأس هذه على مجموعات تقنية مختلفة أيضًا. بينما يعتمد Sanity.io على React.js ، يستند Storyblok إلى Vue.js. بصفتي مطورًا لـ React ، فإن هذا هو السبب الرئيسي الذي جعلني سريعًا في اختيار الاهتمام بـ Sanity. ومع ذلك ، نظرًا لكونها CMS مقطوعة الرأس ، يمكن توصيل كل من هذه الأنظمة الأساسية بأي واجهة أمامية ، سواء كانت Angular أو Vue أو React.

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

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

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

ملاحظة: تتطلب هذه المقالة فهمًا أساسيًا لكل من React و Redux و CSS.

المزيد بعد القفز! أكمل القراءة أدناه ↓

الشروع في العمل مع Sanity.io

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

للقيام بذلك ، أدخل الأوامر التالية في جهازك.

 npm install -g @sanity/cli

تتيح العلامة -g في الأمر أعلاه إمكانية التثبيت العام.

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

في مدونتها ، شرحت Kapehe بالتفصيل كيفية دمج Sanity مع React. سيكون من المفيد الاطلاع على المقالة قبل متابعة هذا البرنامج التعليمي.

أدخل الأوامر التالية لتهيئة Sanity في تطبيق React الخاص بك.

 sanity init

يصبح الأمر sanity متاحًا لنا عندما نقوم بتثبيت أداة Sanity CLI. يمكنك عرض قائمة بأوامر Sanity المتاحة عن طريق كتابة sanity sanity help في جهازك الطرفي.

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

لعرض مجموعة البيانات الخاصة بك وتحريرها ، اضغط على دليل cd الفرعي في الجهاز الطرفي وأدخل sanity start . يعمل هذا عادةً على https://localhost:3333/ . قد يُطلب منك تسجيل الدخول للوصول إلى الواجهة (تأكد من تسجيل الدخول بنفس الحساب الذي استخدمته عند بدء المشروع). يتم عرض لقطة شاشة للبيئة أدناه.

نظرة عامة على خادم Sanity
نظرة عامة على خادم العقل لمجموعة بيانات أفلام الخيال العلمي. (معاينة كبيرة)

التواصل ثنائي الاتجاه

يحتاج العقل وفاعلية إلى التواصل مع بعضهما البعض من أجل تطبيق يعمل بكامل طاقته.

إعداد أصول CORS في مدير الصحة

سنقوم أولاً بتوصيل تطبيق React الخاص بنا بـ Sanity. للقيام بذلك ، قم بتسجيل الدخول إلى https://manage.sanity.io/ وحدد موقع CORS origins ضمن API Settings في علامة التبويب " Settings ". هنا ، ستحتاج إلى ربط أصل الواجهة الأمامية بخلفية Sanity. يعمل تطبيق React الخاص بنا على https://localhost:3000/ افتراضيًا ، لذلك نحتاج إلى إضافة ذلك إلى CORS.

هذا هو مبين في الشكل أدناه.

إعدادات أصل CORS
تحديد أصل CORS في Sanity.io Manager. (معاينة كبيرة)

ربط العقل بالرد

يربط Sanity project ID مشروع بكل مشروع تقوم بإنشائه. هذا المعرف مطلوب عند توصيله بتطبيق الواجهة الأمامية الخاص بك. يمكنك العثور على معرّف المشروع في مدير Sanity الخاص بك.

تتواصل الواجهة الخلفية مع React باستخدام مكتبة تُعرف باسم sanity client . تحتاج إلى تثبيت هذه المكتبة في مشروع Sanity الخاص بك عن طريق إدخال الأوامر التالية.

 npm install @sanity/client

قم بإنشاء ملف sanitySetup.js (لا يهم اسم الملف) ، في مجلد src الخاص بالمشروع وأدخل رموز React التالية لإعداد اتصال بين Sanity و React.

 import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });

لقد مررنا useCdn المشروع dataset name المنطقي projectId إلى مثيل عميل العقل الذي تم استيراده من @sanity/client . يعمل هذا السحر ويربط تطبيقنا بالواجهة الخلفية.

الآن وقد أكملنا الاتصال ثنائي الاتجاه ، فلننتقل مباشرةً لبناء مشروعنا.

إعداد وتوصيل Redux بتطبيقنا

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

 npm install redux react-redux redux-thunk

Redux هي مكتبة إدارة حالة عالمية يمكن استخدامها مع معظم أطر عمل ومكتبات الواجهة الأمامية مثل React. ومع ذلك ، فنحن بحاجة إلى أداة وسيطة react-redux وإعادة الإرسال لتمكين الاتصال بين متجر Redux الخاص بنا وتطبيق React الخاص بنا. سيساعدنا Redux thunk على إرجاع دالة بدلاً من كائن عمل من Redux.

بينما يمكننا كتابة سير عمل Redux بالكامل في ملف واحد ، غالبًا ما يكون من الأفضل فصل مخاوفنا. لهذا ، سنقسم سير العمل لدينا إلى ثلاثة ملفات ، وهي actions ، reducers ، ثم store . ومع ذلك ، نحتاج أيضًا إلى ملف منفصل لتخزين action types ، والمعروفة أيضًا باسم constants .

انشاء المتجر

المخزن هو أهم ملف في Redux. يقوم بتنظيم وتعبئة الدول وشحنها إلى تطبيق React الخاص بنا.

هذا هو الإعداد الأولي لمتجر Redux الخاص بنا المطلوب لتوصيل سير عمل Redux الخاص بنا.

 import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );

تأخذ وظيفة createStore في هذا الملف ثلاث معاملات: reducer (مطلوب) ، والحالة الأولية والمحسن (عادةً ما يكون برنامج وسيط ، في هذه الحالة ، thunk الموفر من خلال applyMiddleware ). سيتم تخزين أدوات التخفيض الخاصة بنا في مجلد reducers وسنقوم بدمجها وتصديرها في ملف index.js في مجلد reducers . هذا هو الملف الذي قمنا باستيراده في الكود أعلاه. سنعود إلى هذا الملف لاحقًا.

مقدمة إلى لغة GROQ في Sanity

يأخذ Sanity الاستعلام عن بيانات JSON خطوة إلى الأمام من خلال تقديم GROQ. GROQ لتقف على استعلامات كائن العلاقة بالرسم البياني. وفقًا لـ Sanity.io ، GROQ هي لغة استعلام تعريفية مصممة للاستعلام عن مجموعات من مستندات JSON التي لا تحتوي على مخطط إلى حد كبير.

يوفر Sanity أيضًا ملعب GROQ لمساعدة المطورين على التعرف على اللغة. ومع ذلك ، للوصول إلى الملعب ، تحتاج إلى تثبيت رؤية العقل . قم بتشغيل sanity install @sanity/vision على جهازك الطرفي لتثبيته.

يحتوي GROQ على بناء جملة مماثل لـ GraphQL ولكنه أكثر تكثيفًا وأسهل في القراءة. علاوة على ذلك ، على عكس GraphQL ، يمكن استخدام GROQ للاستعلام عن بيانات JSON.

على سبيل المثال ، لاسترداد كل عنصر في مستند الفيلم الخاص بنا ، سنستخدم صيغة GROQ التالية.

 *[_type == "movie"]

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

 `*[_type == 'movie']{ _id, crewMembers }

هنا ، استخدمنا * لإخبار GROQ أننا نريد كل مستند من فيلم _type . _type هي سمة ضمن مجموعة الفيلم. يمكننا أيضًا إرجاع النوع كما فعلنا مع _id وأعضاء crewMembers على النحو التالي:

 *[_type == 'movie']{ _id, _type, crewMembers }

سنعمل أكثر على GROQ من خلال تنفيذه في إجراءات Redux الخاصة بنا ولكن يمكنك التحقق من وثائق Sanity.io لـ GROQ لمعرفة المزيد عنها. توفر ورقة الغش لاستعلام GROQ الكثير من الأمثلة لمساعدتك على إتقان لغة الاستعلام.

إنشاء الثوابت

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

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

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

 export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";

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

وبالمثل ، يمكننا إنشاء ثوابت أخرى لجلب أنواع الإجراءات للإشارة إلى نجاح الطلب أو فشله. يوجد رمز كامل لـ movieConstants.js في الكود أدناه.

 export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";

هنا قمنا بتعريف عدة ثوابت لجلب فيلم أو قائمة أفلام وفرز وجلب الأفلام الأكثر شهرة. لاحظ أننا قمنا بتعيين ثوابت لتحديد وقت loading الطلب successful failed .

وبالمثل ، يوجد ملف personConstants.js بنا أدناه:

 export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST"; export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS"; export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL"; export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST"; export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS"; export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL"; export const PERSONS_COUNT = "PERSONS_COUNT";

مثل movieConstants.js ، وضعنا قائمة بالثوابت لجلب شخص أو أشخاص. وضعنا أيضًا ثابتًا لعد الأشخاص. تتبع الثوابت الاتفاقية الموصوفة لـ movieConstants.js وقمنا أيضًا بتصديرها لتكون في متناول أجزاء أخرى من تطبيقنا.

أخيرًا ، سنقوم بتنفيذ الوضع الفاتح والظلام في التطبيق ، وبالتالي لدينا ملف ثوابت آخر globalConstants.js . دعونا نلقي نظرة عليه.

 export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";

هنا نضع ثوابت لتحديد وقت إرسال الوضع الفاتح أو المظلم. تحدد SET_LIGHT_THEME متى ينتقل المستخدم إلى سمة الضوء ويحدد SET_DARK_THEME متى يتم تحديد السمة الداكنة. قمنا أيضًا بتصدير ثوابتنا كما هو موضح.

إعداد الإجراءات

وفقًا للاتفاقية ، يتم تخزين إجراءاتنا في مجلد منفصل. يتم تجميع الإجراءات وفقًا لأنواعها. على سبيل المثال ، يتم تخزين إجراءات الفيلم الخاصة بنا في movieActions.js بينما يتم تخزين إجراءاتنا الشخصية في ملف personActions.js .

لدينا أيضًا globalActions.js السمة من الوضع الفاتح إلى الوضع المظلم.

لنجلب جميع الأفلام الموجودة في moviesActions.js .

 import sanityAPI from "../../sanitySetup"; import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster": poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } };

هل تتذكر عندما أنشأنا ملف sanitySetup.js لربط React بخلفية Sanity؟ هنا ، قمنا باستيراد الإعداد لتمكيننا من الاستعلام عن الخلفية الصحية الخاصة بنا باستخدام GROQ. قمنا أيضًا باستيراد بعض الثوابت المصدرة من ملف movieConstants.js في مجلد constants .

بعد ذلك ، أنشأنا وظيفة الحركة fetchAllMovies لجلب كل فيلم في مجموعتنا. تستخدم معظم تطبيقات axios التقليدية المحاور أو fetch لجلب البيانات من الواجهة الخلفية. ولكن بينما يمكننا استخدام أي منها هنا ، فإننا نستخدم GROQ من Sanity. للدخول إلى وضع GROQ ، نحتاج إلى استدعاء وظيفة sanityAPI.fetch() كما هو موضح في الكود أعلاه. هنا ، sanityAPI هو اتصال React-Sanity الذي أنشأناه سابقًا. يؤدي هذا إلى إرجاع Promise ولذا يجب استدعاؤه بشكل غير متزامن. لقد استخدمنا هنا بناء الجملة async-await ، ولكن يمكننا أيضًا استخدام .then الجملة.

نظرًا لأننا نستخدم thunk في تطبيقنا ، يمكننا إرجاع دالة بدلاً من كائن Action. ومع ذلك ، اخترنا تمرير بيان الإرجاع في سطر واحد.

 const fetchAllMovies = () => async (dispatch) => { ... }

لاحظ أنه يمكننا أيضًا كتابة الوظيفة بهذه الطريقة:

 const fetchAllMovies = () => { return async (dispatch)=>{ ... } }

بشكل عام ، لجلب جميع الأفلام ، أرسلنا أولاً نوع إجراء يتتبع وقت استمرار تحميل الطلب. ثم استخدمنا صيغة Sanity's GROQ للاستعلام عن مستند الفيلم بشكل غير متزامن. _id url الخاص ببيانات الفيلم. ثم أعدنا حمولة تحتوي على البيانات التي تم الحصول عليها من واجهة برمجة التطبيقات.

وبالمثل ، يمكننا استرداد الأفلام من خلال _id ، وفرز الأفلام ، والحصول على الأفلام الأكثر شهرة.

يمكننا أيضًا جلب الأفلام التي تطابق مرجع شخص معين. لقد فعلنا ذلك في دالة fetchMoviesByRef .

 const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title } ` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } };

تأخذ هذه الوظيفة وسيطة وتتحقق مما إذا كان person._ref في castMembers أو crewMembers يطابق الوسيطة التي تم تمريرها. نعيد الفيلم _id poster url title جنبًا إلى جنب. نرسل أيضًا إجراءً من النوع MOVIES_REF_FETCH_SUCCESS ، مع إرفاق حمولة البيانات التي تم إرجاعها ، وفي حالة حدوث خطأ ، نرسل إجراءً من النوع MOVIE_REF_FETCH_FAIL ، مع إرفاق حمولة من رسالة الخطأ ، وذلك بفضل غلاف try-catch .

في وظيفة fetchMovieById ، استخدمنا GROQ لاسترداد فيلم يطابق id معينًا تم تمريره إلى الوظيفة.

يتم عرض صيغة GROQ للوظيفة أدناه.

 const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` );

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

استرجعنا id الفيلم وأيضًا بعض السمات في مصفوفة castMembers وهي ref و characterName واسم الشخص وصورة الشخص. قمنا أيضًا بتغيير الاسم المستعار من castMembers إلى cast .

مثل أعضاء فريق العمل ، castMembers بعض السمات من مجموعة crewMembers ، وهي ref department job واسم الشخص وصورة الشخص. قمنا أيضًا بتغيير الاسم المستعار من crewMembers الطاقم إلى crew .

وبنفس الطريقة ، اخترنا نص العرض العام والشهرة وعنوان URL لملصق الفيلم وتاريخ إصدار الفيلم وعنوانه.

تتيح لنا لغة GROQ من Sanity أيضًا فرز المستند. لفرز عنصر ، نقوم بتمرير الطلب بجانب مشغل الأنابيب .

على سبيل المثال ، إذا كنا نرغب في فرز الأفلام حسب releaseDate بترتيب تصاعدي ، فيمكننا القيام بما يلي.

 const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );

استخدمنا هذه الفكرة في دالة sortMoviesBy للفرز إما بترتيب تصاعدي أو تنازلي.

دعنا نلقي نظرة على هذه الوظيفة أدناه.

 const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } };

بدأنا بإرسال إجراء من النوع MOVIES_SORT_REQUEST لتحديد وقت تحميل الطلب. ثم استخدمنا صيغة GROQ لفرز وجلب البيانات من مجموعة movie . يتم توفير العنصر المطلوب الفرز وفقًا له في item المتغير ويتم توفير وضع الفرز (تصاعديًا أو تنازليًا) في type المتغير. وبالتالي ، قمنا بإرجاع id وعنوان url والملصق والعنوان. بمجرد إرجاع البيانات ، أرسلنا إجراءً من النوع MOVIES_SORT_SUCCESS وإذا فشل ، نرسل إجراءً من النوع MOVIES_SORT_FAIL .

ينطبق مفهوم GROQ مماثل على دالة getMostPopular . يتم عرض صيغة GROQ أدناه.

 const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` );

الاختلاف الوحيد هنا هو أننا قمنا بفرز الأفلام حسب الشعبية بترتيب تنازلي ثم اخترنا الأفلام الثلاثة الأولى فقط. يتم إرجاع العناصر في فهرس قائم على الصفر ، وبالتالي فإن العناصر الثلاثة الأولى هي العناصر 0 و 1 و 2. إذا أردنا استرداد العناصر العشرة الأولى ، فيمكننا تمرير [0..9] إلى الوظيفة.

هذا هو الكود الكامل لإجراءات الفيلم في ملف movieActions.js .

 import sanityAPI from "../../sanitySetup"; import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL, MOVIES_REF_FETCH_REQUEST } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } }; const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title }` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } }; const fetchMovieById = (id) => async (dispatch) => { try { dispatch({ type: MOVIE_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` ); dispatch({ type: MOVIE_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIE_FETCH_FAIL, payload: error.message }); } }; const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_MOST_POPULAR_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } }; const getMostPopular = () => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` ); dispatch({ type: MOVIES_MOST_POPULAR_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_MOST_POPULAR_FAIL, payload: error.message }); } }; export { fetchAllMovies, fetchMovieById, sortMoviesBy, getMostPopular, fetchMoviesByRef };

إعداد المخفضات

المخفضات هي واحدة من أهم المفاهيم في Redux. يأخذون الحالة السابقة ويقررون تغييرات الحالة.

عادة ، سنستخدم تعليمة التبديل لتنفيذ شرط لكل نوع من أنواع الإجراءات. على سبيل المثال ، يمكننا إعادة loading عندما يشير نوع الإجراء إلى التحميل ، ثم الحمولة عندما تشير إلى النجاح أو الخطأ. من المتوقع أن تتخذ في initial state action كحجج.

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

لنبدأ بإلقاء نظرة على fetchAllMoviesReducer في ملف movieReducers.js .

 import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } };

مثل جميع المخفضات ، يأخذ fetchAllMoviesReducer كائن الحالة الأولية ( state ) وكائن action كوسائط. استخدمنا بيان التبديل للتحقق من أنواع الإجراءات في كل نقطة زمنية. إذا كان يتوافق مع MOVIES_FETCH_REQUEST ، فإننا نعيد التحميل على النحو الصحيح لتمكيننا من إظهار مؤشر التحميل للمستخدم.

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

لدينا نفس الهيكل لمخفضات أخرى. يتم عرض movieReducers.js الكاملة أدناه.

 import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_FETCH_RESET, MOVIES_REF_FETCH_REQUEST, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } }; const fetchMoviesByRefReducer = (state = {}, action) => { switch (action.type) { case MOVIES_REF_FETCH_REQUEST: return { loading: true }; case MOVIES_REF_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_REF_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const fetchMovieByIdReducer = (state = {}, action) => { switch (action.type) { case MOVIE_FETCH_REQUEST: return { loading: true }; case MOVIE_FETCH_SUCCESS: return { loading: false, movie: action.payload }; case MOVIE_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const sortMoviesByReducer = (state = {}, action) => { switch (action.type) { case MOVIES_SORT_REQUEST: return { loading: true }; case MOVIES_SORT_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_SORT_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const getMostPopularReducer = (state = {}, action) => { switch (action.type) { case MOVIES_MOST_POPULAR_REQUEST: return { loading: true }; case MOVIES_MOST_POPULAR_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_MOST_POPULAR_FAIL: return { loading: false, error: action.payload }; default: return state; } }; export { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer };

لقد اتبعنا أيضًا نفس البنية بالضبط لـ personReducers.js . على سبيل المثال ، تحدد الدالة fetchAllPersonsReducer حالات جلب جميع الأشخاص في قاعدة البيانات.

ويرد هذا في الكود أدناه.

 import { PERSONS_FETCH_FAIL, PERSONS_FETCH_REQUEST, PERSONS_FETCH_SUCCESS, } from "../constants/personConstants"; const fetchAllPersonsReducer = (state = {}, action) => { switch (action.type) { case PERSONS_FETCH_REQUEST: return { loading: true }; case PERSONS_FETCH_SUCCESS: return { loading: false, persons: action.payload }; case PERSONS_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } };

تمامًا مثل fetchAllMoviesReducer ، قمنا بتعريف fetchAllPersonsReducer state action كوسائط. هذه إعدادات قياسية لمخفضات Redux. استخدمنا بعد ذلك عبارة switch للتحقق من أنواع الإجراءات وإذا كانت من النوع PERSONS_FETCH_REQUEST ، فسنعيد التحميل على أنه صحيح. إذا كانت PERSONS_FETCH_SUCCESS ، فإننا نوقف التحميل ونعيد الحمولة ، وإذا كانت PERSONS_FETCH_FAIL ، فإننا نعيد الخطأ.

الجمع بين المخفضات

تتيح لنا وظيفة Redux combineReducers دمج أكثر من مخفض واحد وتمريره إلى المتجر. سنقوم بدمج أفلامنا ومخفضات الأشخاص في ملف index.js داخل مجلد reducers .

دعونا نلقي نظرة عليه.

 import { combineReducers } from "redux"; import { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer } from "./movieReducers"; import { fetchAllPersonsReducer, fetchPersonByIdReducer, countPersonsReducer } from "./personReducers"; import { toggleTheme } from "./globalReducers"; export default combineReducers({ fetchAllMoviesReducer, fetchMovieByIdReducer, fetchAllPersonsReducer, fetchPersonByIdReducer, sortMoviesByReducer, getMostPopularReducer, countPersonsReducer, fetchMoviesByRefReducer, toggleTheme });

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

سنعمل على globalReducers لاحقًا.

يمكننا الآن تمرير المخفضات في ملف Redux store.js . هذا موضح أدناه.

 import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));

بعد إعداد سير عمل Redux ، فلنقم بإعداد تطبيق React الخاص بنا.

إعداد تطبيق React الخاص بنا

سيقوم تطبيق رد الفعل الخاص بنا بسرد الأفلام والممثلين وأعضاء الطاقم المطابقين. react-router-dom للتوجيه styled-components لتصميم التطبيق. سنستخدم أيضًا واجهة المستخدم المادية للأيقونات وبعض مكونات واجهة المستخدم.

أدخل الأمر bash التالي لتثبيت التبعيات.

 npm install react-router-dom @material-ui/core @material-ui/icons query-string

هذا ما سنبنيه:

ربط Redux بتطبيق React الخاص بنا

React-redux بوظيفة المزود التي تتيح لنا توصيل تطبيقنا بمتجر Redux. للقيام بذلك ، يتعين علينا تمرير مثيل المتجر إلى المزود. يمكننا القيام بذلك إما في ملف index.js أو App.js .

هذا هو ملف index.js الخاص بنا.

 import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );

هنا ، قمنا باستيراد Provider من react-redux والتخزين من store Redux الخاص بنا. ثم قمنا بلف شجرة مكوناتنا بالكامل مع الموفر ، ونقلنا المتجر إليها.

بعد ذلك ، نحتاج إلى react-router-dom للتوجيه في تطبيق React الخاص بنا. يأتي React react-router-dom مع BrowserRouter و Switch و Route الذي يمكن استخدامه لتحديد مسارنا وطرقنا.

نقوم بذلك في ملف App.js بنا. هذا موضح أدناه.

 import React from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import MoviesList from "./pages/MoviesListPage"; import PersonsList from "./pages/PersonsListPage"; function App() { return ( <Router> <main className="contentwrap"> <Header /> <Switch> <Route path="/persons/"> <PersonsList /> </Route> <Route path="/" exact> <MoviesList /> </Route> </Switch> </main> <Footer /> </Router> ); } export default App;

هذا إعداد قياسي للتوجيه باستخدام جهاز التوجيه التفاعلي دوم. يمكنك التحقق من ذلك في وثائقهم. قمنا باستيراد PersonsList من Header Footer وقائمة الأشخاص وقائمة MovieList . نقوم بعد ذلك بإعداد react-router-dom عن طريق تغليف كل شيء في Router Switch .

نظرًا لأننا نريد أن تشارك صفحاتنا نفس الرأس والتذييل ، فقد كان علينا تمرير مكون <Header /> و <Footer /> قبل تغليف الهيكل باستخدام Switch . لقد فعلنا أيضًا شيئًا مشابهًا مع العنصر main لأننا نريده أن يلف التطبيق بأكمله.

مررنا كل مكون إلى المسار باستخدام Route من react-router-dom .

تحديد صفحاتنا ومكوناتنا

طلبنا منظم بطريقة منظمة. يتم تخزين المكونات القابلة لإعادة الاستخدام في مجلد components بينما يتم تخزين Pages في مجلد pages .

تشتمل pages على movieListPage.js و moviePage.js و PersonListPage.js و PersonPage.js . يسرد MovieListPage.js جميع الأفلام الموجودة في الواجهة الخلفية لـ Sanity.io بالإضافة إلى الأفلام الأكثر شهرة.

لسرد جميع الأفلام ، نقوم ببساطة dispatch الإجراء fetchAllMovies المحدد في ملف movieAction.js بنا. نظرًا لأننا نحتاج إلى جلب القائمة بمجرد تحميل الصفحة ، يتعين علينا تحديدها في useEffect . هذا موضح أدناه.

 import React, { useEffect } from "react"; import { fetchAllMovies } from "../redux/actions/movieActions"; import { useDispatch, useSelector } from "react-redux"; const MoviesListPage = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchAllMovies()); }, [dispatch]); const { loading, error, movies } = useSelector( (state) => state.fetchAllMoviesReducer ); return ( ... ) }; export default MoviesListPage;

بفضل useDispatch و useSelector Hooks ، يمكننا إرسال إجراءات Redux وتحديد الحالات المناسبة من متجر Redux. لاحظ أن حالات loading error movies تم تعريفها في وظائف Reducer الخاصة بنا وتم تحديدها هنا باستخدام الخطاف useSelector من React Redux. تصبح هذه الحالات ، وهي loading error movies متاحة على الفور ، قمنا بإرسال إجراءات fetchAllMovies() .

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

هذا هو الكود الكامل لملف moviesListPage.js .

import React, {useState, useEffect} from 'react' import {fetchAllMovies, getMostPopular, sortMoviesBy} from "../redux/actions/movieActions" import {useDispatch, useSelector} from "react-redux" import Loader from "../components/BackdropLoader" import {MovieListContainer} from "../styles/MovieStyles.js" import SortIcon from '@material-ui/icons/Sort'; import SortModal from "../components/Modal" import {useLocation, Link} from "react-router-dom" import queryString from "query-string" import {MOVIES_FETCH_RESET} from "../redux/constants/movieConstants" const MoviesListPage = () => { const location = useLocation() const dispatch = useDispatch() const [openSort, setOpenSort] = useState(false) useEffect(()=>{ dispatch(getMostPopular()) const {order, type} = queryString.parse(location.search) if(order && type){ dispatch({ type: MOVIES_FETCH_RESET }) dispatch(sortMoviesBy(order, type)) }else{ dispatch(fetchAllMovies()) } }, [dispatch, location.search]) const {loading: popularLoading, error: popularError, movies: popularMovies } = useSelector(state => state.getMostPopularReducer) const { loading: moviesLoading, error: moviesError, movies } = useSelector(state => state.fetchAllMoviesReducer) const { loading: sortLoading, error: sortError, movies: sortMovies } = useSelector(state => state.sortMoviesByReducer) return ( <MovieListContainer> <div className="mostpopular"> { popularLoading ? <Loader /> : popularError ? popularError : popularMovies && popularMovies.map(movie => ( <Link to={`/movie?id=${movie._id}`} className="popular" key={movie._id} style={{backgroundImage: `url(${movie.poster})`}}> <div className="content"> <h2>{movie.title}</h2> <p>{movie.overview.text.substring(0, 50)}…</p> </div> </Link> )) } </div> <div className="moviespanel"> <div className="top"> <h2>All Movies</h2> <SortIcon onClick={()=> setOpenSort(true)} /> </div> <div className="movieslist"> { moviesLoading ? <Loader /> : moviesError ? moviesError : movies && movies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) } { ( sortLoading ? !movies && <Loader /> : sortError ? sortError : sortMovies && sortMovies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) ) } </div> </div> <SortModal open={openSort} setOpen={setOpenSort} /> </MovieListContainer> ) } export default MoviesListPage

بدأنا بإرسال حركة أفلام getMostPopular (يحدد هذا الإجراء الأفلام ذات الشعبية الأكبر) في خطاف useEffect . هذا يسمح لنا باسترداد الأفلام الأكثر شعبية بمجرد تحميل الصفحة. بالإضافة إلى ذلك ، سمحنا للمستخدمين بفرز الأفلام حسب releaseDate popularity . يتم التعامل مع هذا عن طريق sortMoviesBy الإجراء المرسل في الكود أعلاه. علاوة على ذلك ، fetchAllMovies اعتمادًا على معاملات الاستعلام.

أيضًا ، استخدمنا خطاف useSelector لتحديد المخفضات المقابلة لكل من هذه الإجراءات. اخترنا حالات loading error movies لكل من مخفضات السرعة.

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

<MovieListContainer> … </MovieListContainer> هي div معرفة باستخدام مكونات ذات نمط. سنلقي نظرة سريعة على ذلك قريبًا.

تصميم تطبيقنا بمكونات مصممة

تسمح لنا المكونات المصممة بتصميم صفحاتنا ومكوناتنا على أساس فردي. كما أنه يقدم بعض الميزات المثيرة للاهتمام مثل inheritance ، Theming ، passing of props ، وما إلى ذلك.

على الرغم من أننا نريد دائمًا تصميم صفحاتنا على أساس فردي ، فقد يكون التصميم العالمي مرغوبًا في بعض الأحيان. ومن المثير للاهتمام أن المكونات المصممة توفر طريقة للقيام بذلك بفضل وظيفة createGlobalStyle .

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

 npm install styled-components

بعد تثبيت المكونات المصممة ، دعنا نبدأ مع الأنماط العالمية الخاصة بنا.

لنقم بإنشاء مجلد منفصل في دليل src الخاص بنا باسم styles . هذا سوف يخزن كل ما لدينا من أنماط. لنقم أيضًا بإنشاء ملف globalStyles.js داخل مجلد الأنماط. لإنشاء نمط عالمي في المكونات المصممة ، نحتاج إلى استيراد createGlobalStyle .

 import { createGlobalStyle } from "styled-components";

يمكننا بعد ذلك تحديد أنماطنا على النحو التالي:

 export const GlobalStyle = createGlobalStyle` ... `

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

لقد قمنا أيضًا باستيراد deviceWidth المحدد في ملف يسمى definition.js . يحتوي deviceWidth على تعريف نقاط التوقف لتعيين استعلامات الوسائط الخاصة بنا.

 import { deviceWidth } from "./definition";

وضعنا الفائض على المخفي للتحكم في تدفق تطبيقنا.

 html, body{ overflow-x: hidden; }

حددنا أيضًا نمط الرأس باستخدام محدد نمط .header .

 .header{ z-index: 5; background-color: ${(props)=>props.theme.midDarkBlue}; display:flex; align-items:center; padding: 0 20px; height:50px; justify-content:space-between; position:fixed; top:0; width:100%; @media ${deviceWidth.laptop_lg} { width:97%; } ... }

هنا ، يتم تحديد أنماط مختلفة مثل لون الخلفية ، والفهرس z ، والحشو ، والكثير من خصائص CSS التقليدية الأخرى.

لقد استخدمنا props المكونات المصممة لتعيين لون الخلفية. هذا يسمح لنا بتعيين المتغيرات الديناميكية التي يمكن تمريرها من المكون الخاص بنا. علاوة على ذلك ، مررنا أيضًا متغير السمة لتمكيننا من تحقيق أقصى استفادة من تبديل السمة.

يمكن وضع التصميم هنا لأننا قمنا بلف تطبيقنا بالكامل باستخدام ThemeProvider من المكونات المصممة. سنتحدث عن هذا في لحظة. علاوة على ذلك ، استخدمنا CSS flexbox بشكل صحيح وضبط الموضع على fixed للتأكد من أنه يظل ثابتًا فيما يتعلق بالمتصفح. لقد حددنا أيضًا نقاط التوقف لجعل الرؤوس متوافقة مع الأجهزة المحمولة.

هذا هو الكود الكامل لملف globalStyles.js الخاص بنا.

 import { createGlobalStyle } from "styled-components"; import { deviceWidth } from "./definition"; export const GlobalStyle = createGlobalStyle` html{ overflow-x: hidden; } body{ background-color: ${(props) => props.theme.lighter}; overflow-x: hidden; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } #root{ display: grid; flex-direction: column; } h1,h2,h3, label{ font-family: 'Aclonica', sans-serif; } h1, h2, h3, p, span:not(.MuiIconButton-label), div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){ color: ${(props) => props.theme.bodyText} } p, span, div, input{ font-family: 'Jost', sans-serif; } .paginate button{ color: ${(props) => props.theme.bodyText} } .header{ z-index: 5; background-color: ${(props) => props.theme.midDarkBlue}; display: flex; align-items: center; padding: 0 20px; height: 50px; justify-content: space-between; position: fixed; top: 0; width: 100%; @media ${deviceWidth.laptop_lg}{ width: 97%; } @media ${deviceWidth.tablet}{ width: 100%; justify-content: space-around; } a{ text-decoration: none; } label{ cursor: pointer; color: ${(props) => props.theme.goldish}; font-size: 1.5rem; } .hamburger{ cursor: pointer; color: ${(props) => props.theme.white}; @media ${deviceWidth.desktop}{ display: none; } @media ${deviceWidth.tablet}{ display: block; } } } .mobileHeader{ z-index: 5; background-color: ${(props) => props.theme.darkBlue}; color: ${(props) => props.theme.white}; display: grid; place-items: center; width: 100%; @media ${deviceWidth.tablet}{ width: 100%; } height: calc(100% - 50px); transition: all 0.5s ease-in-out; position: fixed; right: 0; top: 50px; .menuitems{ display: flex; box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme}; flex-direction: column; align-items: center; justify-content: space-around; height: 60%; width: 40%; a{ display: flex; flex-direction: column; align-items:center; cursor: pointer; color: ${(props) => props.theme.white}; text-decoration: none; &:hover{ border-bottom: 2px solid ${(props) => props.theme.goldish}; .MuiSvgIcon-root{ color: ${(props) => props.theme.lightred} } } } } } footer{ min-height: 30px; margin-top: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.875rem; background-color: ${(props) => props.theme.midDarkBlue}; color: ${(props) => props.theme.white}; } `;

لاحظ أننا كتبنا كود CSS خالصًا داخل الحرف الحرفي ولكن هناك بعض الاستثناءات. تسمح لنا المكونات المصممة بتمرير الدعائم. يمكنك معرفة المزيد عن هذا في الوثائق.

بصرف النظر عن تحديد الأنماط العامة ، يمكننا تحديد أنماط للصفحات الفردية.

على سبيل المثال ، هذا هو نمط PersonListPage.js المحدد في PersonStyle.js في مجلد styles .

 import styled from "styled-components"; import { deviceWidth, colors } from "./definition"; export const PersonsListContainer = styled.div` margin: 50px 80px; @media ${deviceWidth.tablet} { margin: 50px 10px; } a { text-decoration: none; } .top { display: flex; justify-content: flex-end; padding: 5px; .MuiSvgIcon-root { cursor: pointer; &:hover { color: ${colors.darkred}; } } } .personslist { margin-top: 20px; display: grid; place-items: center; grid-template-columns: repeat(5, 1fr); @media ${deviceWidth.laptop} { grid-template-columns: repeat(4, 1fr); } @media ${deviceWidth.tablet} { grid-template-columns: repeat(3, 1fr); } @media ${deviceWidth.tablet_md} { grid-template-columns: repeat(2, 1fr); } @media ${deviceWidth.mobile_lg} { grid-template-columns: repeat(1, 1fr); } grid-gap: 30px; .person { width: 200px; position: relative; img { width: 100%; } .content { position: absolute; bottom: 0; left: 8px; border-right: 2px solid ${colors.goldish}; border-left: 2px solid ${colors.goldish}; border-radius: 10px; width: 80%; margin: 20px auto; padding: 8px 10px; background-color: ${colors.transparentWhite}; color: ${colors.darkBlue}; h2 { font-size: 1.2rem; } } } } `;

styled أولاً نمطًا من styled-components deviceWidth من ملف definition . ثم PersonsListContainer على أنه div لإبقاء أنماطنا. باستخدام استعلامات الوسائط ونقاط التوقف المحددة ، جعلنا الصفحة مناسبة للجوّال من خلال تعيين نقاط توقف مختلفة.

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

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

 import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;

سينتج الغلاف div لأننا قمنا بتعريفه على أنه div في أنماطنا.

إضافة الموضوعات والتفافها

إنها دائمًا ميزة رائعة لإضافة سمات إلى تطبيقنا. لهذا نحتاج إلى ما يلي:

  • تم تحديد سماتنا المخصصة في ملف منفصل (في ملف definition.js الحالة الخاص بنا).
  • المنطق المحدد في إجراءات Redux ومخفضاتنا.
  • استدعاء السمة الخاصة بنا في تطبيقنا وتمريرها عبر شجرة المكونات.

دعنا نتحقق من هذا.

ها هو theme موضوعنا في ملف definition.js .

 export const theme = { light: { dark: "#0B0C10", darkBlue: "#253858", midDarkBlue: "#42526e", lightBlue: "#0065ff", normal: "#dcdcdd", lighter: "#F4F5F7", white: "#FFFFFF", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "#0B0C10", lightshadowtheme: "rgba(0, 0, 0, 0.1)" }, dark: { dark: "white", darkBlue: "#06090F", midDarkBlue: "#161B22", normal: "#dcdcdd", lighter: "#06090F", white: "white", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "white", lightshadowtheme: "rgba(255, 255, 255, 0.9)" } };

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

بعد ذلك ، دعنا نضيف الوظيفة إلى Redux.

لقد أنشأنا globalActions.js في مجلد إجراءات Redux وأضفنا الأكواد التالية.

 import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; import { theme } from "../../styles/definition"; export const switchToLightTheme = () => (dispatch) => { dispatch({ type: SET_LIGHT_THEME, payload: theme.light }); localStorage.setItem("theme", JSON.stringify(theme.light)); localStorage.setItem("light", JSON.stringify(true)); }; export const switchToDarkTheme = () => (dispatch) => { dispatch({ type: SET_DARK_THEME, payload: theme.dark }); localStorage.setItem("theme", JSON.stringify(theme.dark)); localStorage.setItem("light", JSON.stringify(false)); };

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

نحتاج أيضًا إلى تحديد مخفضنا للموضوعات.

 import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; export const toggleTheme = (state = {}, action) => { switch (action.type) { case SET_LIGHT_THEME: return { theme: action.payload, light: true }; case SET_DARK_THEME: return { theme: action.payload, light: false }; default: return state; } };

هذا مشابه جدًا لما كنا نفعله. استخدمنا عبارة switch للتحقق من نوع الإجراء ثم أعدنا payload المناسبة. قمنا أيضًا بإعادة light الحالة الذي يحدد ما إذا كان المستخدم قد تم تحديد السمة الفاتحة أو الداكنة. سنستخدم هذا في مكوناتنا.

نحتاج أيضًا إلى إضافته إلى مخفض الجذر والمخزن. هذا هو الرمز الكامل store.js .

 import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { theme as initialTheme } from "../styles/definition"; import reducers from "./reducers/index"; const theme = localStorage.getItem("theme") ? JSON.parse(localStorage.getItem("theme")) : initialTheme.light; const light = localStorage.getItem("light") ? JSON.parse(localStorage.getItem("light")) : true; const initialState = { toggleTheme: { light, theme } }; export default createStore(reducers, initialState, applyMiddleware(thunk));

نظرًا لأننا احتجنا إلى استمرار الموضوع عند تحديث المستخدم ، فقد كان علينا الحصول عليه من التخزين المحلي باستخدام localStorage.getItem() إلى حالتنا الأولية.

إضافة الوظيفة إلى تطبيق React الخاص بنا

تزودنا المكونات المصممة بـ ThemeProvider الذي يسمح لنا بتمرير السمات من خلال تطبيقنا. يمكننا تعديل ملف App.js لإضافة هذه الوظيفة.

دعونا نلقي نظرة عليه.

 import React from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { useSelector } from "react-redux"; import { ThemeProvider } from "styled-components"; function App() { const { theme } = useSelector((state) => state.toggleTheme); let Theme = theme ? theme : {}; return ( <ThemeProvider theme={Theme}> <Router> ... </Router> </ThemeProvider> ); } export default App;

من خلال تمرير السمات عبر ThemeProvider ، يمكننا بسهولة استخدام دعائم السمات في أنماطنا.

على سبيل المثال ، يمكننا ضبط اللون على bodyText المخصص للنص الأساسي على النحو التالي.

 color: ${(props) => props.theme.bodyText};

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

على سبيل المثال ، لتحديد border-bottom ، نقوم بما يلي.

 border-bottom: 2px solid ${(props) => props.theme.goldish};

خاتمة

بدأنا بالخوض في Sanity.io وإعداده وربطه بتطبيق React الخاص بنا. ثم قمنا بإعداد Redux واستخدمنا لغة GROQ للاستعلام عن واجهة برمجة التطبيقات الخاصة بنا. لقد رأينا كيفية الاتصال واستخدام Redux لتطبيقنا React باستخدام react-redux واستخدام المكونات المصممة والتخصيص.

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

موارد

  • وثائق السلامة الصحية
  • كيفية إنشاء مدونة باستخدام Sanity.io بواسطة Kapehe
  • إعادة التوثيق
  • توثيق المكونات المصممة
  • ورقة الغش GROQ
  • وثائق واجهة المستخدم المادية
  • برنامج Redux Middleware و SideEffects
  • إعادة توثيق Thunk