Repository: dejwid/food-ordering Branch: master Commit: 920357fedd80 Files: 69 Total size: 80.9 KB Directory structure: gitextract_j8pgupht/ ├── .eslintrc.json ├── .gitignore ├── README.md ├── jsconfig.json ├── next.config.js ├── package.json ├── postcss.config.js ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ ├── auth/ │ │ │ │ └── [...nextauth]/ │ │ │ │ └── route.js │ │ │ ├── categories/ │ │ │ │ └── route.js │ │ │ ├── checkout/ │ │ │ │ └── route.js │ │ │ ├── menu-items/ │ │ │ │ └── route.js │ │ │ ├── orders/ │ │ │ │ └── route.js │ │ │ ├── profile/ │ │ │ │ └── route.js │ │ │ ├── register/ │ │ │ │ └── route.js │ │ │ ├── upload/ │ │ │ │ └── route.js │ │ │ ├── users/ │ │ │ │ └── route.js │ │ │ └── webhook/ │ │ │ └── route.js │ │ ├── cart/ │ │ │ └── page.js │ │ ├── categories/ │ │ │ └── page.js │ │ ├── globals.css │ │ ├── layout.js │ │ ├── login/ │ │ │ └── page.js │ │ ├── menu/ │ │ │ └── page.js │ │ ├── menu-items/ │ │ │ ├── edit/ │ │ │ │ └── [id]/ │ │ │ │ └── page.js │ │ │ ├── new/ │ │ │ │ └── page.js │ │ │ └── page.js │ │ ├── orders/ │ │ │ ├── [id]/ │ │ │ │ └── page.js │ │ │ └── page.js │ │ ├── page.js │ │ ├── profile/ │ │ │ └── page.js │ │ ├── register/ │ │ │ └── page.js │ │ └── users/ │ │ ├── [id]/ │ │ │ └── page.js │ │ └── page.js │ ├── components/ │ │ ├── AppContext.js │ │ ├── DeleteButton.js │ │ ├── UseProfile.js │ │ ├── icons/ │ │ │ ├── Bars2.js │ │ │ ├── ChevronDown.js │ │ │ ├── ChevronUp.js │ │ │ ├── Left.js │ │ │ ├── Plus.js │ │ │ ├── Right.js │ │ │ ├── ShoppingCart.js │ │ │ └── Trash.js │ │ ├── layout/ │ │ │ ├── AddressInputs.js │ │ │ ├── EditableImage.js │ │ │ ├── Header.js │ │ │ ├── Hero.js │ │ │ ├── HomeMenu.js │ │ │ ├── InfoBox.js │ │ │ ├── MenuItemForm.js │ │ │ ├── MenuItemPriceProps.js │ │ │ ├── SectionHeaders.js │ │ │ ├── SuccessBox.js │ │ │ ├── UserForm.js │ │ │ └── UserTabs.js │ │ └── menu/ │ │ ├── AddToCartButton.js │ │ ├── CartProduct.js │ │ ├── MenuItem.js │ │ └── MenuItemTile.js │ ├── libs/ │ │ ├── datetime.js │ │ └── mongoConnect.js │ └── models/ │ ├── Category.js │ ├── MenuItem.js │ ├── Order.js │ ├── User.js │ └── UserInfo.js ├── tailwind.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .env .idea .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: jsconfig.json ================================================ { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ================================================ FILE: next.config.js ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [ { protocol: 'https', hostname: '*.googleusercontent.com', }, { protocol: 'https', hostname: 'dawid-food-ordering.s3.amazonaws.com', }, ] } } module.exports = nextConfig ================================================ FILE: package.json ================================================ { "name": "food-ordering-app", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@auth/mongodb-adapter": "^2.0.3", "@aws-sdk/client-s3": "^3.438.0", "bcrypt": "^5.1.1", "micro": "^10.0.1", "mongodb": "^6.2.0", "mongoose": "^7.6.3", "next": "14.0.0", "next-auth": "^4.24.4", "react": "^18", "react-dom": "^18", "react-flying-item": "^1.1.2", "react-hot-toast": "^2.4.1", "stripe": "^14.3.0", "uniqid": "^5.4.0" }, "devDependencies": { "@types/node": "20.8.9", "@types/react": "18.2.33", "autoprefixer": "^10", "eslint": "^8", "eslint-config-next": "14.0.0", "postcss": "^8", "tailwindcss": "^3", "typescript": "5.2.2" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: src/app/api/auth/[...nextauth]/route.js ================================================ import clientPromise from "@/libs/mongoConnect"; import {UserInfo} from "@/models/UserInfo"; import bcrypt from "bcrypt"; import * as mongoose from "mongoose"; import {User} from '@/models/User'; import NextAuth, {getServerSession} from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import GoogleProvider from "next-auth/providers/google"; import { MongoDBAdapter } from "@auth/mongodb-adapter" export const authOptions = { secret: process.env.SECRET, adapter: MongoDBAdapter(clientPromise), providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: 'Credentials', id: 'credentials', credentials: { username: { label: "Email", type: "email", placeholder: "test@example.com" }, password: { label: "Password", type: "password" }, }, async authorize(credentials, req) { const email = credentials?.email; const password = credentials?.password; mongoose.connect(process.env.MONGO_URL); const user = await User.findOne({email}); const passwordOk = user && bcrypt.compareSync(password, user.password); if (passwordOk) { return user; } return null } }) ], }; export async function isAdmin() { const session = await getServerSession(authOptions); const userEmail = session?.user?.email; if (!userEmail) { return false; } const userInfo = await UserInfo.findOne({email:userEmail}); if (!userInfo) { return false; } return userInfo.admin; } const handler = NextAuth(authOptions); export { handler as GET, handler as POST } ================================================ FILE: src/app/api/categories/route.js ================================================ import {isAdmin} from "@/app/api/auth/[...nextauth]/route"; import {Category} from "@/models/Category"; import mongoose from "mongoose"; export async function POST(req) { mongoose.connect(process.env.MONGO_URL); const {name} = await req.json(); if (await isAdmin()) { const categoryDoc = await Category.create({name}); return Response.json(categoryDoc); } else { return Response.json({}); } } export async function PUT(req) { mongoose.connect(process.env.MONGO_URL); const {_id, name} = await req.json(); if (await isAdmin()) { await Category.updateOne({_id}, {name}); } return Response.json(true); } export async function GET() { mongoose.connect(process.env.MONGO_URL); return Response.json( await Category.find() ); } export async function DELETE(req) { mongoose.connect(process.env.MONGO_URL); const url = new URL(req.url); const _id = url.searchParams.get('_id'); if (await isAdmin()) { await Category.deleteOne({_id}); } return Response.json(true); } ================================================ FILE: src/app/api/checkout/route.js ================================================ import {authOptions} from "@/app/api/auth/[...nextauth]/route"; import {MenuItem} from "@/models/MenuItem"; import {Order} from "@/models/Order"; import mongoose from "mongoose"; import {getServerSession} from "next-auth"; const stripe = require('stripe')(process.env.STRIPE_SK); export async function POST(req) { mongoose.connect(process.env.MONGO_URL); const {cartProducts, address} = await req.json(); const session = await getServerSession(authOptions); const userEmail = session?.user?.email; const orderDoc = await Order.create({ userEmail, ...address, cartProducts, paid: false, }); const stripeLineItems = []; for (const cartProduct of cartProducts) { const productInfo = await MenuItem.findById(cartProduct._id); let productPrice = productInfo.basePrice; if (cartProduct.size) { const size = productInfo.sizes .find(size => size._id.toString() === cartProduct.size._id.toString()); productPrice += size.price; } if (cartProduct.extras?.length > 0) { for (const cartProductExtraThing of cartProduct.extras) { const productExtras = productInfo.extraIngredientPrices; const extraThingInfo = productExtras .find(extra => extra._id.toString() === cartProductExtraThing._id.toString()); productPrice += extraThingInfo.price; } } const productName = cartProduct.name; stripeLineItems.push({ quantity: 1, price_data: { currency: 'USD', product_data: { name: productName, }, unit_amount: productPrice * 100, }, }); } const stripeSession = await stripe.checkout.sessions.create({ line_items: stripeLineItems, mode: 'payment', customer_email: userEmail, success_url: process.env.NEXTAUTH_URL + 'orders/' + orderDoc._id.toString() + '?clear-cart=1', cancel_url: process.env.NEXTAUTH_URL + 'cart?canceled=1', metadata: {orderId:orderDoc._id.toString()}, payment_intent_data: { metadata:{orderId:orderDoc._id.toString()}, }, shipping_options: [ { shipping_rate_data: { display_name: 'Delivery fee', type: 'fixed_amount', fixed_amount: {amount: 500, currency: 'USD'}, }, } ], }); return Response.json(stripeSession.url); } ================================================ FILE: src/app/api/menu-items/route.js ================================================ import {isAdmin} from "@/app/api/auth/[...nextauth]/route"; import {MenuItem} from "@/models/MenuItem"; import mongoose from "mongoose"; export async function POST(req) { mongoose.connect(process.env.MONGO_URL); const data = await req.json(); if (await isAdmin()) { const menuItemDoc = await MenuItem.create(data); return Response.json(menuItemDoc); } else { return Response.json({}); } } export async function PUT(req) { mongoose.connect(process.env.MONGO_URL); if (await isAdmin()) { const {_id, ...data} = await req.json(); await MenuItem.findByIdAndUpdate(_id, data); } return Response.json(true); } export async function GET() { mongoose.connect(process.env.MONGO_URL); return Response.json( await MenuItem.find() ); } export async function DELETE(req) { mongoose.connect(process.env.MONGO_URL); const url = new URL(req.url); const _id = url.searchParams.get('_id'); if (await isAdmin()) { await MenuItem.deleteOne({_id}); } return Response.json(true); } ================================================ FILE: src/app/api/orders/route.js ================================================ import {authOptions, isAdmin} from "@/app/api/auth/[...nextauth]/route"; import {Order} from "@/models/Order"; import mongoose from "mongoose"; import {getServerSession} from "next-auth"; export async function GET(req) { mongoose.connect(process.env.MONGO_URL); const session = await getServerSession(authOptions); const userEmail = session?.user?.email; const admin = await isAdmin(); const url = new URL(req.url); const _id = url.searchParams.get('_id'); if (_id) { return Response.json( await Order.findById(_id) ); } if (admin) { return Response.json( await Order.find() ); } if (userEmail) { return Response.json( await Order.find({userEmail}) ); } } ================================================ FILE: src/app/api/profile/route.js ================================================ import {authOptions} from "@/app/api/auth/[...nextauth]/route"; import {User} from "@/models/User"; import {UserInfo} from "@/models/UserInfo"; import mongoose from "mongoose"; import {getServerSession} from "next-auth"; export async function PUT(req) { mongoose.connect(process.env.MONGO_URL); const data = await req.json(); const {_id, name, image, ...otherUserInfo} = data; let filter = {}; if (_id) { filter = {_id}; } else { const session = await getServerSession(authOptions); const email = session.user.email; filter = {email}; } const user = await User.findOne(filter); await User.updateOne(filter, {name, image}); await UserInfo.findOneAndUpdate({email:user.email}, otherUserInfo, {upsert:true}); return Response.json(true); } export async function GET(req) { mongoose.connect(process.env.MONGO_URL); const url = new URL(req.url); const _id = url.searchParams.get('_id'); let filterUser = {}; if (_id) { filterUser = {_id}; } else { const session = await getServerSession(authOptions); const email = session?.user?.email; if (!email) { return Response.json({}); } filterUser = {email}; } const user = await User.findOne(filterUser).lean(); const userInfo = await UserInfo.findOne({email:user.email}).lean(); return Response.json({...user, ...userInfo}); } ================================================ FILE: src/app/api/register/route.js ================================================ import {User} from "@/models/User"; import bcrypt from "bcrypt"; import mongoose from "mongoose"; export async function POST(req) { const body = await req.json(); mongoose.connect(process.env.MONGO_URL); const pass = body.password; if (!pass?.length || pass.length < 5) { new Error('password must be at least 5 characters'); } const notHashedPassword = pass; const salt = bcrypt.genSaltSync(10); body.password = bcrypt.hashSync(notHashedPassword, salt); const createdUser = await User.create(body); return Response.json(createdUser); } ================================================ FILE: src/app/api/upload/route.js ================================================ import {PutObjectCommand, S3Client} from "@aws-sdk/client-s3"; import uniqid from 'uniqid'; export async function POST(req) { const data = await req.formData(); if (data.get('file')) { // upload the file const file = data.get('file'); const s3Client = new S3Client({ region: 'us-east-1', credentials: { accessKeyId: process.env.MY_AWS_ACCESS_KEY, secretAccessKey: process.env.MY_AWS_SECRET_KEY, }, }); const ext = file.name.split('.').slice(-1)[0]; const newFileName = uniqid() + '.' + ext; const chunks = []; for await (const chunk of file.stream()) { chunks.push(chunk); } const buffer = Buffer.concat(chunks); const bucket = 'dawid-food-ordering'; await s3Client.send(new PutObjectCommand({ Bucket: bucket, Key: newFileName, ACL: 'public-read', ContentType: file.type, Body: buffer, })); const link = 'https://'+bucket+'.s3.amazonaws.com/'+newFileName; return Response.json(link); } return Response.json(true); } ================================================ FILE: src/app/api/users/route.js ================================================ import {isAdmin} from "@/app/api/auth/[...nextauth]/route"; import {User} from "@/models/User"; import mongoose from "mongoose"; export async function GET() { mongoose.connect(process.env.MONGO_URL); if (await isAdmin()) { const users = await User.find(); return Response.json(users); } else { return Response.json([]); } } ================================================ FILE: src/app/api/webhook/route.js ================================================ import {Order} from "@/models/Order"; const stripe = require('stripe')(process.env.STRIPE_SK); export async function POST(req) { const sig = req.headers.get('stripe-signature'); let event; try { const reqBuffer = await req.text(); const signSecret = process.env.STRIPE_SIGN_SECRET; event = stripe.webhooks.constructEvent(reqBuffer, sig, signSecret); } catch (e) { console.error('stripe error'); console.log(e); return Response.json(e, {status: 400}); } if (event.type === 'checkout.session.completed') { console.log(event); const orderId = event?.data?.object?.metadata?.orderId; const isPaid = event?.data?.object?.payment_status === 'paid'; if (isPaid) { await Order.updateOne({_id:orderId}, {paid:true}); } } return Response.json('ok', {status: 200}); } ================================================ FILE: src/app/cart/page.js ================================================ 'use client'; import {CartContext, cartProductPrice} from "@/components/AppContext"; import Trash from "@/components/icons/Trash"; import AddressInputs from "@/components/layout/AddressInputs"; import SectionHeaders from "@/components/layout/SectionHeaders"; import CartProduct from "@/components/menu/CartProduct"; import {useProfile} from "@/components/UseProfile"; import Image from "next/image"; import {useContext, useEffect, useState} from "react"; import toast from "react-hot-toast"; export default function CartPage() { const {cartProducts,removeCartProduct} = useContext(CartContext); const [address, setAddress] = useState({}); const {data:profileData} = useProfile(); useEffect(() => { if (typeof window !== 'undefined') { if (window.location.href.includes('canceled=1')) { toast.error('Payment failed 😔'); } } }, []); useEffect(() => { if (profileData?.city) { const {phone, streetAddress, city, postalCode, country} = profileData; const addressFromProfile = { phone, streetAddress, city, postalCode, country }; setAddress(addressFromProfile); } }, [profileData]); let subtotal = 0; for (const p of cartProducts) { subtotal += cartProductPrice(p); } function handleAddressChange(propName, value) { setAddress(prevAddress => ({...prevAddress, [propName]:value})); } async function proceedToCheckout(ev) { ev.preventDefault(); // address and shopping cart products const promise = new Promise((resolve, reject) => { fetch('/api/checkout', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ address, cartProducts, }), }).then(async (response) => { if (response.ok) { resolve(); window.location = await response.json(); } else { reject(); } }); }); await toast.promise(promise, { loading: 'Preparing your order...', success: 'Redirecting to payment...', error: 'Something went wrong... Please try again later', }) } if (cartProducts?.length === 0) { return (

Your shopping cart is empty 😔

); } return (
{cartProducts?.length === 0 && (
No products in your shopping cart
)} {cartProducts?.length > 0 && cartProducts.map((product, index) => ( ))}
Subtotal:
Delivery:
Total:
${subtotal}
$5
${subtotal + 5}

Checkout

); } ================================================ FILE: src/app/categories/page.js ================================================ 'use client'; import DeleteButton from "@/components/DeleteButton"; import UserTabs from "@/components/layout/UserTabs"; import {useEffect, useState} from "react"; import {useProfile} from "@/components/UseProfile"; import toast from "react-hot-toast"; export default function CategoriesPage() { const [categoryName, setCategoryName] = useState(''); const [categories, setCategories] = useState([]); const {loading:profileLoading, data:profileData} = useProfile(); const [editedCategory, setEditedCategory] = useState(null); useEffect(() => { fetchCategories(); }, []); function fetchCategories() { fetch('/api/categories').then(res => { res.json().then(categories => { setCategories(categories); }); }); } async function handleCategorySubmit(ev) { ev.preventDefault(); const creationPromise = new Promise(async (resolve, reject) => { const data = {name:categoryName}; if (editedCategory) { data._id = editedCategory._id; } const response = await fetch('/api/categories', { method: editedCategory ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); setCategoryName(''); fetchCategories(); setEditedCategory(null); if (response.ok) resolve(); else reject(); }); await toast.promise(creationPromise, { loading: editedCategory ? 'Updating category...' : 'Creating your new category...', success: editedCategory ? 'Category updated' : 'Category created', error: 'Error, sorry...', }); } async function handleDeleteClick(_id) { const promise = new Promise(async (resolve, reject) => { const response = await fetch('/api/categories?_id='+_id, { method: 'DELETE', }); if (response.ok) { resolve(); } else { reject(); } }); await toast.promise(promise, { loading: 'Deleting...', success: 'Deleted', error: 'Error', }); fetchCategories(); } if (profileLoading) { return 'Loading user info...'; } if (!profileData.admin) { return 'Not an admin'; } return (
setCategoryName(ev.target.value)} />

Existing categories

{categories?.length > 0 && categories.map(c => (
{c.name}
handleDeleteClick(c._id)} />
))}
); } ================================================ FILE: src/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; section.hero{ @apply block md:grid; grid-template-columns: .4fr .6fr; } select, input[type="email"], input[type="password"], input[type="tel"], input[type="text"] { @apply block w-full mb-2 rounded-xl; @apply border p-2 border-gray-300 bg-gray-100; } input[type="email"]:disabled, input[type="password"]:disabled, input[type="tel"]:disabled, input[type="text"]:disabled { @apply bg-gray-300 border-0 cursor-not-allowed text-gray-500; } label{ @apply text-gray-500 text-sm leading-tight; } label + input{ margin-top: -2px; } button, .button{ @apply flex w-full justify-center gap-2 text-gray-700 font-semibold; @apply border border-gray-300 rounded-xl px-6 py-2; } button[type="submit"], .submit, button.primary{ @apply border-primary bg-primary text-white; } button[type="submit"]:disabled, .submit:disabled{ @apply cursor-not-allowed bg-red-400; } div.tabs > * { @apply bg-gray-300 text-gray-700 rounded-full py-2 px-4; } div.tabs > *.active{ @apply bg-primary text-white; } .flying-button-parent button{ @apply border-primary bg-primary text-white rounded-full; } ================================================ FILE: src/app/layout.js ================================================ import {AppProvider} from "@/components/AppContext"; import Header from "@/components/layout/Header"; import { Roboto } from 'next/font/google' import './globals.css' import {Toaster} from "react-hot-toast"; const roboto = Roboto({ subsets: ['latin'], weight: ['400', '500', '700'] }) export const metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children }) { return (
{children}
) } ================================================ FILE: src/app/login/page.js ================================================ 'use client'; import {signIn} from "next-auth/react"; import Image from "next/image"; import {useState} from "react"; export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loginInProgress, setLoginInProgress] = useState(false); async function handleFormSubmit(ev) { ev.preventDefault(); setLoginInProgress(true); await signIn('credentials', {email, password, callbackUrl: '/'}); setLoginInProgress(false); } return (

Login

setEmail(ev.target.value)} /> setPassword(ev.target.value)}/>
or login with provider
); } ================================================ FILE: src/app/menu/page.js ================================================ 'use client'; import SectionHeaders from "@/components/layout/SectionHeaders"; import MenuItem from "@/components/menu/MenuItem"; import {useEffect, useState} from "react"; export default function MenuPage() { const [categories, setCategories] = useState([]); const [menuItems, setMenuItems] = useState([]); useEffect(() => { fetch('/api/categories').then(res => { res.json().then(categories => setCategories(categories)) }); fetch('/api/menu-items').then(res => { res.json().then(menuItems => setMenuItems(menuItems)); }); }, []); return (
{categories?.length > 0 && categories.map(c => (
{menuItems.filter(item => item.category === c._id).map(item => ( ))}
))}
); } ================================================ FILE: src/app/menu-items/edit/[id]/page.js ================================================ 'use client'; import DeleteButton from "@/components/DeleteButton"; import Left from "@/components/icons/Left"; import EditableImage from "@/components/layout/EditableImage"; import MenuItemForm from "@/components/layout/MenuItemForm"; import UserTabs from "@/components/layout/UserTabs"; import {useProfile} from "@/components/UseProfile"; import Link from "next/link"; import {redirect, useParams} from "next/navigation"; import {useEffect, useState} from "react"; import toast from "react-hot-toast"; export default function EditMenuItemPage() { const {id} = useParams(); const [menuItem, setMenuItem] = useState(null); const [redirectToItems, setRedirectToItems] = useState(false); const {loading, data} = useProfile(); useEffect(() => { fetch('/api/menu-items').then(res => { res.json().then(items => { const item = items.find(i => i._id === id); setMenuItem(item); }); }) }, []); async function handleFormSubmit(ev, data) { ev.preventDefault(); data = {...data, _id:id}; const savingPromise = new Promise(async (resolve, reject) => { const response = await fetch('/api/menu-items', { method: 'PUT', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }); if (response.ok) resolve(); else reject(); }); await toast.promise(savingPromise, { loading: 'Saving this tasty item', success: 'Saved', error: 'Error', }); setRedirectToItems(true); } async function handleDeleteClick() { const promise = new Promise(async (resolve, reject) => { const res = await fetch('/api/menu-items?_id='+id, { method: 'DELETE', }); if (res.ok) resolve(); else reject(); }); await toast.promise(promise, { loading: 'Deleting...', success: 'Deleted', error: 'Error', }); setRedirectToItems(true); } if (redirectToItems) { return redirect('/menu-items'); } if (loading) { return 'Loading user info...'; } if (!data.admin) { return 'Not an admin.'; } return (
Show all menu items
); } ================================================ FILE: src/app/menu-items/new/page.js ================================================ 'use client'; import Left from "@/components/icons/Left"; import Right from "@/components/icons/Right"; import EditableImage from "@/components/layout/EditableImage"; import MenuItemForm from "@/components/layout/MenuItemForm"; import UserTabs from "@/components/layout/UserTabs"; import {useProfile} from "@/components/UseProfile"; import Link from "next/link"; import {redirect} from "next/navigation"; import {useState} from "react"; import toast from "react-hot-toast"; export default function NewMenuItemPage() { const [redirectToItems, setRedirectToItems] = useState(false); const {loading, data} = useProfile(); async function handleFormSubmit(ev, data) { ev.preventDefault(); const savingPromise = new Promise(async (resolve, reject) => { const response = await fetch('/api/menu-items', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }); if (response.ok) resolve(); else reject(); }); await toast.promise(savingPromise, { loading: 'Saving this tasty item', success: 'Saved', error: 'Error', }); setRedirectToItems(true); } if (redirectToItems) { return redirect('/menu-items'); } if (loading) { return 'Loading user info...'; } if (!data.admin) { return 'Not an admin.'; } return (
Show all menu items
); } ================================================ FILE: src/app/menu-items/page.js ================================================ 'use client'; import Right from "@/components/icons/Right"; import UserTabs from "@/components/layout/UserTabs"; import {useProfile} from "@/components/UseProfile"; import Image from "next/image"; import Link from "next/link"; import {useEffect, useState} from "react"; export default function MenuItemsPage() { const [menuItems, setMenuItems] = useState([]); const {loading, data} = useProfile(); useEffect(() => { fetch('/api/menu-items').then(res => { res.json().then(menuItems => { setMenuItems(menuItems); }); }) }, []); if (loading) { return 'Loading user info...'; } if (!data.admin) { return 'Not an admin.'; } return (
Crete new menu item

Edit menu item:

{menuItems?.length > 0 && menuItems.map(item => (
{''}
{item.name}
))}
); } ================================================ FILE: src/app/orders/[id]/page.js ================================================ 'use client'; import {CartContext, cartProductPrice} from "@/components/AppContext"; import AddressInputs from "@/components/layout/AddressInputs"; import SectionHeaders from "@/components/layout/SectionHeaders"; import CartProduct from "@/components/menu/CartProduct"; import {useParams} from "next/navigation"; import {useContext, useEffect, useState} from "react"; export default function OrderPage() { const {clearCart} = useContext(CartContext); const [order, setOrder] = useState(); const [loadingOrder, setLoadingOrder] = useState(true); const {id} = useParams(); useEffect(() => { if (typeof window.console !== "undefined") { if (window.location.href.includes('clear-cart=1')) { clearCart(); } } if (id) { setLoadingOrder(true); fetch('/api/orders?_id='+id).then(res => { res.json().then(orderData => { setOrder(orderData); setLoadingOrder(false); }); }) } }, []); let subtotal = 0; if (order?.cartProducts) { for (const product of order?.cartProducts) { subtotal += cartProductPrice(product); } } return (

Thanks for your order.

We will call you when your order will be on the way.

{loadingOrder && (
Loading order...
)} {order && (
{order.cartProducts.map(product => ( ))}
Subtotal: ${subtotal}
Delivery: $5
Total: ${subtotal + 5}
)}
); } ================================================ FILE: src/app/orders/page.js ================================================ 'use client'; import SectionHeaders from "@/components/layout/SectionHeaders"; import UserTabs from "@/components/layout/UserTabs"; import {useProfile} from "@/components/UseProfile"; import {dbTimeForHuman} from "@/libs/datetime"; import Link from "next/link"; import {useEffect, useState} from "react"; export default function OrdersPage() { const [orders, setOrders] = useState([]); const [loadingOrders, setLoadingOrders] = useState(true); const {loading, data:profile} = useProfile(); useEffect(() => { fetchOrders(); }, []); function fetchOrders() { setLoadingOrders(true); fetch('/api/orders').then(res => { res.json().then(orders => { setOrders(orders.reverse()); setLoadingOrders(false); }) }) } return (
{loadingOrders && (
Loading orders...
)} {orders?.length > 0 && orders.map(order => (
{order.paid ? 'Paid' : 'Not paid'}
{order.userEmail}
{dbTimeForHuman(order.createdAt)}
{order.cartProducts.map(p => p.name).join(', ')}
Show order
))}
); } ================================================ FILE: src/app/page.js ================================================ import Header from "@/components/layout/Header"; import Hero from "@/components/layout/Hero"; import HomeMenu from "@/components/layout/HomeMenu"; import SectionHeaders from "@/components/layout/SectionHeaders"; export default function Home() { return ( <>

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magni minima odit recusandae. Illum ipsa non repudiandae? Eum ipsam iste quos suscipit tempora? Aperiam esse fugiat inventore laboriosam officiis quam rem!

At consectetur delectus ducimus est facere iure molestias obcaecati quaerat vitae voluptate? Aspernatur dolor explicabo iste minus molestiae pariatur provident quibusdam saepe?

Laborum molestias neque nulla obcaecati odio quia quod reprehenderit sit vitae voluptates? Eos, tenetur.

+46 738 123 123
) } ================================================ FILE: src/app/profile/page.js ================================================ 'use client'; import EditableImage from "@/components/layout/EditableImage"; import InfoBox from "@/components/layout/InfoBox"; import SuccessBox from "@/components/layout/SuccessBox"; import UserForm from "@/components/layout/UserForm"; import UserTabs from "@/components/layout/UserTabs"; import {useSession} from "next-auth/react"; import Image from "next/image"; import Link from "next/link"; import {redirect} from "next/navigation"; import {useEffect, useState} from "react"; import toast from "react-hot-toast"; export default function ProfilePage() { const session = useSession(); const [user, setUser] = useState(null); const [isAdmin, setIsAdmin] = useState(false); const [profileFetched, setProfileFetched] = useState(false); const {status} = session; useEffect(() => { if (status === 'authenticated') { fetch('/api/profile').then(response => { response.json().then(data => { setUser(data); setIsAdmin(data.admin); setProfileFetched(true); }) }); } }, [session, status]); async function handleProfileInfoUpdate(ev, data) { ev.preventDefault(); const savingPromise = new Promise(async (resolve, reject) => { const response = await fetch('/api/profile', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data), }); if (response.ok) resolve() else reject(); }); await toast.promise(savingPromise, { loading: 'Saving...', success: 'Profile saved!', error: 'Error', }); } if (status === 'loading' || !profileFetched) { return 'Loading...'; } if (status === 'unauthenticated') { return redirect('/login'); } return (
); } ================================================ FILE: src/app/register/page.js ================================================ "use client"; import {signIn} from "next-auth/react"; import Image from "next/image"; import Link from "next/link"; import {useState} from "react"; export default function RegisterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [creatingUser, setCreatingUser] = useState(false); const [userCreated, setUserCreated] = useState(false); const [error, setError] = useState(false); async function handleFormSubmit(ev) { ev.preventDefault(); setCreatingUser(true); setError(false); setUserCreated(false); const response = await fetch('/api/register', { method: 'POST', body: JSON.stringify({email, password}), headers: {'Content-Type': 'application/json'}, }); if (response.ok) { setUserCreated(true); } else { setError(true); } setCreatingUser(false); } return (

Register

{userCreated && (
User created.
Now you can{' '} Login »
)} {error && (
An error has occurred.
Please try again later
)}
setEmail(ev.target.value)} /> setPassword(ev.target.value)}/>
or login with provider
Existing account?{' '} Login here »
); } ================================================ FILE: src/app/users/[id]/page.js ================================================ 'use client'; import UserForm from "@/components/layout/UserForm"; import UserTabs from "@/components/layout/UserTabs"; import {useProfile} from "@/components/UseProfile"; import {useParams} from "next/navigation"; import {useEffect, useState} from "react"; import toast from "react-hot-toast"; export default function EditUserPage() { const {loading, data} = useProfile(); const [user, setUser] = useState(null); const {id} = useParams(); useEffect(() => { fetch('/api/profile?_id='+id).then(res => { res.json().then(user => { setUser(user); }); }) }, []); async function handleSaveButtonClick(ev, data) { ev.preventDefault(); const promise = new Promise(async (resolve, reject) => { const res = await fetch('/api/profile', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({...data,_id:id}), }); if (res.ok) resolve(); else reject(); }); await toast.promise(promise, { loading: 'Saving user...', success: 'User saved', error: 'An error has occurred while saving the user', }); } if (loading) { return 'Loading user profile...'; } if (!data.admin) { return 'Not an admin'; } return (
); } ================================================ FILE: src/app/users/page.js ================================================ 'use client'; import UserTabs from "@/components/layout/UserTabs"; import {useProfile} from "@/components/UseProfile"; import Link from "next/link"; import {useEffect, useState} from "react"; export default function UsersPage() { const [users, setUsers] = useState([]); const {loading,data} = useProfile(); useEffect(() => { fetch('/api/users').then(response => { response.json().then(users => { setUsers(users); }); }) }, []); if (loading) { return 'Loading user info...'; } if (!data.admin) { return 'Not an admin'; } return (
{users?.length > 0 && users.map(user => (
{!!user.name && ({user.name})} {!user.name && (No name)}
{user.email}
Edit
))}
); } ================================================ FILE: src/components/AppContext.js ================================================ 'use client'; import {SessionProvider} from "next-auth/react"; import {createContext, useEffect, useState} from "react"; import toast from "react-hot-toast"; export const CartContext = createContext({}); export function cartProductPrice(cartProduct) { let price = cartProduct.basePrice; if (cartProduct.size) { price += cartProduct.size.price; } if (cartProduct.extras?.length > 0) { for (const extra of cartProduct.extras) { price += extra.price; } } return price; } export function AppProvider({children}) { const [cartProducts,setCartProducts] = useState([]); const ls = typeof window !== 'undefined' ? window.localStorage : null; useEffect(() => { if (ls && ls.getItem('cart')) { setCartProducts( JSON.parse( ls.getItem('cart') ) ); } }, []); function clearCart() { setCartProducts([]); saveCartProductsToLocalStorage([]); } function removeCartProduct(indexToRemove) { setCartProducts(prevCartProducts => { const newCartProducts = prevCartProducts .filter((v,index) => index !== indexToRemove); saveCartProductsToLocalStorage(newCartProducts); return newCartProducts; }); toast.success('Product removed'); } function saveCartProductsToLocalStorage(cartProducts) { if (ls) { ls.setItem('cart', JSON.stringify(cartProducts)); } } function addToCart(product, size=null, extras=[]) { setCartProducts(prevProducts => { const cartProduct = {...product, size, extras}; const newProducts = [...prevProducts, cartProduct]; saveCartProductsToLocalStorage(newProducts); return newProducts; }); } return ( {children} ); } ================================================ FILE: src/components/DeleteButton.js ================================================ import {useState} from "react"; export default function DeleteButton({label,onDelete}) { const [showConfirm, setShowConfirm] = useState(false); if (showConfirm) { return (
Are you sure you want to delete?
); } return ( ); } ================================================ FILE: src/components/UseProfile.js ================================================ import {useEffect, useState} from "react"; export function useProfile() { const [data, setData] = useState(false); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/profile').then(response => { response.json().then(data => { setData(data); setLoading(false); }); }) }, []); return {loading, data}; } ================================================ FILE: src/components/icons/Bars2.js ================================================ export default function Bars2({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/ChevronDown.js ================================================ export default function ChevronDown({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/ChevronUp.js ================================================ export default function ChevronUp({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/Left.js ================================================ export default function Left({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/Plus.js ================================================ export default function Plus({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/Right.js ================================================ export default function Right({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/ShoppingCart.js ================================================ export default function ShoppingCart({className = "w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/icons/Trash.js ================================================ export default function Trash({className="w-6 h-6"}) { return ( ); } ================================================ FILE: src/components/layout/AddressInputs.js ================================================ export default function AddressInputs({addressProps,setAddressProp,disabled=false}) { const {phone, streetAddress, postalCode, city, country} = addressProps; return ( <> setAddressProp('phone', ev.target.value)} /> setAddressProp('streetAddress', ev.target.value)} />
setAddressProp('postalCode', ev.target.value)} />
setAddressProp('city', ev.target.value)} />
setAddressProp('country', ev.target.value)} /> ); } ================================================ FILE: src/components/layout/EditableImage.js ================================================ import Image from "next/image"; import toast from "react-hot-toast"; export default function EditableImage({link, setLink}) { async function handleFileChange(ev) { const files = ev.target.files; if (files?.length === 1) { const data = new FormData; data.set('file', files[0]); const uploadPromise = fetch('/api/upload', { method: 'POST', body: data, }).then(response => { if (response.ok) { return response.json().then(link => { setLink(link); }) } throw new Error('Something went wrong'); }); await toast.promise(uploadPromise, { loading: 'Uploading...', success: 'Upload complete', error: 'Upload error', }); } } return ( <> {link && ( {'avatar'} )} {!link && (
No image
)} ); } ================================================ FILE: src/components/layout/Header.js ================================================ 'use client'; import {CartContext} from "@/components/AppContext"; import Bars2 from "@/components/icons/Bars2"; import ShoppingCart from "@/components/icons/ShoppingCart"; import {signOut, useSession} from "next-auth/react"; import Link from "next/link"; import {useContext, useState} from "react"; function AuthLinks({status, userName}) { if (status === 'authenticated') { return ( <> Hello, {userName} ); } if (status === 'unauthenticated') { return ( <> Login Register ); } } export default function Header() { const session = useSession(); const status = session?.status; const userData = session.data?.user; let userName = userData?.name || userData?.email; const {cartProducts} = useContext(CartContext); const [mobileNavOpen, setMobileNavOpen] = useState(false); if (userName && userName.includes(' ')) { userName = userName.split(' ')[0]; } return (
ST PIZZA
{cartProducts?.length > 0 && ( {cartProducts.length} )}
{mobileNavOpen && (
setMobileNavOpen(false)} className="md:hidden p-4 bg-gray-200 rounded-lg mt-2 flex flex-col gap-2 text-center"> Home Menu About Contact
)}
); } ================================================ FILE: src/components/layout/Hero.js ================================================ import Right from "@/components/icons/Right"; import Image from "next/image"; export default function Hero() { return (

Everything
is better
with a  Pizza

Pizza is the missing piece that makes every day complete, a simple yet delicious joy in life

{'pizza'}
); } ================================================ FILE: src/components/layout/HomeMenu.js ================================================ 'use client'; import SectionHeaders from "@/components/layout/SectionHeaders"; import MenuItem from "@/components/menu/MenuItem"; import Image from "next/image"; import {useEffect, useState} from "react"; export default function HomeMenu() { const [bestSellers, setBestSellers] = useState([]); useEffect(() => { fetch('/api/menu-items').then(res => { res.json().then(menuItems => { setBestSellers(menuItems.slice(-3)); }); }); }, []); return (
{'sallad'}
{'sallad'}
{bestSellers?.length > 0 && bestSellers.map(item => ( ))}
); } ================================================ FILE: src/components/layout/InfoBox.js ================================================ export default function InfoBox({children}) { return (
{children}
); } ================================================ FILE: src/components/layout/MenuItemForm.js ================================================ import Plus from "@/components/icons/Plus"; import Trash from "@/components/icons/Trash"; import EditableImage from "@/components/layout/EditableImage"; import MenuItemPriceProps from "@/components/layout/MenuItemPriceProps"; import {useEffect, useState} from "react"; export default function MenuItemForm({onSubmit,menuItem}) { const [image, setImage] = useState(menuItem?.image || ''); const [name, setName] = useState(menuItem?.name || ''); const [description, setDescription] = useState(menuItem?.description || ''); const [basePrice, setBasePrice] = useState(menuItem?.basePrice || ''); const [sizes, setSizes] = useState(menuItem?.sizes || []); const [category, setCategory] = useState(menuItem?.category || ''); const [categories, setCategories] = useState([]); const [ extraIngredientPrices, setExtraIngredientPrices, ] = useState(menuItem?.extraIngredientPrices || []); useEffect(() => { fetch('/api/categories').then(res => { res.json().then(categories => { setCategories(categories); }); }); }, []); return (
onSubmit(ev, { image,name,description,basePrice,sizes,extraIngredientPrices,category, }) } className="mt-8 max-w-2xl mx-auto">
setName(ev.target.value)} /> setDescription(ev.target.value)} /> setBasePrice(ev.target.value)} />
); } ================================================ FILE: src/components/layout/MenuItemPriceProps.js ================================================ import ChevronDown from "@/components/icons/ChevronDown"; import ChevronUp from "@/components/icons/ChevronUp"; import Plus from "@/components/icons/Plus"; import Trash from "@/components/icons/Trash"; import {useState} from "react"; export default function MenuItemPriceProps({name,addLabel,props,setProps}) { const [isOpen, setIsOpen] = useState(false); function addProp() { setProps(oldProps => { return [...oldProps, {name:'', price:0}]; }); } function editProp(ev, index, prop) { const newValue = ev.target.value; setProps(prevSizes => { const newSizes = [...prevSizes]; newSizes[index][prop] = newValue; return newSizes; }); } function removeProp(indexToRemove) { setProps(prev => prev.filter((v,index) => index !== indexToRemove)); } return (
{props?.length > 0 && props.map((size,index) => (
editProp(ev, index, 'name')} />
editProp(ev, index, 'price')} />
))}
); } ================================================ FILE: src/components/layout/SectionHeaders.js ================================================ export default function SectionHeaders({subHeader,mainHeader}) { return ( <>

{subHeader}

{mainHeader}

); } ================================================ FILE: src/components/layout/SuccessBox.js ================================================ export default function SuccessBox({children}) { return (
{children}
); } ================================================ FILE: src/components/layout/UserForm.js ================================================ 'use client'; import AddressInputs from "@/components/layout/AddressInputs"; import EditableImage from "@/components/layout/EditableImage"; import {useProfile} from "@/components/UseProfile"; import {useState} from "react"; export default function UserForm({user,onSave}) { const [userName, setUserName] = useState(user?.name || ''); const [image, setImage] = useState(user?.image || ''); const [phone, setPhone] = useState(user?.phone || ''); const [streetAddress, setStreetAddress] = useState(user?.streetAddress || ''); const [postalCode, setPostalCode] = useState(user?.postalCode || ''); const [city, setCity] = useState(user?.city || ''); const [country, setCountry] = useState(user?.country || ''); const [admin, setAdmin] = useState(user?.admin || false); const {data:loggedInUserData} = useProfile(); function handleAddressChange(propName, value) { if (propName === 'phone') setPhone(value); if (propName === 'streetAddress') setStreetAddress(value); if (propName === 'postalCode') setPostalCode(value); if (propName === 'city') setCity(value); if (propName === 'country') setCountry(value); } return (
onSave(ev, { name:userName, image, phone, admin, streetAddress, city, country, postalCode, }) } > setUserName(ev.target.value)} /> {loggedInUserData.admin && (
)}
); } ================================================ FILE: src/components/layout/UserTabs.js ================================================ 'use client'; import Link from "next/link"; import {usePathname} from "next/navigation"; export default function UserTabs({isAdmin}) { const path = usePathname(); return (
Profile {isAdmin && ( <> Categories Menu Items Users )} Orders
); } ================================================ FILE: src/components/menu/AddToCartButton.js ================================================ import FlyingButton from 'react-flying-item' export default function AddToCartButton({ hasSizesOrExtras, onClick, basePrice, image }) { if (!hasSizesOrExtras) { return (
Add to cart ${basePrice}
); } return ( ); } ================================================ FILE: src/components/menu/CartProduct.js ================================================ import {cartProductPrice} from "@/components/AppContext"; import Trash from "@/components/icons/Trash"; import Image from "next/image"; export default function CartProduct({product,onRemove}) { return (
{''}

{product.name}

{product.size && (
Size: {product.size.name}
)} {product.extras?.length > 0 && (
{product.extras.map(extra => (
{extra.name} ${extra.price}
))}
)}
${cartProductPrice(product)}
{!!onRemove && (
)}
); } ================================================ FILE: src/components/menu/MenuItem.js ================================================ import {CartContext} from "@/components/AppContext"; import MenuItemTile from "@/components/menu/MenuItemTile"; import Image from "next/image"; import {useContext, useState} from "react"; import FlyingButton from "react-flying-item"; import toast from "react-hot-toast"; export default function MenuItem(menuItem) { const { image,name,description,basePrice, sizes, extraIngredientPrices, } = menuItem; const [ selectedSize, setSelectedSize ] = useState(sizes?.[0] || null); const [selectedExtras, setSelectedExtras] = useState([]); const [showPopup, setShowPopup] = useState(false); const {addToCart} = useContext(CartContext); async function handleAddToCartButtonClick() { console.log('add to cart'); const hasOptions = sizes.length > 0 || extraIngredientPrices.length > 0; if (hasOptions && !showPopup) { setShowPopup(true); return; } addToCart(menuItem, selectedSize, selectedExtras); await new Promise(resolve => setTimeout(resolve, 1000)); console.log('hiding popup'); setShowPopup(false); } function handleExtraThingClick(ev, extraThing) { const checked = ev.target.checked; if (checked) { setSelectedExtras(prev => [...prev, extraThing]); } else { setSelectedExtras(prev => { return prev.filter(e => e.name !== extraThing.name); }); } } let selectedPrice = basePrice; if (selectedSize) { selectedPrice += selectedSize.price; } if (selectedExtras?.length > 0) { for (const extra of selectedExtras) { selectedPrice += extra.price; } } return ( <> {showPopup && (
setShowPopup(false)} className="fixed inset-0 bg-black/80 flex items-center justify-center">
ev.stopPropagation()} className="my-8 bg-white p-2 rounded-lg max-w-md">
{name}

{name}

{description}

{sizes?.length > 0 && (

Pick your size

{sizes.map(size => ( ))}
)} {extraIngredientPrices?.length > 0 && (

Any extras?

{extraIngredientPrices.map(extraThing => ( ))}
)}
Add to cart ${selectedPrice}
)} ); } ================================================ FILE: src/components/menu/MenuItemTile.js ================================================ import AddToCartButton from "@/components/menu/AddToCartButton"; export default function MenuItemTile({onAddToCart, ...item}) { const {image, description, name, basePrice, sizes, extraIngredientPrices, } = item; const hasSizesOrExtras = sizes?.length > 0 || extraIngredientPrices?.length > 0; return (
pizza

{name}

{description}

); } ================================================ FILE: src/libs/datetime.js ================================================ export function dbTimeForHuman(str) { return str.replace('T', ' ').substring(0, 16); } ================================================ FILE: src/libs/mongoConnect.js ================================================ // This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb import { MongoClient } from "mongodb" if (!process.env.MONGO_URL) { throw new Error('Invalid/Missing environment variable: "MONGODB_URI"') } const uri = process.env.MONGO_URL const options = {} let client let clientPromise; if (process.env.NODE_ENV === "development") { // In development mode, use a global variable so that the value // is preserved across module reloads caused by HMR (Hot Module Replacement). if (!global._mongoClientPromise) { client = new MongoClient(uri, options) global._mongoClientPromise = client.connect() } clientPromise = global._mongoClientPromise } else { // In production mode, it's best to not use a global variable. client = new MongoClient(uri, options) clientPromise = client.connect() } // Export a module-scoped MongoClient promise. By doing this in a // separate module, the client can be shared across functions. export default clientPromise ================================================ FILE: src/models/Category.js ================================================ import {model, models, Schema} from "mongoose"; const CategorySchema = new Schema({ name: {type:String, required:true}, }, {timestamps: true}); export const Category = models?.Category || model('Category', CategorySchema); ================================================ FILE: src/models/MenuItem.js ================================================ import mongoose, {model, models, Schema} from "mongoose"; const ExtraPriceSchema = new Schema({ name: String, price: Number, }); const MenuItemSchema = new Schema({ image: {type: String}, name: {type: String}, description: {type: String}, category: {type: mongoose.Types.ObjectId}, basePrice: {type: Number}, sizes: {type:[ExtraPriceSchema]}, extraIngredientPrices: {type:[ExtraPriceSchema]}, }, {timestamps: true}); export const MenuItem = models?.MenuItem || model('MenuItem', MenuItemSchema); ================================================ FILE: src/models/Order.js ================================================ import {model, models, Schema} from "mongoose"; const OrderSchema = new Schema({ userEmail: String, phone: String, streetAddress: String, postalCode: String, city: String, country: String, cartProducts: Object, paid: {type: Boolean, default: false}, }, {timestamps: true}); export const Order = models?.Order || model('Order', OrderSchema); ================================================ FILE: src/models/User.js ================================================ import {model, models, Schema} from "mongoose"; const UserSchema = new Schema({ name: {type: String}, email: {type: String, required: true, unique: true}, password: {type: String}, image: {type: String}, }, {timestamps: true}); export const User = models?.User || model('User', UserSchema); ================================================ FILE: src/models/UserInfo.js ================================================ import {model, models, Schema} from "mongoose"; const UserInfoSchema = new Schema({ email: {type: String, required: true}, streetAddress: {type: String}, postalCode: {type: String}, city: {type: String}, country: {type: String}, phone: {type: String}, admin: {type: Boolean, default: false}, }, {timestamps: true}); export const UserInfo = models?.UserInfo || model('UserInfo', UserInfoSchema); ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './src/pages/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { primary: '#f13a01', }, }, }, plugins: [], } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "lib": [ "dom", "dom.iterable", "esnext" ], "paths": { "@/*": ["./src/*"] }, "allowJs": true, "skipLibCheck": true, "strict": false, "noEmit": true, "incremental": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "plugins": [ { "name": "next" } ], "forceConsistentCasingInFileNames": true }, "include": [ "next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx" ], "exclude": [ "node_modules" ] }