IT10306493_Lab – MongoDB+Express

ติดตั้ง MongoDB และ MongoExpress ผ่าน Docker

สร้างไฟล์ docker-compose.yaml ไว้ที่ root directory พร้อมคัดลอกโค้ดดังต่อไปนี้

services:
  mongo:
    image: mongo:latest
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=root1234
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    healthcheck:
      test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
      interval: 5s
      timeout: 5s
      retries: 5

  mongo-express:
    image: mongo-express
    restart: always
    ports:
      - "8088:8081"
    environment:
      - ME_CONFIG_MONGODB_SERVER=mongo
      - ME_CONFIG_MONGODB_ADMINUSERNAME=root
      - ME_CONFIG_MONGODB_ADMINPASSWORD=root1234
      - ME_CONFIG_MONGODB_AUTH_DATABASE=admin
      - ME_CONFIG_BASICAUTH_USERNAME=root
      - ME_CONFIG_BASICAUTH_PASSWORD=root1234
    depends_on:
      - mongo

volumes:
  mongo-data:

เปิดโปรแกรม Docker Desktop และรันคำสั่งต่อไปนี้ใน Command Line

docker compose up -d

จะปรากฎผลลัพธ์ดังแสดงต่อไปนี้

และในหน้าจอ Docker Desktop ดังแสดงต่อไปนี้

ทดสอบเป็นไปที่ URL: http://localhost:8088/

กรอก Username เป็น “root” และ Password เป็น “root1234” ตามที่ได้กำหนดในไฟล์ docker-compose.yaml จะปรากฎหน้าจอ

ติดตั้ง MongoDB และ Mongoose

รันคำสั่งต่อไปนี้ใน Command Line

npm install mongodb
npm install mongoose

แก้ไขโค้ดให้เชื่อมต่อกับ MongoDB ด้วย Mongoose

แก้ไขไฟล์ .env ให้มีรายละเอียดดังต่อไปนี้

NODE_ENV = development
PORT = 8000
DB_USER = root
DB_PASSWORD = root1234
DB_NAME = tourdb

แก้ไขไฟล์ server.js ดังแสดงต่อไปนี้

"use strict";
const mongoose = require("mongoose");  // <- เรียกใช้ mongoose lib
const dotenv = require("dotenv");
dotenv.config();
const app = require("./app");

// Connect to DB
mongoose.connect(`mongodb://${process.env.DB_USER}:${process.env.DB_PASSWORD}@localhost:27017/${process.env.DB_NAME}?authSource=admin`)
  .then(() => {
    console.log("DB connection successful!");
  })
  .catch((err) => {
    console.log("DB connection error:", err);
  });

const port = process.env.PORT || 8000;
app.listen(port, () => {
  console.log(`App listening on ${port}...`);
});

เขียนโค้ดเพื่อสร้าง Model Schema ในตอนนี้เราจะสร้าง Tour model

สร้าง director ชื่อ models พร้อมสร้างไฟล์ชื่อ tourModel.js และเขียนโค้ดดังแสดงต่อไปนี้

const mongoose = require("mongoose");

const tourSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: [true, "A tour must have a name"],
      unique: true,
      trim: true,
      maxlength: [40, "A tour name must have less or equal than 40 characters"],
      minlength: [5, "A tour name must have more or equal than 5 characters"],
    },
    duration: {
      type: Number,
      required: [true, "A tour must have a duration"],
    },
    maxGroupSize: {
      type: Number,
      required: [true, "A tour must have a group size"],
    },
    difficulty: {
      type: String,
      required: [true, "A tour must have a difficulty"],
      enum: {
        values: ["easy", "medium", "difficult"],
        message: "Difficulty is either: easy, medium, difficult",
      },
    },
    ratingsAverage: {
      type: Number,
      default: 4.5,
      min: [1, "Rating must be above 1.0"],
      max: [5, "Rating must not be more than 5.0"],
      set: (val) => Math.round(val * 10) / 10, // Round to 1 decimal place
    },
    ratingsQuantity: {
      type: Number,
      default: 0,
    },
    price: {
      type: Number,
      required: [true, "A tour must have a price"],
    },
    priceDiscount: {
      type: Number,
      validate: {
        validator: function (val) {
          // this only points to current doc on NEW document creation
          return val < this.price;
        },
        message: "Discount price ({VALUE}) should be below regular price",
      },
    },
    summary: {
      type: String,
      trim: true,
      required: [true, "A tour must have a summary"],
    },
    description: {
      type: String,
      trim: true,
    },
    imageCover: {
      type: String,
      required: [true, "A tour must have a cover image"],
    },
    images: [String],
    startDates: [String],
    startLocation: String,
    locations: [
      {
        type: {
          type: String,
          default: "Point",
          enum: ["Point"],
        },
        coordinates: [Number],
        address: String,
        description: String,
        day: Number,
      },
    ],
  },
  {
    timestamps: true,
    toJSON: { virtuals: true },
    toObject: { virtuals: true },
  }
);

// Indexes for better performance
tourSchema.index({ price: 1, ratingsAverage: -1 });
tourSchema.index({ startLocation: 1 });

// Virtual property for duration in weeks
tourSchema.virtual("durationWeeks").get(function () {
  return this.duration / 7;
});

// Document middleware: runs before .save() and .create()
tourSchema.pre("save", function (next) {
  // You can add any pre-save logic here
  next();
});

// Query middleware
tourSchema.pre(/^find/, function (next) {
  // this points to the current query
  this.find({ active: { $ne: false } });
  next();
});

// Aggregation middleware
tourSchema.pre("aggregate", function (next) {
  this.pipeline().unshift({ $match: { active: { $ne: false } } });
  next();
});

const Tour = mongoose.model("Tour", tourSchema);

module.exports = Tour;

ใน controllers ไฟล์ tourController.js แก้ไขโค้ดดังต่อไปนี้

เพิ่ม require tourModel.js

const Tour = require("../models/tourModel");

แก้ไขโค้ดในฟังก์ชัน readAllTours() ด้งแสดงต่อไปนี้

exports.readAllTours = async (req, res) => {
  try {
    const tours = await Tour.find(); // <- อ่านจาก MongoDB
    res.status(200).json({
      status: "success",
      results: tours.length,
      data: tours,
    });
  } catch (err) {
    res.status(500).json({
      status: "error",
      message: err.message,
    });
  }
};

ทดสอบอ่านค่าจาก Postman ด้วย HTTP GET Method

http://127.0.0.1:8000/api/v1/tours

จะได้

{
    "status": "success",
    "results": 0,
    "data": []
}

เราได้ข้อมูลว่าง เนื่องจากยังไม่ได้เพิ่มข้อมูล

แก้ไขฟังก์ชัน createTour() ดังแสดงต่อไปนี้

exports.createTour = async (req, res) => {
  const newTour = new Tour(req.body);
  try {
    await newTour.save();
    res.status(201).json({
      status: "success",
      data: {
        tour: newTour,
      },
    });
  } catch (err) {
    res.status(400).json({
      status: "fail",
      message: err.message,
    });
  }
};

ทดสอบสร้าง tour ผ่าน Postman โดยกรอกข้อมูลดังแสดงต่อไปนี้

  1. method: POST
  2. url: http://127.0.0.1:8000/api/v1/tours
  3. Body type: raw -> JSON

JSON data:

{
  "name": "The Forest Hiker",
  "duration": 5,
  "maxGroupSize": 25,
  "difficulty": "easy",
  "ratingsAverage": 4.7,
  "ratingsQuantity": 37,
  "price": 397,
  "summary": "Breathtaking hike through the Canadian Banff National Park",
  "description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
  "imageCover": "tour-1-cover.jpg",
  "images": ["tour-1-1.jpg", "tour-1-2.jpg", "tour-1-3.jpg"],
  "startDates": ["2021-04-25,10:00", "2021-07-20,10:00", "2021-10-05,10:00"]
}

กดปุ่ม Send ใน Postman หากสร้างสำเร็จจะแสดงผลลัพธ์ดังต่อไปนี้

เปิดไปที่ http://localhost:8088/ จะเห็นชื่อ tourdb เป็น database ที่เราได้สร้างไว้ จากนั้นคลิกเข้าไปใน tourdb

จะปรากฎหน้ารายการ collections ในตอนนี้เรามีแค่ 1 collections (อาจเพิ่มมาได้อีกเช่น users, logins, …)

คลิกไปที่ tours

จะเห็นข้อมูลที่เราเพิ่มไปใหม่ จากนั้นทำการเพิ่มข้อมูลที่เหลือดังต่อไปนี้ ทีละชุดตามลำดับ (นักศึกษาสังเกตขอบเขตข้อมูลแต่ละชุดให้ดีใน {…} )

[
  {
    "name": "The Sea Explorer",
    "duration": 7,
    "maxGroupSize": 15,
    "difficulty": "medium",
    "ratingsAverage": 4.8,
    "ratingsQuantity": 23,
    "price": 497,
    "summary": "Exploring the jaw-dropping US east coast by foot and by boat",
    "description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
    "imageCover": "tour-2-cover.jpg",
    "images": ["tour-2-1.jpg", "tour-2-2.jpg", "tour-2-3.jpg"],
    "startDates": ["2021-06-19,10:00", "2021-07-20,10:00", "2021-08-18,10:00"]
  },
  {
    "name": "The Snow Adventurer",
    "duration": 4,
    "maxGroupSize": 10,
    "difficulty": "difficult",
    "ratingsAverage": 4.5,
    "ratingsQuantity": 13,
    "price": 997,
    "summary": "Exciting adventure in the snow with snowboarding and skiing",
    "description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!",
    "imageCover": "tour-3-cover.jpg",
    "images": ["tour-3-1.jpg", "tour-3-2.jpg", "tour-3-3.jpg"],
    "startDates": ["2022-01-05,10:00", "2022-02-12,10:00", "2023-01-06,10:00"]
  },
  {
    "name": "The City Wanderer",
    "duration": 9,
    "maxGroupSize": 20,
    "difficulty": "easy",
    "ratingsAverage": 4.6,
    "ratingsQuantity": 54,
    "price": 1197,
    "summary": "Living the life of Wanderlust in the US' most beatiful cities",
    "description": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat lorem ipsum dolor sit amet.\nConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat!",
    "imageCover": "tour-4-cover.jpg",
    "images": ["tour-4-1.jpg", "tour-4-2.jpg", "tour-4-3.jpg"],
    "startDates": ["2021-03-11,10:00", "2021-05-02,10:00", "2021-06-09,10:00"]
  },
  {
    "name": "The Park Camper",
    "duration": 10,
    "maxGroupSize": 15,
    "difficulty": "medium",
    "ratingsAverage": 4.9,
    "ratingsQuantity": 19,
    "price": 1497,
    "summary": "Breathing in Nature in America's most spectacular National Parks",
    "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!",
    "imageCover": "tour-5-cover.jpg",
    "images": ["tour-5-1.jpg", "tour-5-2.jpg", "tour-5-3.jpg"],
    "startDates": ["2021-08-05,10:00", "2022-03-20,10:00", "2022-08-12,10:00"]
  },
  {
    "name": "The Sports Lover",
    "duration": 14,
    "maxGroupSize": 8,
    "difficulty": "difficult",
    "ratingsAverage": 4.7,
    "ratingsQuantity": 28,
    "price": 2997,
    "summary": "Surfing, skating, parajumping, rock climbing and more, all in one tour",
    "description": "Nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nVoluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur!",
    "imageCover": "tour-6-cover.jpg",
    "images": ["tour-6-1.jpg", "tour-6-2.jpg", "tour-6-3.jpg"],
    "startDates": ["2021-07-19,10:00", "2021-09-06,10:00", "2022-03-18,10:00"]
  },
  {
    "name": "The Wine Taster",
    "duration": 5,
    "maxGroupSize": 8,
    "difficulty": "easy",
    "ratingsAverage": 4.5,
    "ratingsQuantity": 35,
    "price": 1997,
    "summary": "Exquisite wines, scenic views, exclusive barrel tastings,  and much more",
    "description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
    "imageCover": "tour-7-cover.jpg",
    "images": ["tour-7-1.jpg", "tour-7-2.jpg", "tour-7-3.jpg"],
    "startDates": ["2021-02-12,10:00", "2021-04-14,10:00", "2021-09-01,10:00"]
  },
  {
    "name": "The Star Gazer",
    "duration": 9,
    "maxGroupSize": 8,
    "difficulty": "medium",
    "ratingsAverage": 4.7,
    "ratingsQuantity": 28,
    "price": 2997,
    "summary": "The most remote and stunningly beautiful places for seeing the night sky",
    "description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "imageCover": "tour-8-cover.jpg",
    "images": ["tour-8-1.jpg", "tour-8-2.jpg", "tour-8-3.jpg"],
    "startDates": ["2021-03-23,10:00", "2021-10-25,10:00", "2022-01-30,10:00"]
  },
  {
    "name": "The Northern Lights",
    "duration": 3,
    "maxGroupSize": 12,
    "difficulty": "easy",
    "ratingsAverage": 4.9,
    "ratingsQuantity": 33,
    "price": 1497,
    "summary": "Enjoy the Northern Lights in one of the best places in the world",
    "description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!",
    "imageCover": "tour-9-cover.jpg",
    "images": ["tour-9-1.jpg", "tour-9-2.jpg", "tour-9-3.jpg"],
    "startDates": ["2021-12-16,10:00", "2022-01-16,10:00", "2022-12-12,10:00"]
  },
  {
    "name": "The Sports Lover",
    "startLocation": "California, USA",
    "nextStartDate": "July 2021",
    "duration": 14,
    "maxGroupSize": 8,
    "difficulty": "difficult",
    "avgRating": 4.7,
    "numReviews": 23,
    "regPrice": 2997
  }
]

หลังจากเพิ่มข้อมูลทั้งหมดแล้วจะได้

แก้ไขฟังก์ชันที่เหลือให้เป็น MondoDB

exports.getTourById = async(req, res) => {
  const id = req.params.id;
  try {
    const tour = await Tour.findById(id);
    if (!tour) {
      return res.status(404).json({
        status: "failed",
        message: "Tour not found",
      });
    }
    res.status(200).json({
      status: "success",
      requestedAt: req.requestTime,
      data: tour,
    });
  } catch (err) {
    res.status(500).json({
      status: "error",
      message: err.message,
    });
  }
};
exports.updateTour = async(req, res) => {
  try {
    const tour = await Tour.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
      runValidators: true,
    });
    if (!tour) {
      return res.status(404).json({
        status: "failed",
        message: "Tour not found",
      });
    }
    res.status(200).json({
      status: "success",
      data: {
        tour,
      },
    });
  } catch (err) {
    res.status(400).json({
      status: "fail",
      message: err.message,
    });
  }
};
exports.deleteTour = async (req, res) => {
  try {
    const tour = await Tour.findByIdAndDelete(req.params.id);
    if (!tour) {
      return res.status(404).json({
        status: "failed",
        message: "Tour not found",
      });
    }
    res.status(204).json({
      status: "success",
      data: null,
    });
  } catch (err) {
    res.status(500).json({
      status: "error",
      message: err.message,
    });
  }
};

ลบโค้ดที่ไม่ได้ใช้แล้วออก

const fs = require("fs");
const path = require("path");
const Tour = require("../models/tourModel");

const jsonData = fs.readFileSync(
  path.join(__dirname, "../", "dev-data", "tours-simple.json"),
  "utf-8"
);
const tours = JSON.parse(jsonData);
...

ทดสอบการทำงานของฟังก์ชันทั้งหมดด้วย Postman

หากเปิดไปที่ http://localhost:8088/db/tourdb/tours จะเห็น id ของข้อมูลใน MongoDB เป็นชื่อ _id เช่น 68de0ed81dc914853af2442f

ฟังก์ชัน getTourById() แก้ไขจากเดิม

http://127.0.0.1:8000/api/v1/tours/2

แก้เป็น

http://127.0.0.1:8000/api/v1/tours/68de0ed81dc914853af2442f

หมายความว่า id ใน MondoDB จะเป็นชุดตัวอักษรยาว ๆ เป็น “68de0ed81dc914853af2442f” นั่นเอง

แสดงผลลัพธ์ Tours ในหน้าเว็บ HTML

ในไฟล์ app.js แก้ไขโค้ดให้เพิ่ม homeRoutes

"use strict";

const express = require("express");
const tourRoute = require("./routes/tourRoutes");
const homeRoute = require("./routes/homeRoutes");

const app = express();

app.use((req, res, next) => {
  req.requestTime = new Date().toISOString();
  next();
});

app.use("/", homeRoute);
app.use(express.static(`${__dirname}/public`));
app.use(express.json());
app.use("/api/v1/tours", tourRoute);

module.exports = app;

ไน routes เพิ่มไฟล์ homeRoutes.js

const express = require("express");
const homeControllers = require("../controllers/homeControllers");

const tourRoute = express.Router();

tourRoute.get("/", homeControllers.getOverview);

module.exports = tourRoute;

ใน controllers เพิ่ม homeControllers.js

const fs = require("fs");
const path = require("path");

const overviewHTML = fs.readFileSync(
  path.join(__dirname, "../", "templates", "overview.html"),
  "utf-8"
);

exports.getOverview = (req, res) => {
  res.status(200).send(overviewHTML);
};

ย้ายไฟล์ public/overview.html และ public/tour.html ไปไว้ใน template

ทดสอบและรับโปรแกรม

สร้างไฟล์ใหม่ชื่อ tourCard.html

จากนั้นคัดลอกเฉพาะส่วน <div class=”card”>…</div> ไปบันทึก

<div class="card">
  <div class="card__header">
    <div class="card__picture">
      <div class="card__picture-overlay">&nbsp;</div>
      <img
        src="img/tours/tour-1-cover.jpg"
        alt="Tour 1"
        class="card__picture-img"
      />
    </div>

    <h3 class="heading-tertirary">
      <span>The Forest Hiker</span>
    </h3>
  </div>

  <div class="card__details">
    <h4 class="card__sub-heading">Easy 5-day tour</h4>
    <p class="card__text">
      Breathtaking hike through the Canadian Banff National Park
    </p>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-map-pin"></use>
      </svg>
      <span>Banff, Canada</span>
    </div>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-calendar"></use>
      </svg>
      <span>April 2021</span>
    </div>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-flag"></use>
      </svg>
      <span>3 stops</span>
    </div>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-user"></use>
      </svg>
      <span>25 people</span>
    </div>
  </div>

  <div class="card__footer">
    <p>
      <span class="card__footer-value">$297</span>
      <span class="card__footer-text">per person</span>
    </p>
    <p class="card__ratings">
      <span class="card__footer-value">4.9</span>
      <span class="card__footer-text">rating (21)</span>
    </p>
    <a href="#" class="btn btn--green btn--small">Details</a>
  </div>
</div>

แก้ไข overview.html ดังต่อไปนี้

...
    <main class="main">
      <div class="card-container">{%CARDS%}</div>
    </main>
...

ลบทุกๆ <div class=”card”>…</div>

แก้ไข tourCard.js โดยทำให้เป็น Template
<div class="card">
  <div class="card__header">
    <div class="card__picture">
      <div class="card__picture-overlay">&nbsp;</div>
      <img
        src="img/tours/{%imageCover%}"
        alt="Tour 1"
        class="card__picture-img"
      />
    </div>

    <h3 class="heading-tertirary">
      <span>{%name%}</span>
    </h3>
  </div>

  <div class="card__details">
    <h4 class="card__sub-heading">{%difficulty%} {%%duration%}-day tour</h4>
    <p class="card__text">{%summary%}</p>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-map-pin"></use>
      </svg>
      <span>Banff, Canada</span>
    </div>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-calendar"></use>
      </svg>
      <span>April 2021</span>
    </div>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-flag"></use>
      </svg>
      <span>3 stops</span>
    </div>
    <div class="card__data">
      <svg class="card__icon">
        <use xlink:href="img/icons.svg#icon-user"></use>
      </svg>
      <span>25 people</span>
    </div>
  </div>

  <div class="card__footer">
    <p>
      <span class="card__footer-value">${%price%}</span>
      <span class="card__footer-text">per person</span>
    </p>
    <p class="card__ratings">
      <span class="card__footer-value">{%ratingsAverage%}</span>
      <span class="card__footer-text">rating ({%ratingsQuantity%})</span>
    </p>
    <a href="#" class="btn btn--green btn--small">Details</a>
  </div>
</div>
แก้ไขไฟล์ homeController.js ดังต่อไปนี้
const fs = require("fs");
const path = require("path");
const Tour = require("../models/tourModel");

const tourCardHTML = fs.readFileSync(
  path.join(__dirname, "../", "templates", "card.html"),
  "utf-8"
);

const overviewHTML = fs.readFileSync(
  path.join(__dirname, "../", "templates", "overview.html"),
  "utf-8"
);

const tourHTML = fs.readFileSync(
  path.join(__dirname, "../", "templates", "tour.html"),
  "utf-8"
);

const formatDate = (date) => {
  const [datePart, timePart] = date.split(",");
  const [year, month, day] = datePart.split("-").map(Number);
  const [hour, minute] = timePart.split(":").map(Number);
  const newDate = new Date(year, month - 1, day, hour, minute);

  const options = { year: "numeric", month: "long" };
  return new Intl.DateTimeFormat("en-US", options).format(newDate);
};

const replaceTemplate = (temp, tour) => {
  // replace template placeholders with actual data
  let output = temp.replace(/{%name%}/g, tour.name);
  output = output.replace(/{%imageCover%}/g, tour.imageCover);
  output = output.replace(/{%price%}/g, tour.price);
  output = output.replace(/{%difficulty%}/g, tour.difficulty);
  output = output.replace(/{%duration%}/g, tour.duration);
  output = output.replace(/{%summary%}/g, tour.summary);
  output = output.replace(/{%ratingsAverage%}/g, tour.ratingsAverage);
  output = output.replace(/{%ratingsQuantity%}/g, tour.ratingsQuantity);
  output = output.replace(/{%maxGroupSize%}/g, tour.maxGroupSize);
  output = output.replace(
    /{%startDates%}/g,
    tour.startDates[0] ? formatDate(tour.startDates[0]) : "N/A"
  );
  output = output.replace(/{%id%}/g, tour.id);
  return output;
};

exports.getOverview = async (req, res) => {
  try {
    const tours = await Tour.find();
    const cardsHtml = tours
      .map((tour) => replaceTemplate(tourCardHTML, tour))
      .join("");
    const outputHtml = overviewHTML.replace("{%CARDS%}", cardsHtml);
    res.status(200).send(outputHtml);
  } catch (err) {
    res.status(500).json({
      status: "error",
      message: "Server error",
    });
  }
};

exports.getTour = async (req, res) => {
  try {
    const tour = await Tour.findById(req.params.id);
    if (!tour) {
      return res.status(404).json({
        status: "fail",
        message: "Tour not found",
      });
    }
    const outputHtml = replaceTemplate(tourHTML, tour);
    res.status(200).send(outputHtml);
  } catch (err) {
    res.status(500).json({
      status: "error",
      message: "Server error",
    });
  }
};

ให้นักศึกษาทำงานที่เหลือต่อให้เสร็จเช่น เมื่อผู้ใช้คลิกปุ่ม “details” ต้องทำอย่างไร

Dr.Watcharin Sarachai
Dr.Watcharin Sarachai

Hello! I’m Watcharin Sarachai, an enthusiastic and dedicated educator with a passion for advancing the field of computer science. I hold a Ph.D. in Computer Science from Chiang Mai University, where I focused on cutting-edge research in Machine Learning, Computer Programming, and Embedded Systems.

Articles: 30