diff --git a/src/app/page.jsx b/src/app/page.jsx
deleted file mode 100644
index 35c685a..0000000
--- a/src/app/page.jsx
+++ /dev/null
@@ -1,328 +0,0 @@
-'use client'
-
-import './order/Order.css';
-import {useEffect, useState} from "react";
-import OrderButton from "@/app/components/order/OrderButton.jsx";
-import Timeline from "@/app/components/Timeline.jsx";
-import ErrorMessage from "@/app/components/ErrorMessage.jsx";
-import WithSystemCheck from "./WithSystemCheck.jsx";
-import {getDateFromTimeSlot} from "@/lib/time";
-
-const EVERY_X_SECONDS = 60;
-
-const Food = ({food, className, onClick}) => {
- return (
-
-
- {food.price}€ {food.name}
-
-
- {food.dietary && {food.dietary}}
- {food.type}
-
-
- );
-};
-
-const FloatingIslandElement = ({content, title}) => {
- return (
-
- )
-}
-
-
-const PizzaIngredientsTable = () => {
- const tableCellClass = "border border-gray-300 px-4 py-2";
- const headerCellClass = `bg-gray-200 ${tableCellClass}`;
-
- const pizzas = [
- {name: "Salami", ingredients: ["Cheese 🧀", "Tomato Sauce 🍅", "Salami 🍕"]},
- {name: "Ham and mushrooms", ingredients: ["Cheese 🧀", "Tomato Sauce 🍅", "Ham 🥓", "Mushrooms 🍄"]},
- {
- name: "Capriccosa",
- ingredients: ["Cheese 🧀", "Tomato Sauce 🍅", "Mushrooms 🍄", "Artichokes 🌱", "Olives 🫒", "Ham 🥓", "Basil 🌿"]
- },
- {name: "Margherita", ingredients: ["Cheese 🧀", "Tomato Sauce 🍅", "Basil 🌿"]},
- {
- name: "Veggies",
- ingredients: ["Cheese 🧀", "Tomato Sauce 🍅", "Mushrooms 🍄", "Onions 🧅", "Green Peppers 🫑", "Olives 🫒"]
- },
- {name: "Margherita vegan", ingredients: ["Vegan Cheese 🧀", "Tomato Sauce 🍅", "Basil 🌿"]},
- {
- name: "Capriccosa vegan",
- ingredients: ["Vegan Cheese 🧀", "Tomato Sauce 🍅", "Mushrooms 🍄", "Artichokes 🌱", "Olives 🫒", "Basil 🌿"]
- }
- ];
-
- return (
-
- );
-};
-
-// Order component
-const Page = () => {
- // State to hold the order
- const [error, setError] = useState('');
- const [foods, setFoods] = useState([]);
- const [order, setOrder] = useState({name: '', items: [], comment: '', timeslot: null});
-
- // Set the start and end date for the timeline
- const start = new Date();
- start.setHours(start.getHours() - 1); // Previous hour
- start.setMinutes(0, 0, 0);
-
- const end = new Date();
- end.setHours(end.getHours() + 1); // Next hour
- end.setMinutes(59, 59, 999);
-
- // Fetch the pizza menu from the server
- useEffect(() => {
- // Fetch the pizza menu from the server
- fetch("/api/pizza")
- .then(async response => {
- const data = await response.json();
- if (!response.ok) {
- const error = (data && data.message) || response.statusText;
- throw new Error(error);
- }
- return data;
- })
- .then(data => {
- setFoods(data);
- })
- .catch(error => {
- console.error('There was an error!', error);
- setError(error.message);
- });
- }, []);
-
- /**
- * Update the order with the new values
- * @param updatedOrder
- */
- const updateOrder = (updatedOrder) => {
- setOrder({...order, ...updatedOrder});
- };
-
- /**
- * Add food to the order
- * @param food
- */
- const addToOrder = (food) => {
- setError('');
- const newOrder = [...order.items];
- newOrder.push(food);
- updateOrder({items: newOrder});
- }
-
- /**
- * Remove food from the order
- * @param index
- */
- const removeFromOrder = (index) => {
- setError('');
- const newOrder = [...order.items];
- newOrder.splice(index, 1);
- updateOrder({items: newOrder});
- }
-
- /**
- * Set the timeslot of the order
- */
- const setTimeslot = (timeslot) => {
- // Check if the timeslot is not in the past
-
- const BUFFER = 10;
-
- // Get time with buffer
- const currentTime = new Date();
- currentTime.setMinutes(currentTime.getMinutes() + BUFFER);
-
- const timeslotTime = getDateFromTimeSlot(timeslot).toDate()
-
- if (timeslotTime < currentTime) {
- setError('You cannot choose a timeslot in the past.');
- setTimeout(() => setError(''), 5000);
- return;
- }
-
- updateOrder({timeslot: timeslot});
- }
-
- /**
- * Set the name of the order
- * @param name
- */
- const setName = (name) => {
- updateOrder({name: name});
- };
-
- /**
- * Set the comment of the order
- */
- const setComment = (comment) => {
- updateOrder({comment: comment});
- }
-
-
- return (
-
-
-
Order your pizza at Sommerfest 2024!
-
-
- -
-
Choose Pizza:
- Select whole or halved from the list below (a whole pizza has a diameter of 12 inches / 30 cm).
-
- -
-
Pick-Up Time:
- Choose a time (some slots may be full).
-
- -
-
Pay in Cash:
- Pay when collecting at the counter.
-
-
-
-
Order Times:
-
Earliest pick-up: 17:25
-
Latest order: 23:40
-
-
Enjoy your evening!
-
-
-
-
-
-
-
Menu:
-
Select your pizza from the list below.
- Ingredients are at the bottom.
-
-
- {foods
- .filter(food => food.enabled)
- .map((food, index) => (
- addToOrder(food)}
- />
- ))}
- {!foods.length && Loading...
}
-
-
-
-
-
-
Your current order:
- {error &&
}
-
- {order.items
- .map((food, index) => (
- removeFromOrder(food)}
- />
- ))}
-
-
-
-
- setName(e.target.value)}
- />
-
-
-
-
-
Timeslot
-
Select your timeslot for pick-up.
-
-
-
-
-
-
-
-
-
- {/* Floating island */}
-
-
- {!error && (
- <>
-
total + item.size, 0)}/>
-
- total + item.price, 0)}€`}/>
-
-
- >)
- }
- {error && (
-
- )}
-
-
-
-
-
- );
-};
-
-export default WithSystemCheck(Page);
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..e074f4d
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,315 @@
+'use client'
+
+import './order/Order.css';
+import { useEffect, useState } from "react";
+import Timeline from "@/app/components/Timeline.jsx";
+import ErrorMessage from "@/app/components/ErrorMessage.jsx";
+import WithSystemCheck from "./WithSystemCheck.jsx";
+import { getDateFromTimeSlot } from "@/lib/time";
+import { ORDER } from "@/config";
+import { FoodDocument } from '@/model/food';
+
+const EVERY_X_SECONDS = 60;
+
+const Food = ({ food, addClick }: { food: FoodDocument, addClick: () => void }) => {
+ return (
+
+
+
+
+ {food.dietary && ({food.dietary}
+ )}
+ {food.type}
+
+
{food.price}€ {food.name}
+
+
+
+
+
+
+ {food.ingredients ? food.ingredients.join(', ') : ''}
+
+
+ );
+};
+
+const FloatingIslandElement = ({ content, title }: { content: string | number, title: string }) => {
+ return (
+
+ )
+}
+
+// Order component
+const Page = () => {
+ // State to hold the order
+ const [error, setError] = useState('');
+ const [foods, setFoods] = useState({} as { [_id: string]: FoodDocument[] });
+ const [floatingIslandOpen, setFloatingIslandOpen] = useState(false);
+
+ interface OrderType {
+ name: string;
+ items: { [_id: string]: FoodDocument[] };
+ comment: string;
+ timeslot: string | null;
+ }
+
+ const [order, setOrder] = useState
({ name: '', items: {}, comment: '', timeslot: null });
+
+ // Set the start and end date for the timeline
+ const start = new Date();
+ start.setHours(start.getHours() - 1); // Previous hour
+ start.setMinutes(0, 0, 0);
+
+ const end = new Date();
+ end.setHours(end.getHours() + 1); // Next hour
+ end.setMinutes(59, 59, 999);
+
+ // Fetch the pizza menu from the server
+ useEffect(() => {
+ // Fetch the pizza menu from the server
+ fetch("/api/pizza")
+ .then(async response => {
+ const data = await response.json();
+ if (!response.ok) {
+ const error = (data && data.message) || response.statusText;
+ throw new Error(error);
+ }
+ return data;
+ })
+ .then(data => {
+ setFoods(data);
+ })
+ .catch(error => {
+ console.error('There was an error!', error);
+ setError(error.message);
+ });
+ }, []);
+
+ /**
+ * Update the order with the new values
+ * @param updatedOrder
+ */
+ const updateOrder = (updatedOrder: Partial) => {
+ setOrder({ ...order, ...updatedOrder });
+ };
+
+ /**
+ * Add food to the order
+ * @param food
+ */
+ const addToOrder = (food: FoodDocument) => {
+ setError('');
+ const updateItems = [...order.items[food._id.toString()] ?? [], food];
+ const items = { ...order.items };
+ items[food._id.toString()] = updateItems;
+ updateOrder({ items: items });
+ }
+
+ /**
+ * Remove food from the order
+ * @param food
+ */
+ const removeFromOrder = (food: FoodDocument) => {
+ setError('');
+ const updateItems = [...order.items[food._id.toString()] ?? []];
+ updateItems.pop();
+ const items = { ...order.items };
+ updateOrder({ items: items });
+ }
+
+ /**
+ * Set the timeslot of the order
+ */
+ const setTimeslot = (timeslot: string) => {
+ // Check if the timeslot is not in the past
+ const BUFFER = ORDER.TIMESLOT_DURATION;
+
+ // Get time with buffer
+ const currentTime = new Date();
+ currentTime.setMinutes(currentTime.getMinutes() + BUFFER);
+
+ const timeslotTime = getDateFromTimeSlot(timeslot).toDate()
+
+ if (timeslotTime < currentTime) {
+ setError('You cannot choose a timeslot in the past.');
+ setTimeout(() => setError(''), 5000);
+ return;
+ }
+
+ updateOrder({ timeslot: timeslot });
+ }
+
+ /**
+ * Set the name of the order
+ * @param name
+ */
+ const setName = (name: string) => {
+ updateOrder({ name: name });
+ };
+
+ /**
+ * Set the comment of the order
+ */
+ const setComment = (comment: string) => {
+ updateOrder({ comment: comment });
+ }
+
+ /**
+ * Get the total price of the order
+ */
+ const getTotalPrice = (): number => {
+ return Object.values(order.items).reduce((total, items) => {
+ return total + items.reduce((total, item) => total + item.price, 0);
+ }, 0);
+ }
+
+ /**
+ * Get the total number of items in the order
+ */
+ const getTotalItems = (): number => {
+ return Object.values(order.items)
+ .flatMap(items => items)
+ .reduce((total, item) => total + item.size, 0);
+ }
+
+
+ return (
+
+
+
Order your pizza at Sommerfest
+ 2024!
+
+
+ -
+
Choose Pizza:
+ Select whole or halved from the list below (a whole pizza has a diameter of 12 inches /
+ 30 cm).
+
+ -
+
Pick-Up Time:
+ Choose a time (some slots may be full).
+
+ -
+
Pay in Cash:
+ Pay when collecting at the counter.
+
+
+
+
Order Times:
+
Earliest pick-up: 17:25
+
Latest order: 23:40
+
+
Enjoy your evening!
+
+
+
+
+
+
+
Menu
+
Select your pizza from the list
+ below. Ingredients are at the bottom.
+
+
+ {Object.entries(foods)
+ .flatMap(([_, foods]) => foods)
+ .filter(food => food.enabled)
+ .map((food, index) => (
+ addToOrder(food)}
+ />
+ ))}
+ {!foods.length && Loading...
}
+
+
+
+
+
+
Your current order
+ {error &&
}
+
+
+
+ setName(e.target.value)}
+ />
+
+
+
+
+
Timeslot
+
Select your timeslot for
+ pick-up.
+
+
+
+
+
+
+
+ {/* Floating island */}
+
+
setFloatingIslandOpen(!floatingIslandOpen)}>
+ {floatingIslandOpen && (
+
+ )}
+ {!floatingIslandOpen && !error && (
+ <>
+
+
+
+
+
+ >)
+ }
+ {error && (
+
+ )}
+
+
+
+ );
+};
+
+export default WithSystemCheck(Page);
diff --git a/src/config/index.ts b/src/config/index.ts
index 4229507..12de9b8 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -14,6 +14,7 @@ export const tokens = {
export const ORDER = {
MAX_ITEMS_PER_TIMESLOT: 4,
+ TIMESLOT_DURATION: 10,
}
export const FOOD = {
diff --git a/src/model/food.ts b/src/model/food.ts
index 7da5d4a..211ecaf 100644
--- a/src/model/food.ts
+++ b/src/model/food.ts
@@ -1,56 +1,42 @@
-// Pizza model
-import { type Document, Model, model, Schema } from "mongoose"
+import { Schema, model, Document, Model, Types } from "mongoose";
import { FOOD } from "@/config";
-export interface FoodDocument extends Document {
- _id: string;
- name: string;
- price: number;
- // Type of food
- type: string;
- // Dietary requirements
- dietary?: string;
- // Size, e.g., 0.5 for half a pizza
- size: number
- // Maximum number of items available
- max: number;
- enabled: boolean;
- createdAt: Date;
+export interface Food {
+ name: string;
+ price: number;
+ type: string; // Type of food
+ dietary?: string; // Dietary requirements
+ ingredients?: string[];
+ size: number; // Size, e.g., 0.5 for half a pizza
+ max: number; // Maximum number of items available
+ enabled: boolean;
+ createdAt: Date;
}
-const foodSchema = new Schema({
+// Extend the interface to include the MongoDB `_id` field
+export interface FoodDocument extends Food, Document {
+ _id: Types.ObjectId; // Explicitly define `_id` as ObjectId
+}
+
+// Define the schema for the Food model
+const foodSchema = new Schema(
+ {
name: { type: String, required: true },
- price: {
- required: true,
- type: Number,
- default: 0,
- min: 0,
- max: 100,
- },
- type: {
- type: String,
- required: true
- },
- dietary: {
- type: String,
- required: false
- },
- size: {
- required: true,
- type: Number,
- default: 1,
- min: 0.1,
- max: 1
- },
+ price: { type: Number, required: true, default: 0, min: 0, max: 100 },
+ type: { type: String, required: true },
+ dietary: { type: String },
+ ingredients: { type: [String] },
+ size: { type: Number, required: true, default: 1, min: 0.1, max: 1 },
max: { type: Number, default: FOOD.MAX_ITEMS },
enabled: { type: Boolean, default: true },
- createdAt: { type: Date, default: Date.now }
-});
+ createdAt: { type: Date, default: Date.now },
+ },
+ {
+ timestamps: true,
+ }
+);
-let Food: Model;
-try {
- Food = model('food', foodSchema);
-} catch (error) {
- Food = model('food');
-}
-export { Food }
+// Create the Food model
+const FoodModel: Model = model("Food", foodSchema);
+
+export { FoodModel };
\ No newline at end of file
diff --git a/src/model/order.ts b/src/model/order.ts
index 5c3403d..7911d4d 100644
--- a/src/model/order.ts
+++ b/src/model/order.ts
@@ -1,169 +1,122 @@
-import { type Document, Model, model, Schema } from "mongoose";
+import { Schema, model, Document, Model, Types } from "mongoose";
import { FoodDocument } from "./food";
import { getDateFromTimeSlot } from "@/lib/time";
/**
* Represents the different statuses an order can have.
- * - `ordered`: The order has been placed and is waiting to be processed.
- * - `inPreparation`: The order is being prepared.
- * - `ready`: The order is ready to be delivered.
- * - `delivered`: The order has been delivered.
- * - `cancelled`: The order has been cancelled.
*/
-export type OrderStatus =
- 'ordered' |
- 'inPreparation' |
- 'ready' |
- 'delivered' |
- 'cancelled';
-/**
- * Array of possible order statuses.
- */
-export const ORDER_STATES: OrderStatus[] = ['ordered', 'inPreparation', 'ready', 'delivered', 'cancelled'];
-
+export type OrderStatus = "ordered" | "inPreparation" | "ready" | "delivered" | "cancelled";
+export const ORDER_STATES: OrderStatus[] = ["ordered", "inPreparation", "ready", "delivered", "cancelled"];
/**
* Represents the different statuses an item (food) can have during preparation.
- * - `prepping`: The item is being prepared.
- * - `readyToCook`: The item is ready to be cooked.
- * - `cooking`: The item is being cooked.
- * - `ready`: The item is ready.
- * - `delivered`: The item has been delivered.
- * - `cancelled`: The item has been cancelled.
*/
-export type ItemStatus =
- 'prepping' |
- 'readyToCook' |
- 'cooking' |
- 'ready' |
- 'delivered' |
- 'cancelled';
+export type ItemStatus = "prepping" | "readyToCook" | "cooking" | "ready" | "delivered" | "cancelled";
+export const ITEM_STATES: ItemStatus[] = ["prepping", "readyToCook", "cooking", "ready", "delivered", "cancelled"];
/**
- * Array of possible item statuses.
+ * Order item interface.
*/
-export const ITEM_STATES: ItemStatus[] = ['prepping', 'readyToCook', 'cooking', 'ready', 'delivered', 'cancelled'];
+export interface OrderItem {
+ food: FoodDocument; // Reference to FoodDocument
+ status: ItemStatus;
+}
+/**
+ * Order interface.
+ */
export interface Order {
- name: string;
- comment?: string;
- items: {
- food: FoodDocument;
- status: ItemStatus;
- }[];
- orderDate: Date;
- timeslot: string;
- totalPrice: number;
- isPaid: boolean;
- status: OrderStatus;
- finishedAt?: Date;
+ name: string;
+ comment?: string;
+ items: OrderItem[];
+ orderDate: Date;
+ timeslot: string;
+ totalPrice: number;
+ isPaid: boolean;
+ status: OrderStatus;
+ finishedAt?: Date;
}
-export interface OrderWithId extends Order {
- _id: string
+/**
+ * Order document interface (extends Mongoose's Document).
+ * Includes the `_id` field explicitly.
+ */
+export interface OrderDocument extends Order, Document {
+ _id: Types.ObjectId; // Explicitly include the `_id` field
}
/**
- * Represents an order.
+ * Order schema definition.
*/
-export interface OrderDocument extends Order, Document {}
-
-const orderSchema = new Schema({
- name: {
- type: String,
- required: true,
- },
- comment: {
- type: String,
- required: false,
- },
+const orderSchema = new Schema(
+ {
+ name: { type: String, required: true },
+ comment: { type: String },
items: [
- {
- food: {
- type: Schema.Types.ObjectId,
- ref: 'food',
- required: true,
- },
- status: {
- type: String,
- enum: ITEM_STATES,
- default: ITEM_STATES[0],
- },
- },
+ {
+ food: { type: Schema.Types.ObjectId, ref: "food", required: true },
+ status: { type: String, enum: ITEM_STATES, default: ITEM_STATES[0] },
+ },
],
- orderDate: {
- type: Date,
- default: Date.now,
- },
- timeslot: {
- type: String,
- required: true,
- },
- totalPrice: {
- type: Number,
- required: true,
- min: 0,
- },
- isPaid: {
- type: Boolean,
- default: false,
- },
- status: {
- type: String,
- enum: ORDER_STATES,
- default: ORDER_STATES[0],
- },
- finishedAt: {
- type: Date,
- },
-}, {
- timestamps: true,
-});
+ orderDate: { type: Date, default: Date.now },
+ timeslot: { type: String, required: true },
+ totalPrice: { type: Number, required: true, min: 0 },
+ isPaid: { type: Boolean, default: false },
+ status: { type: String, enum: ORDER_STATES, default: ORDER_STATES[0] },
+ finishedAt: { type: Date },
+ },
+ {
+ timestamps: true, // Automatically adds `createdAt` and `updatedAt` fields
+ }
+);
-// Middleware to set finishedAt when the order is marked as finished
-orderSchema.pre('save', function (next) {
- if (this.status === 'delivered' && !this.finishedAt) {
- this.finishedAt = new Date();
- }
- next();
+/**
+ * Middleware to set `finishedAt` when the order is marked as delivered.
+ */
+orderSchema.pre("save", function (next) {
+ if (this.status === "delivered" && !this.finishedAt) {
+ this.finishedAt = new Date();
+ }
+ next();
});
-// Middleware
-orderSchema.pre('save', function (next) {
- const allStatuses = this.items.map(item => item.status);
- const uniqueStatuses = new Set(allStatuses);
+/**
+ * Middleware to update order status based on item statuses.
+ */
+orderSchema.pre("save", function (next) {
+ const allStatuses = this.items.map((item) => item.status);
+ const uniqueStatuses = new Set(allStatuses);
+
+ // If the order is cancelled or delivered, set all items to the same status
+ if (this.status === "cancelled" || this.status === "delivered") {
+ this.items.forEach((item) => {
+ item.status = this.status as ItemStatus; // Safe cast
+ });
+ return next();
+ }
- // If the order is cancelled or delivered, set all items to the same status
- if (this.status === 'cancelled' || this.status === 'delivered') {
- this.items.forEach(item => {
- // This is safe because item.status can be 'cancelled' or 'delivered'
- item.status = this.status as ItemStatus;
- });
- return next();
- }
+ // Update the order status based on the statuses of the items
+ if (uniqueStatuses.has("prepping")) {
+ this.status = "ordered";
+ } else if (uniqueStatuses.size === 1 && uniqueStatuses.has("readyToCook")) {
+ this.status = "inPreparation";
+ } else if (uniqueStatuses.size === 1 && uniqueStatuses.has("cooking")) {
+ this.status = "inPreparation";
- // Set the status of the order based on the status of the items
- if (uniqueStatuses.has('prepping')) {
- this.status = 'ordered';
- } else if (uniqueStatuses.size === 1 && uniqueStatuses.has('readyToCook')) {
- this.status = 'inPreparation';
- } else if (uniqueStatuses.size === 1 && uniqueStatuses.has('cooking')) {
- this.status = 'inPreparation';
- if (getDateFromTimeSlot(this.timeslot).toDate().getTime() <= new Date().getTime()) {
- this.status = 'ready';
- }
- } else if (uniqueStatuses.size === 1 && uniqueStatuses.has('ready')) {
- this.status = 'ready';
+ // If the timeslot has passed, mark the order as ready
+ if (getDateFromTimeSlot(this.timeslot).toDate().getTime() <= new Date().getTime()) {
+ this.status = "ready";
}
+ } else if (uniqueStatuses.size === 1 && uniqueStatuses.has("ready")) {
+ this.status = "ready";
+ }
- next();
+ next();
});
+/**
+ * Order model.
+ */
+const OrderModel: Model = model("order", orderSchema);
-let Order: Model;
-try {
- Order = model('order', orderSchema);
-} catch (error) {
- Order = model('order');
-}
-export { Order }
+export { OrderModel };
\ No newline at end of file