المحطّة ٦: واجهة React تعرض المواعيد وتحجز
تكامل الطبقتين: واجهةٌ تعرض المواعيد وتحجزها من خادمك. ⏱ نحو 34 دقيقة.
- تجلب بيانات الخادم وتعرضها في
React. - ترسل طلب
POSTبجسمJSONمن معالج حدث. - تحدّث الواجهة لتعكس حالة الخادم بعد التغيير.
- تربط طبقتَي المشروع في تطبيقٍ واحدٍ يعمل.
١الطبقة الأخيرة: واجهةٌ يلمسها المستخدم
الخادم جاهزٌ من المحطّات السابقة. نبني الآن واجهة React تستهلك الـ API: تجلب المواعيد المتاحة وتعرضها، وتحجز بضغطة زرّ. هذا ربطٌ أتقنته في مقرّر 242 (fetch داخل useEffect، والإرسال في معالج الحدث)؛ هنا نطبّقه على مشروعنا.
قاعدتان من 242 نكرّرهما:
- الجلب عند الظهور في
useEffect(() => {{…}}, []). - بعد الحجز: أعد الجلب (أو حدّث الحالة) حتى تعكس الواجهة ما صار في الخادم.
٢التطبيق الكامل يعمل
هذه الواجهة متّصلةٌ بالخادم الحقيقيّ في الصفحة. جرّب: اضغط «احجز» على موعدٍ فيختفي من المتاحة وتظهر رسالة تأكيد:
import { useState, useEffect } from 'react'; const API = 'http://localhost:3000';const PATIENT = 'سعود'; // المريض المسجَّل دخوله (من محطّة المصادقة) export default function App() { const [slots, setSlots] = useState([]); const [msg, setMsg] = useState(''); const load = () => fetch(`${API}/api/appointments?status=free`) .then(r => r.json()) .then(d => setSlots(d.data)); useEffect(() => { load(); }, []); async function book(id) { const res = await fetch(`${API}/api/appointments/${id}/book`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patient: PATIENT }), }); const data = await res.json(); if (!res.ok) return setMsg(data.error.message); setMsg('تمّ الحجز'); load(); } return ( <div> <h2>المواعيد المتاحة</h2> {msg && <p className="msg">{msg}</p>} <ul className="slots"> {slots.map(s => ( <li key={s.id}> {s.doctor} {s.time} <button onClick={() => book(s.id)}>احجز</button> </li> ))} </ul> </div> );}
const express = require('express');const app = express();app.use(express.json()); let appts = [ { id: 1, doctor: 'د. منى', time: '09:00', bookedBy: null }, { id: 2, doctor: 'د. منى', time: '09:30', bookedBy: 'سارة' }, { id: 3, doctor: 'د. أحمد', time: '10:00', bookedBy: null },]; app.get('/api/appointments', (req, res) => { let data = appts; if (req.query.status === 'free') data = appts.filter(a => a.bookedBy === null); res.json({ data, meta: { total: data.length } });});app.post('/api/appointments/:id/book', (req, res) => { const a = appts.find(x => x.id === Number(req.params.id)); if (!a) return res.status(404).json({ error: { code: 'NOT_FOUND', message: 'لا يوجد موعد' } }); if (a.bookedBy !== null) return res.status(409).json({ error: { code: 'CONFLICT', message: 'الموعد محجوز' } }); a.bookedBy = req.body.patient; res.json({ data: a });}); app.listen(3000);
لاحظ كيف أنّ «مصدر الحقيقة» هو الخادم: بعد الحجز نعيد الجلب فتختفي البطاقة لأنّ bookedBy لم يعد null. الواجهة تعرض حالة الخادم، لا حالةً خاصّةً بها.
توقّع وتحقّق
أين نجلب البيانات عند ظهور المكوّن؟
لماذا نعيد الجلب بعد الحجز؟
fetch عند ردّ 409 ماذا يفعل؟
ما الذي يرسل جسم JSON في POST؟
الخلاصة
- الجلب في
useEffectعند الظهور؛ الإرسال في معالج النقر. POST = method + headers Content-Type + body JSON.stringify.- بعد التغيير أعِد الجلب حتى تعكس الواجهة مصدر الحقيقة (الخادم).
fetchلا يرمي عند 409؛ افحصres.okوأظهر رسالة الخطأ.- بهذا اكتمل المنتج: واجهةٌ تطلب، وخادمٌ آمنٌ يردّ.
تحدٍّ برمجي: واجهة الحجز
برمجيالخادم جاهزٌ في server/server.js. اكتب واجهة React تعرض المواعيد المتاحة وتحجز.
- عند الظهور: اجلب
GET /api/appointments?status=freeواعرض كل موعدٍ في<li>داخل<ul className="slots">، مع زرّ «احجز». - عند الضغط:
POST /api/appointments/:id/bookبالجسم{"patient": PATIENT}، ثم أعِد الجلب فيختفي الموعد المحجوز. - عند النجاح اعرض
<p className="msg">تمّ الحجز</p>.
👀 اعرض الناتج المطلوب (وجرّبه)
- الكود يعمل بلا أخطاء في Console
- يعرض الموعدين المتاحين من الخادم
- الحجز يرسل للخادم فيختفي الموعد وتظهر رسالة التأكيد
- يستخدم fetch مع POST و JSON.stringify
💡 تلميح 1
useEffect(() => { load(); }, []) مع دالّة load تجلب المتاح.💡 تلميح 2
async function book(id) ترسل POST ثم تستدعي load() وتضبط الرسالة.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
import { useState, useEffect } from 'react'; const API = 'http://localhost:3000';const PATIENT = 'سعود'; // المريض المسجَّل دخوله (من محطّة المصادقة) export default function App() { const [slots, setSlots] = useState([]); const [msg, setMsg] = useState(''); const load = () => fetch(`${API}/api/appointments?status=free`) .then(r => r.json()) .then(d => setSlots(d.data)); useEffect(() => { load(); }, []); async function book(id) { const res = await fetch(`${API}/api/appointments/${id}/book`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patient: PATIENT }), }); const data = await res.json(); if (!res.ok) return setMsg(data.error.message); setMsg('تمّ الحجز'); load(); } return ( <div> <h2>المواعيد المتاحة</h2> {msg && <p className="msg">{msg}</p>} <ul className="slots"> {slots.map(s => ( <li key={s.id}> {s.doctor} {s.time} <button onClick={() => book(s.id)}>احجز</button> </li> ))} </ul> </div> );}
const express = require('express');const app = express();app.use(express.json()); let appts = [ { id: 1, doctor: 'د. منى', time: '09:00', bookedBy: null }, { id: 2, doctor: 'د. منى', time: '09:30', bookedBy: 'سارة' }, { id: 3, doctor: 'د. أحمد', time: '10:00', bookedBy: null },]; app.get('/api/appointments', (req, res) => { let data = appts; if (req.query.status === 'free') data = appts.filter(a => a.bookedBy === null); res.json({ data, meta: { total: data.length } });});app.post('/api/appointments/:id/book', (req, res) => { const a = appts.find(x => x.id === Number(req.params.id)); if (!a) return res.status(404).json({ error: { code: 'NOT_FOUND', message: 'لا يوجد موعد' } }); if (a.bookedBy !== null) return res.status(409).json({ error: { code: 'CONFLICT', message: 'الموعد محجوز' } }); a.bookedBy = req.body.patient; res.json({ data: a });}); app.listen(3000);