Menu
CoddyTech

وصفة اليوم

جزء من قسم أساسيات Express في رحلة Node.js على Coddy. الدرس 22 من 23.

يمكن لـ Recipe Box استخدام مهارات مألوفة في المسارات وقراءة الملفات لتقديم وصفة واحدة لكل يوم. يحوّل المساعد dayOfYear المزوّد تاريخًا إلى رقم: 1 يناير هو 1، و1 مارس 2026 هو 60. إذا لم يُحدَّد تاريخ، فسيُستخدم تاريخ اليوم؛ أما النص الذي يتعذّر قراءته فيُرجع null.

const day = dayOfYear(req.query.date);
// ?date=2026-03-01 يُرجع 60
// ?date=soon يُرجع null

استخدم عامل باقي القسمة لاختيار موضع: day % recipes.length. مع أربع وصفات، يختار اليوم 60 الموضع صفر. حمّل الوصفات داخل الطلب كي تُؤخذ الوصفات المنشورة حديثًا في الحسبان أيضًا. تحليل التاريخ متوفّر مسبقًا؛ ركّز على تركيب المسار.

challenge icon

التحدي

سهل

أضف نقطة نهاية لوصفة اليوم إلى Recipe Box. يصدّر الملف المقدَّم day-of-year.js الدالة dayOfYear(text)، التي تُعيد رقم اليوم أو null للتاريخ غير الصالح.

  1. صدّر loadRecipes من routes/recipes.js. استورده في server.js: يبقى الموجّه هو الاستيراد الافتراضي، ويتبعه { loadRecipes } بعد فاصلة.
  2. استورد dayOfYear. في server.js، أضف مسارًا غير متزامنًا لـ GET /api/today فوق معالج 404. مرّر req.query.date إلى الدالة المساعدة.
  3. عندما تكون القيمة null، أعد الحالة 400 مع {"error":"Use a date like 2026-03-01"}.
  4. وإلا، انتظر loadRecipes() وأعد recipes[day % recipes.length] بتنسيق JSON. أبقِ المسارات الحالية ومعالج الأخطاء كما هي.

جرّب GET /api/today?date=2026-03-01: ستُعاد الوصفة الأولى لأن رقم اليوم هو 60 ولأن عدد الوصفات أربع. جرّب ?date=soon: سيُعاد خطأ 400 المذكور أعلاه.

جرّب بنفسك

import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
import recipesRouter from './routes/recipes.js';
import reviewsRouter from './routes/reviews.js';

const app = express();

app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});

app.use((req, res, next) => {
    res.set('X-Request-Id', randomUUID());
    next();
});

app.use(express.json());

app.use(express.static('public'));

app.get('/', (req, res) => {
    const items = recipes.map((r) => `<li><a href="/recipes/${r.id}">${escapeHtml(r.title)}</a></li>`);
    res.send(`<link rel="stylesheet" href="/style.css"><h1>Recipe Box</h1><ul>${items.join('')}</ul>`);
});

// ترسل type('text') نصًا عاديًا، لذا يظهر كل عنوان في سطر مستقل.
app.get('/recipes', (req, res) => {
    const { tag } = req.query;
    const list = tag ? recipes.filter((recipe) => recipe.tags.includes(tag)) : recipes;
    res.type('text').send(list.map((recipe) => recipe.title).join('\n'));
});

app.get('/recipes/:id', (req, res) => {
    const recipe = recipes.find((r) => r.id === Number(req.params.id));
    if (!recipe) {
        res.status(404).send('No such recipe');
        return;
    }
    res.send(recipe.title);
});

app.use('/api/recipes', recipesRouter);
app.use('/api/recipes/:id/reviews', reviewsRouter);

app.get('/about', (req, res) => {
    res.send('Recipe Box collects family recipes.');
});

app.get('/menu', (req, res) => {
    res.redirect(301, '/recipes');
});

app.get('/menu/:id', (req, res) => {
    res.redirect(301, `/recipes/${req.params.id}`);
});

app.use((req, res) => {
    if (req.path.startsWith('/api/')) {
        res.status(404).json({ error: 'Not found' });
        return;
    }
    res.status(404).send('<h1>Page not found</h1><p><a href="/">Back to Recipe Box</a></p>');
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).json({ error: 'Something went wrong' });
});

app.listen(3000, () => {
    console.log('Recipe Box is running on port 3000');
});

جميع دروس أساسيات Express

تدرّب بنفسك: Playground لـ Express