Week 12

Assignment Backend

Node.js Can be used for your assignment backend to serve event data and accept feedback.

import express from "express";
import cors from "cors";

import { fileURLToPath } from "url";
import path, { dirname } from "path";
import fs from "fs/promises";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

let experiences = [];
let feedbacks = [];

/**
 * Fetch data from JSON file and store in memory (simulating a database)
 */
try {
    const data = await fs.readFile(
        path.join(__dirname, "data", "experiences.json"),
        "utf-8"
    );
    experiences = JSON.parse(data);

    console.log("Experiences loaded successfully");
} catch (error) {
    console.error("Error loading experiences:", error);
    process.exit(1);
}

try {
    const data = await fs.readFile(
        path.join(__dirname, "data", "feedback.json"),
        "utf-8"
    );
    feedbacks = JSON.parse(data);

    console.log("Feedback loaded successfully");
} catch (error) {
    console.error("Error loading feedback:", error);
    process.exit(1);
}


const app = express();
const PORT = 3000;

// middleware
app.use(cors());
app.use(express.json());

/**
 * Submits feedback for a specific experience
 * @param {Number} expid 
 * @param {String} name 
 * @param {String} feedback 
 */
function submitFeedback(expid, name, feedback) {
    // Replicate a database ID auto-increment based on array length
    let id = feedbacks.length > 0 ? feedbacks[feedbacks.length - 1].id + 1 : 999; 

    // Store feedback in memory
    feedbacks.push({ id, name, expid, feedback });

    console.log("Feedback submitted:", { id, name, feedback });
}

// endpoints
app.get("/", (req, res) => {
    res.sendFile(path.join(__dirname, 'index.html'));
});

app.get("/api/experiences", (req, res) => {
    console.log("backend: /api/experiences called");

    res.json(experiences);
});

app.get("/api/feedback/", (req, res) => {
    console.log("backend: /api/feedback called");

    res.json(feedbacks);
});

// POST method
app.post("/api/feedback/", (req, res) => {
    console.log("backend: /api/feedback POST called with data:", req.body);
    const { id, name, feedback } = req.body;

    // Basic validation
    if (!id || !name || !feedback) {
        return res.status(400).json({ error: "Missing required fields" });
    }
    else if (typeof id !== "number" || typeof name !== "string" || typeof feedback !== "string") {
        return res.status(400).json({ error: "Invalid data types" });
    }
    else if (experiences.find(exp => exp.id === id) === undefined) {
        return res.status(400).json({ error: "Experience with given ID does not exist" });
    }
    else if (name.trim() === "" || feedback.trim() === "") {
        return res.status(400).json({ error: "Name and feedback cannot be empty" });
    }

    submitFeedback(id, name, feedback);

    res.json({ result: "Feedback submitted successfully" });
});

// Start server
app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});
                

JSON file

JSON files can be used to store data persistently for the API to serve.

experiences.json
[
    {
        "id": 1,
        "title": "Kapa Haka",
        "description": "Traditional Māori performance",
        "image": "kapa-haka.jpg"
    },
    {
        "id": 2,
        "title": "Wood Carving",
        "description": "Workshop teaching authentic woodcarving techniques",
        "image": "wood-carving.jpg"
    },
    {
        "id": 3,
        "title": "Hangi",
        "description": "Māori earth oven cooked meal",
        "image": "hangi.jpg"
    }
]
                
feedback.json
[
    {
        "id": 1,
        "name": "Jesse Schollitt",
        "experienceId": 1,
        "message": "Amazing performance and atmosphere"
    }, 
    {
        "id": 2,
        "name": "John Smith",
        "experienceId": 2,
        "message": "Enjoyed every minute! A must attend event"
    }, 
    {
        "id": 3,
        "name": "James McAvon",
        "experienceId": 2,
        "message": "Entertainment was okay, but way too long"
    }
]
                

React + API

Backend interaction can be performed inside a React component.

API Demo Component

import { useState } from "react";
import ExperienceCard from "./ExperienceCard";

const API_URL = "http://localhost:3000/api/experiences";


function APIDemo() {
    const [loading, setLoading] = useState(false);
    const [data, setData] = useState([]);

    async function fetchData() {
        setLoading(true);
        fetch(API_URL)
            .then(res => res.json())
            .then(resJson => setData(resJson))
            .then(() => {setLoading(false)})
            .catch(error => console.log(error));
    }

    return (
        <div className="container mt-2">
            <button
                className="btn btn-primary mb-4"
                onClick={fetchData}
            >
                Load Experiences
            </button>

            { loading ? ( <h3>Loading...</h3> ) : (
                data.length > 0 && (
                <>
                    <p className="text-muted mb-2">
                        {data.length} data elements loaded.
                    </p>
                    {data.map((element, index) => (
                        <ExperienceCard key={index} details={element} />
                    ))}
                </>
            )
            ) }
        </div>
    );
}

export default APIDemo;

ExperienceCard Component

import 'bootstrap/dist/css/bootstrap.min.css';

function ExperienceCard({ details }) {

    return (
        <div className="card mb-2">
            <div className="card-body">

                <div className="d-flex justify-content-between mb-1">
                    <span className="fw-medium">{details.title}</span>
                </div>

                <p className="card-text mb-2">{details.description}</p>
                <img src={"https://picsum.photos/400?random=" + details.id} width={300}/>
            </div>
        </div>
    );
}

export default ExperienceCard;