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 (
<section className="mt-8 text-center">
<SectionHeaders mainHeader="Cart" />
<p className="mt-4">Your shopping cart is empty 😔</p>
</section>
);
}
return (
<section className="mt-8">
<div className="text-center">
<SectionHeaders mainHeader="Cart" />
</div>
<div className="mt-8 grid gap-8 grid-cols-2">
<div>
{cartProducts?.length === 0 && (
<div>No products in your shopping cart</div>
)}
{cartProducts?.length > 0 && cartProducts.map((product, index) => (
<CartProduct
key={index}
product={product}
onRemove={removeCartProduct}
/>
))}
<div className="py-2 pr-16 flex justify-end items-center">
<div className="text-gray-500">
Subtotal:<br />
Delivery:<br />
Total:
</div>
<div className="font-semibold pl-2 text-right">
${subtotal}<br />
$5<br />
${subtotal + 5}
</div>
</div>
</div>
<div className="bg-gray-100 p-4 rounded-lg">
<h2>Checkout</h2>
<form onSubmit={proceedToCheckout}>
<AddressInputs
addressProps={address}
setAddressProp={handleAddressChange}
/>
<button type="submit">Pay ${subtotal+5}</button>
</form>
</div>
</div>
</section>
);
}
================================================
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 (
<section className="mt-8 max-w-2xl mx-auto">
<UserTabs isAdmin={true} />
<form className="mt-8" onSubmit={handleCategorySubmit}>
<div className="flex gap-2 items-end">
<div className="grow">
<label>
{editedCategory ? 'Update category' : 'New category name'}
{editedCategory && (
<>: <b>{editedCategory.name}</b></>
)}
</label>
<input type="text"
value={categoryName}
onChange={ev => setCategoryName(ev.target.value)}
/>
</div>
<div className="pb-2 flex gap-2">
<button className="border border-primary" type="submit">
{editedCategory ? 'Update' : 'Create'}
</button>
<button
type="button"
onClick={() => {
setEditedCategory(null);
setCategoryName('');
}}>
Cancel
</button>
</div>
</div>
</form>
<div>
<h2 className="mt-8 text-sm text-gray-500">Existing categories</h2>
{categories?.length > 0 && categories.map(c => (
<div
key={c._id}
className="bg-gray-100 rounded-xl p-2 px-4 flex gap-1 mb-1 items-center">
<div className="grow">
{c.name}
</div>
<div className="flex gap-1">
<button type="button"
onClick={() => {
setEditedCategory(c);
setCategoryName(c.name);
}}
>
Edit
</button>
<DeleteButton
label="Delete"
onDelete={() => handleDeleteClick(c._id)} />
</div>
</div>
))}
</div>
</section>
);
}
================================================
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 (
<html lang="en" className="scroll-smooth">
<body className={roboto.className}>
<main className="max-w-4xl mx-auto p-4">
<AppProvider>
<Toaster />
<Header />
{children}
<footer className="border-t p-8 text-center text-gray-500 mt-16">
© 2023 All rights reserved
</footer>
</AppProvider>
</main>
</body>
</html>
)
}
================================================
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 (
<section className="mt-8">
<h1 className="text-center text-primary text-4xl mb-4">
Login
</h1>
<form className="max-w-xs mx-auto" onSubmit={handleFormSubmit}>
<input type="email" name="email" placeholder="email" value={email}
disabled={loginInProgress}
onChange={ev => setEmail(ev.target.value)} />
<input type="password" name="password" placeholder="password" value={password}
disabled={loginInProgress}
onChange={ev => setPassword(ev.target.value)}/>
<button disabled={loginInProgress} type="submit">Login</button>
<div className="my-4 text-center text-gray-500">
or login with provider
</div>
<button type="button" onClick={() => signIn('google', {callbackUrl: '/'})}
className="flex gap-4 justify-center">
<Image src={'/google.png'} alt={''} width={24} height={24} />
Login with google
</button>
</form>
</section>
);
}
================================================
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 (
<section className="mt-8">
{categories?.length > 0 && categories.map(c => (
<div key={c._id}>
<div className="text-center">
<SectionHeaders mainHeader={c.name} />
</div>
<div className="grid sm:grid-cols-3 gap-4 mt-6 mb-12">
{menuItems.filter(item => item.category === c._id).map(item => (
<MenuItem key={item._id} {...item} />
))}
</div>
</div>
))}
</section>
);
}
================================================
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 (
<section className="mt-8">
<UserTabs isAdmin={true} />
<div className="max-w-2xl mx-auto mt-8">
<Link href={'/menu-items'} className="button">
<Left />
<span>Show all menu items</span>
</Link>
</div>
<MenuItemForm menuItem={menuItem} onSubmit={handleFormSubmit} />
<div className="max-w-md mx-auto mt-2">
<div className="max-w-xs ml-auto pl-4">
<DeleteButton
label="Delete this menu item"
onDelete={handleDeleteClick}
/>
</div>
</div>
</section>
);
}
================================================
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 (
<section className="mt-8">
<UserTabs isAdmin={true} />
<div className="max-w-2xl mx-auto mt-8">
<Link href={'/menu-items'} className="button">
<Left />
<span>Show all menu items</span>
</Link>
</div>
<MenuItemForm menuItem={null} onSubmit={handleFormSubmit} />
</section>
);
}
================================================
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 (
<section className="mt-8 max-w-2xl mx-auto">
<UserTabs isAdmin={true} />
<div className="mt-8">
<Link
className="button flex"
href={'/menu-items/new'}>
<span>Crete new menu item</span>
<Right />
</Link>
</div>
<div>
<h2 className="text-sm text-gray-500 mt-8">Edit menu item:</h2>
<div className="grid grid-cols-3 gap-2">
{menuItems?.length > 0 && menuItems.map(item => (
<Link
key={item._id}
href={'/menu-items/edit/'+item._id}
className="bg-gray-200 rounded-lg p-4"
>
<div className="relative">
<Image
className="rounded-md"
src={item.image} alt={''} width={200} height={200} />
</div>
<div className="text-center">
{item.name}
</div>
</Link>
))}
</div>
</div>
</section>
);
}
================================================
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 (
<section className="max-w-2xl mx-auto mt-8">
<div className="text-center">
<SectionHeaders mainHeader="Your order" />
<div className="mt-4 mb-8">
<p>Thanks for your order.</p>
<p>We will call you when your order will be on the way.</p>
</div>
</div>
{loadingOrder && (
<div>Loading order...</div>
)}
{order && (
<div className="grid md:grid-cols-2 md:gap-16">
<div>
{order.cartProducts.map(product => (
<CartProduct key={product._id} product={product} />
))}
<div className="text-right py-2 text-gray-500">
Subtotal:
<span className="text-black font-bold inline-block w-8">${subtotal}</span>
<br />
Delivery:
<span className="text-black font-bold inline-block w-8">$5</span>
<br />
Total:
<span className="text-black font-bold inline-block w-8">
${subtotal + 5}
</span>
</div>
</div>
<div>
<div className="bg-gray-100 p-4 rounded-lg">
<AddressInputs
disabled={true}
addressProps={order}
/>
</div>
</div>
</div>
)}
</section>
);
}
================================================
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 (
<section className="mt-8 max-w-2xl mx-auto">
<UserTabs isAdmin={profile.admin} />
<div className="mt-8">
{loadingOrders && (
<div>Loading orders...</div>
)}
{orders?.length > 0 && orders.map(order => (
<div
key={order._id}
className="bg-gray-100 mb-2 p-4 rounded-lg flex flex-col md:flex-row items-center gap-6">
<div className="grow flex flex-col md:flex-row items-center gap-6">
<div>
<div className={
(order.paid ? 'bg-green-500' : 'bg-red-400')
+ ' p-2 rounded-md text-white w-24 text-center'
}>
{order.paid ? 'Paid' : 'Not paid'}
</div>
</div>
<div className="grow">
<div className="flex gap-2 items-center mb-1">
<div className="grow">{order.userEmail}</div>
<div className="text-gray-500 text-sm">{dbTimeForHuman(order.createdAt)}</div>
</div>
<div className="text-gray-500 text-xs">
{order.cartProducts.map(p => p.name).join(', ')}
</div>
</div>
</div>
<div className="justify-end flex gap-2 items-center whitespace-nowrap">
<Link href={"/orders/"+order._id} className="button">
Show order
</Link>
</div>
</div>
))}
</div>
</section>
);
}
================================================
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 (
<>
<Hero />
<HomeMenu />
<section className="text-center my-16" id="about">
<SectionHeaders
subHeader={'Our story'}
mainHeader={'About us'}
/>
<div className="text-gray-500 max-w-md mx-auto mt-4 flex flex-col gap-4">
<p>
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!
</p>
<p>At consectetur delectus ducimus est facere iure molestias obcaecati quaerat vitae voluptate? Aspernatur dolor explicabo iste minus molestiae pariatur provident quibusdam saepe?</p>
<p>Laborum molestias neque nulla obcaecati odio quia quod reprehenderit sit vitae voluptates? Eos, tenetur.</p>
</div>
</section>
<section className="text-center my-8" id="contact">
<SectionHeaders
subHeader={'Don\'t hesitate'}
mainHeader={'Contact us'}
/>
<div className="mt-8">
<a className="text-4xl underline text-gray-500" href="tel:+46738123123">
+46 738 123 123
</a>
</div>
</section>
</>
)
}
================================================
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 (
<section className="mt-8">
<UserTabs isAdmin={isAdmin} />
<div className="max-w-2xl mx-auto mt-8">
<UserForm user={user} onSave={handleProfileInfoUpdate} />
</div>
</section>
);
}
================================================
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 (
<section className="mt-8">
<h1 className="text-center text-primary text-4xl mb-4">
Register
</h1>
{userCreated && (
<div className="my-4 text-center">
User created.<br />
Now you can{' '}
<Link className="underline" href={'/login'}>Login »</Link>
</div>
)}
{error && (
<div className="my-4 text-center">
An error has occurred.<br />
Please try again later
</div>
)}
<form className="block max-w-xs mx-auto" onSubmit={handleFormSubmit}>
<input type="email" placeholder="email" value={email}
disabled={creatingUser}
onChange={ev => setEmail(ev.target.value)} />
<input type="password" placeholder="password" value={password}
disabled={creatingUser}
onChange={ev => setPassword(ev.target.value)}/>
<button type="submit" disabled={creatingUser}>
Register
</button>
<div className="my-4 text-center text-gray-500">
or login with provider
</div>
<button
onClick={() => signIn('google', {callbackUrl:'/'})}
className="flex gap-4 justify-center">
<Image src={'/google.png'} alt={''} width={24} height={24} />
Login with google
</button>
<div className="text-center my-4 text-gray-500 border-t pt-4">
Existing account?{' '}
<Link className="underline" href={'/login'}>Login here »</Link>
</div>
</form>
</section>
);
}
================================================
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 (
<section className="mt-8 mx-auto max-w-2xl">
<UserTabs isAdmin={true} />
<div className="mt-8">
<UserForm user={user} onSave={handleSaveButtonClick} />
</div>
</section>
);
}
================================================
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 (
<section className="max-w-2xl mx-auto mt-8">
<UserTabs isAdmin={true} />
<div className="mt-8">
{users?.length > 0 && users.map(user => (
<div
key={user._id}
className="bg-gray-100 rounded-lg mb-2 p-1 px-4 flex items-center gap-4">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 grow">
<div className="text-gray-900">
{!!user.name && (<span>{user.name}</span>)}
{!user.name && (<span className="italic">No name</span>)}
</div>
<span className="text-gray-500">{user.email}</span>
</div>
<div>
<Link className="button" href={'/users/'+user._id}>
Edit
</Link>
</div>
</div>
))}
</div>
</section>
);
}
================================================
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 (
<SessionProvider>
<CartContext.Provider value={{
cartProducts, setCartProducts,
addToCart, removeCartProduct, clearCart,
}}>
{children}
</CartContext.Provider>
</SessionProvider>
);
}
================================================
FILE: src/components/DeleteButton.js
================================================
import {useState} from "react";
export default function DeleteButton({label,onDelete}) {
const [showConfirm, setShowConfirm] = useState(false);
if (showConfirm) {
return (
<div className="fixed bg-black/80 inset-0 flex items-center h-full justify-center">
<div className="bg-white p-4 rounded-lg">
<div>Are you sure you want to delete?</div>
<div className="flex gap-2 mt-1">
<button type="button" onClick={() => setShowConfirm(false)}>
Cancel
</button>
<button
onClick={() => {
onDelete();
setShowConfirm(false);
}}
type="button"
className="primary">
Yes, delete!
</button>
</div>
</div>
</div>
);
}
return (
<button type="button" onClick={() => setShowConfirm(true)}>
{label}
</button>
);
}
================================================
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 (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5" />
</svg>
);
}
================================================
FILE: src/components/icons/ChevronDown.js
================================================
export default function ChevronDown({className="w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
);
}
================================================
FILE: src/components/icons/ChevronUp.js
================================================
export default function ChevronUp({className="w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg>
);
}
================================================
FILE: src/components/icons/Left.js
================================================
export default function Left({className="w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 9l-3 3m0 0l3 3m-3-3h7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
================================================
FILE: src/components/icons/Plus.js
================================================
export default function Plus({className="w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
}
================================================
FILE: src/components/icons/Right.js
================================================
export default function Right({className="w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
================================================
FILE: src/components/icons/ShoppingCart.js
================================================
export default function ShoppingCart({className = "w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
);
}
================================================
FILE: src/components/icons/Trash.js
================================================
export default function Trash({className="w-6 h-6"}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
);
}
================================================
FILE: src/components/layout/AddressInputs.js
================================================
export default function AddressInputs({addressProps,setAddressProp,disabled=false}) {
const {phone, streetAddress, postalCode, city, country} = addressProps;
return (
<>
<label>Phone</label>
<input
disabled={disabled}
type="tel" placeholder="Phone number"
value={phone || ''} onChange={ev => setAddressProp('phone', ev.target.value)} />
<label>Street address</label>
<input
disabled={disabled}
type="text" placeholder="Street address"
value={streetAddress || ''} onChange={ev => setAddressProp('streetAddress', ev.target.value)}
/>
<div className="grid grid-cols-2 gap-2">
<div>
<label>Postal code</label>
<input
disabled={disabled}
type="text" placeholder="Postal code"
value={postalCode || ''} onChange={ev => setAddressProp('postalCode', ev.target.value)}
/>
</div>
<div>
<label>City</label>
<input
disabled={disabled}
type="text" placeholder="City"
value={city || ''} onChange={ev => setAddressProp('city', ev.target.value)}
/>
</div>
</div>
<label>Country</label>
<input
disabled={disabled}
type="text" placeholder="Country"
value={country || ''} onChange={ev => 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 && (
<Image className="rounded-lg w-full h-full mb-1" src={link} width={250} height={250} alt={'avatar'} />
)}
{!link && (
<div className="text-center bg-gray-200 p-4 text-gray-500 rounded-lg mb-1">
No image
</div>
)}
<label>
<input type="file" className="hidden" onChange={handleFileChange} />
<span className="block border border-gray-300 rounded-lg p-2 text-center cursor-pointer">Change image</span>
</label>
</>
);
}
================================================
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 (
<>
<Link href={'/profile'} className="whitespace-nowrap">
Hello, {userName}
</Link>
<button
onClick={() => signOut()}
className="bg-primary rounded-full text-white px-8 py-2">
Logout
</button>
</>
);
}
if (status === 'unauthenticated') {
return (
<>
<Link href={'/login'}>Login</Link>
<Link href={'/register'} className="bg-primary rounded-full text-white px-8 py-2">
Register
</Link>
</>
);
}
}
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 (
<header>
<div className="flex items-center md:hidden justify-between">
<Link className="text-primary font-semibold text-2xl" href={'/'}>
ST PIZZA
</Link>
<div className="flex gap-8 items-center">
<Link href={'/cart'} className="relative">
<ShoppingCart />
{cartProducts?.length > 0 && (
<span className="absolute -top-2 -right-4 bg-primary text-white text-xs py-1 px-1 rounded-full leading-3">
{cartProducts.length}
</span>
)}
</Link>
<button
className="p-1 border"
onClick={() => setMobileNavOpen(prev => !prev)}>
<Bars2 />
</button>
</div>
</div>
{mobileNavOpen && (
<div
onClick={() => setMobileNavOpen(false)}
className="md:hidden p-4 bg-gray-200 rounded-lg mt-2 flex flex-col gap-2 text-center">
<Link href={'/'}>Home</Link>
<Link href={'/menu'}>Menu</Link>
<Link href={'/#about'}>About</Link>
<Link href={'/#contact'}>Contact</Link>
<AuthLinks status={status} userName={userName} />
</div>
)}
<div className="hidden md:flex items-center justify-between">
<nav className="flex items-center gap-8 text-gray-500 font-semibold">
<Link className="text-primary font-semibold text-2xl" href={'/'}>
ST PIZZA
</Link>
<Link href={'/'}>Home</Link>
<Link href={'/menu'}>Menu</Link>
<Link href={'/#about'}>About</Link>
<Link href={'/#contact'}>Contact</Link>
</nav>
<nav className="flex items-center gap-4 text-gray-500 font-semibold">
<AuthLinks status={status} userName={userName} />
<Link href={'/cart'} className="relative">
<ShoppingCart />
{cartProducts?.length > 0 && (
<span className="absolute -top-2 -right-4 bg-primary text-white text-xs py-1 px-1 rounded-full leading-3">
{cartProducts.length}
</span>
)}
</Link>
</nav>
</div>
</header>
);
}
================================================
FILE: src/components/layout/Hero.js
================================================
import Right from "@/components/icons/Right";
import Image from "next/image";
export default function Hero() {
return (
<section className="hero md:mt-4">
<div className="py-8 md:py-12">
<h1 className="text-4xl font-semibold">
Everything<br />
is better<br />
with a
<span className="text-primary">
Pizza
</span>
</h1>
<p className="my-6 text-gray-500 text-sm">
Pizza is the missing piece that makes every day complete, a simple yet delicious joy in life
</p>
<div className="flex gap-4 text-sm">
<button className="flex justify-center bg-primary uppercase flex items-center gap-2 text-white px-4 py-2 rounded-full">
Order now
<Right />
</button>
<button className="flex items-center border-0 gap-2 py-2 text-gray-600 font-semibold">
Learn more
<Right />
</button>
</div>
</div>
<div className="relative hidden md:block">
<Image src={'/pizza.png'} layout={'fill'} objectFit={'contain'} alt={'pizza'} />
</div>
</section>
);
}
================================================
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 (
<section className="">
<div className="absolute left-0 right-0 w-full justify-start">
<div className="absolute left-0 -top-[70px] text-left -z-10">
<Image src={'/sallad1.png'} width={109} height={189} alt={'sallad'} />
</div>
<div className="absolute -top-[100px] right-0 -z-10">
<Image src={'/sallad2.png'} width={107} height={195} alt={'sallad'} />
</div>
</div>
<div className="text-center mb-4">
<SectionHeaders
subHeader={'check out'}
mainHeader={'Our Best Sellers'} />
</div>
<div className="grid sm:grid-cols-3 gap-4">
{bestSellers?.length > 0 && bestSellers.map(item => (
<MenuItem key={item._id} {...item} />
))}
</div>
</section>
);
}
================================================
FILE: src/components/layout/InfoBox.js
================================================
export default function InfoBox({children}) {
return (
<div className="text-center bg-blue-100 p-4 rounded-lg border border-blue-300">
{children}
</div>
);
}
================================================
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 (
<form
onSubmit={ev =>
onSubmit(ev, {
image,name,description,basePrice,sizes,extraIngredientPrices,category,
})
}
className="mt-8 max-w-2xl mx-auto">
<div
className="md:grid items-start gap-4"
style={{gridTemplateColumns:'.3fr .7fr'}}>
<div>
<EditableImage link={image} setLink={setImage} />
</div>
<div className="grow">
<label>Item name</label>
<input
type="text"
value={name}
onChange={ev => setName(ev.target.value)}
/>
<label>Description</label>
<input
type="text"
value={description}
onChange={ev => setDescription(ev.target.value)}
/>
<label>Category</label>
<select value={category} onChange={ev => setCategory(ev.target.value)}>
{categories?.length > 0 && categories.map(c => (
<option key={c._id} value={c._id}>{c.name}</option>
))}
</select>
<label>Base price</label>
<input
type="text"
value={basePrice}
onChange={ev => setBasePrice(ev.target.value)}
/>
<MenuItemPriceProps name={'Sizes'}
addLabel={'Add item size'}
props={sizes}
setProps={setSizes} />
<MenuItemPriceProps name={'Extra ingredients'}
addLabel={'Add ingredients prices'}
props={extraIngredientPrices}
setProps={setExtraIngredientPrices}/>
<button type="submit">Save</button>
</div>
</div>
</form>
);
}
================================================
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 (
<div className="bg-gray-200 p-2 rounded-md mb-2">
<button
onClick={() => setIsOpen(prev => !prev)}
className="inline-flex p-1 border-0 justify-start"
type="button">
{isOpen && (
<ChevronUp />
)}
{!isOpen && (
<ChevronDown />
)}
<span>{name}</span>
<span>({props?.length})</span>
</button>
<div className={isOpen ? 'block' : 'hidden'}>
{props?.length > 0 && props.map((size,index) => (
<div key={index} className="flex items-end gap-2">
<div>
<label>Name</label>
<input type="text"
placeholder="Size name"
value={size.name}
onChange={ev => editProp(ev, index, 'name')}
/>
</div>
<div>
<label>Extra price</label>
<input type="text" placeholder="Extra price"
value={size.price}
onChange={ev => editProp(ev, index, 'price')}
/>
</div>
<div>
<button type="button"
onClick={() => removeProp(index)}
className="bg-white mb-2 px-2">
<Trash />
</button>
</div>
</div>
))}
<button
type="button"
onClick={addProp}
className="bg-white items-center">
<Plus className="w-4 h-4" />
<span>{addLabel}</span>
</button>
</div>
</div>
);
}
================================================
FILE: src/components/layout/SectionHeaders.js
================================================
export default function SectionHeaders({subHeader,mainHeader}) {
return (
<>
<h3 className="uppercase text-gray-500 font-semibold leading-4">
{subHeader}
</h3>
<h2 className="text-primary font-bold text-4xl italic">
{mainHeader}
</h2>
</>
);
}
================================================
FILE: src/components/layout/SuccessBox.js
================================================
export default function SuccessBox({children}) {
return (
<div className="text-center bg-green-100 p-4 rounded-lg border border-green-300">
{children}
</div>
);
}
================================================
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 (
<div className="md:flex gap-4">
<div>
<div className="p-2 rounded-lg relative max-w-[120px]">
<EditableImage link={image} setLink={setImage} />
</div>
</div>
<form
className="grow"
onSubmit={ev =>
onSave(ev, {
name:userName, image, phone, admin,
streetAddress, city, country, postalCode,
})
}
>
<label>
First and last name
</label>
<input
type="text" placeholder="First and last name"
value={userName} onChange={ev => setUserName(ev.target.value)}
/>
<label>Email</label>
<input
type="email"
disabled={true}
value={user.email}
placeholder={'email'}
/>
<AddressInputs
addressProps={{phone, streetAddress, postalCode, city, country}}
setAddressProp={handleAddressChange}
/>
{loggedInUserData.admin && (
<div>
<label className="p-2 inline-flex items-center gap-2 mb-2" htmlFor="adminCb">
<input
id="adminCb" type="checkbox" className="" value={'1'}
checked={admin}
onChange={ev => setAdmin(ev.target.checked)}
/>
<span>Admin</span>
</label>
</div>
)}
<button type="submit">Save</button>
</form>
</div>
);
}
================================================
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 (
<div className="flex mx-auto gap-2 tabs justify-center flex-wrap">
<Link
className={path === '/profile' ? 'active' : ''}
href={'/profile'}
>
Profile
</Link>
{isAdmin && (
<>
<Link
href={'/categories'}
className={path === '/categories' ? 'active' : ''}
>
Categories
</Link>
<Link
href={'/menu-items'}
className={path.includes('menu-items') ? 'active' : ''}
>
Menu Items
</Link>
<Link
className={path.includes('/users') ? 'active' : ''}
href={'/users'}
>
Users
</Link>
</>
)}
<Link
className={path === '/orders' ? 'active' : ''}
href={'/orders'}
>
Orders
</Link>
</div>
);
}
================================================
FILE: src/components/menu/AddToCartButton.js
================================================
import FlyingButton from 'react-flying-item'
export default function AddToCartButton({
hasSizesOrExtras, onClick, basePrice, image
}) {
if (!hasSizesOrExtras) {
return (
<div className="flying-button-parent mt-4">
<FlyingButton
targetTop={'5%'}
targetLeft={'95%'}
src={image}>
<div onClick={onClick}>
Add to cart ${basePrice}
</div>
</FlyingButton>
</div>
);
}
return (
<button
type="button"
onClick={onClick}
className="mt-4 bg-primary text-white rounded-full px-8 py-2"
>
<span>Add to cart (from ${basePrice})</span>
</button>
);
}
================================================
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 (
<div className="flex items-center gap-4 border-b py-4">
<div className="w-24">
<Image width={240} height={240} src={product.image} alt={''} />
</div>
<div className="grow">
<h3 className="font-semibold">
{product.name}
</h3>
{product.size && (
<div className="text-sm">
Size: <span>{product.size.name}</span>
</div>
)}
{product.extras?.length > 0 && (
<div className="text-sm text-gray-500">
{product.extras.map(extra => (
<div key={extra.name}>{extra.name} ${extra.price}</div>
))}
</div>
)}
</div>
<div className="text-lg font-semibold">
${cartProductPrice(product)}
</div>
{!!onRemove && (
<div className="ml-2">
<button
type="button"
onClick={() => onRemove(index)}
className="p-2">
<Trash />
</button>
</div>
)}
</div>
);
}
================================================
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 && (
<div
onClick={() => setShowPopup(false)}
className="fixed inset-0 bg-black/80 flex items-center justify-center">
<div
onClick={ev => ev.stopPropagation()}
className="my-8 bg-white p-2 rounded-lg max-w-md">
<div
className="overflow-y-scroll p-2"
style={{maxHeight:'calc(100vh - 100px)'}}>
<Image
src={image}
alt={name}
width={300} height={200}
className="mx-auto" />
<h2 className="text-lg font-bold text-center mb-2">{name}</h2>
<p className="text-center text-gray-500 text-sm mb-2">
{description}
</p>
{sizes?.length > 0 && (
<div className="py-2">
<h3 className="text-center text-gray-700">Pick your size</h3>
{sizes.map(size => (
<label
key={size._id}
className="flex items-center gap-2 p-4 border rounded-md mb-1">
<input
type="radio"
onChange={() => setSelectedSize(size)}
checked={selectedSize?.name === size.name}
name="size"/>
{size.name} ${basePrice + size.price}
</label>
))}
</div>
)}
{extraIngredientPrices?.length > 0 && (
<div className="py-2">
<h3 className="text-center text-gray-700">Any extras?</h3>
{extraIngredientPrices.map(extraThing => (
<label
key={extraThing._id}
className="flex items-center gap-2 p-4 border rounded-md mb-1">
<input
type="checkbox"
onChange={ev => handleExtraThingClick(ev, extraThing)}
checked={selectedExtras.map(e => e._id).includes(extraThing._id)}
name={extraThing.name} />
{extraThing.name} +${extraThing.price}
</label>
))}
</div>
)}
<FlyingButton
targetTop={'5%'}
targetLeft={'95%'}
src={image}>
<div className="primary sticky bottom-2"
onClick={handleAddToCartButtonClick}>
Add to cart ${selectedPrice}
</div>
</FlyingButton>
<button
className="mt-2"
onClick={() => setShowPopup(false)}>
Cancel
</button>
</div>
</div>
</div>
)}
<MenuItemTile
onAddToCart={handleAddToCartButtonClick}
{...menuItem} />
</>
);
}
================================================
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 (
<div className="bg-gray-200 p-4 rounded-lg text-center
group hover:bg-white hover:shadow-md hover:shadow-black/25 transition-all">
<div className="text-center">
<img src={image} className="max-h-auto max-h-24 block mx-auto" alt="pizza"/>
</div>
<h4 className="font-semibold text-xl my-3">{name}</h4>
<p className="text-gray-500 text-sm line-clamp-3">
{description}
</p>
<AddToCartButton
image={image}
hasSizesOrExtras={hasSizesOrExtras}
onClick={onAddToCart}
basePrice={basePrice}
/>
</div>
);
}
================================================
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"
]
}
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
SYMBOL INDEX (63 symbols across 53 files)
FILE: src/app/api/auth/[...nextauth]/route.js
method authorize (line 26) | async authorize(credentials, req) {
function isAdmin (line 44) | async function isAdmin() {
FILE: src/app/api/categories/route.js
function POST (line 5) | async function POST(req) {
function PUT (line 16) | async function PUT(req) {
function GET (line 25) | async function GET() {
function DELETE (line 32) | async function DELETE(req) {
FILE: src/app/api/checkout/route.js
function POST (line 8) | async function POST(req) {
FILE: src/app/api/menu-items/route.js
function POST (line 5) | async function POST(req) {
function PUT (line 16) | async function PUT(req) {
function GET (line 25) | async function GET() {
function DELETE (line 32) | async function DELETE(req) {
FILE: src/app/api/orders/route.js
function GET (line 6) | async function GET(req) {
FILE: src/app/api/profile/route.js
function PUT (line 7) | async function PUT(req) {
function GET (line 28) | async function GET(req) {
FILE: src/app/api/register/route.js
function POST (line 5) | async function POST(req) {
FILE: src/app/api/upload/route.js
function POST (line 4) | async function POST(req) {
FILE: src/app/api/users/route.js
function GET (line 5) | async function GET() {
FILE: src/app/api/webhook/route.js
function POST (line 5) | async function POST(req) {
FILE: src/app/cart/page.js
function CartPage (line 12) | function CartPage() {
FILE: src/app/categories/page.js
function CategoriesPage (line 8) | function CategoriesPage() {
FILE: src/app/layout.js
function RootLayout (line 14) | function RootLayout({ children }) {
FILE: src/app/login/page.js
function LoginPage (line 6) | function LoginPage() {
FILE: src/app/menu-items/edit/[id]/page.js
function EditMenuItemPage (line 13) | function EditMenuItemPage() {
FILE: src/app/menu-items/new/page.js
function NewMenuItemPage (line 13) | function NewMenuItemPage() {
FILE: src/app/menu-items/page.js
function MenuItemsPage (line 9) | function MenuItemsPage() {
FILE: src/app/menu/page.js
function MenuPage (line 6) | function MenuPage() {
FILE: src/app/orders/[id]/page.js
function OrderPage (line 9) | function OrderPage() {
FILE: src/app/orders/page.js
function OrdersPage (line 9) | function OrdersPage() {
FILE: src/app/page.js
function Home (line 6) | function Home() {
FILE: src/app/profile/page.js
function ProfilePage (line 14) | function ProfilePage() {
FILE: src/app/register/page.js
function RegisterPage (line 7) | function RegisterPage() {
FILE: src/app/users/[id]/page.js
function EditUserPage (line 9) | function EditUserPage() {
FILE: src/app/users/page.js
function UsersPage (line 7) | function UsersPage() {
FILE: src/components/AppContext.js
function cartProductPrice (line 8) | function cartProductPrice(cartProduct) {
function AppProvider (line 21) | function AppProvider({children}) {
FILE: src/components/DeleteButton.js
function DeleteButton (line 3) | function DeleteButton({label,onDelete}) {
FILE: src/components/UseProfile.js
function useProfile (line 3) | function useProfile() {
FILE: src/components/icons/Bars2.js
function Bars2 (line 1) | function Bars2({className="w-6 h-6"}) {
FILE: src/components/icons/ChevronDown.js
function ChevronDown (line 1) | function ChevronDown({className="w-6 h-6"}) {
FILE: src/components/icons/ChevronUp.js
function ChevronUp (line 1) | function ChevronUp({className="w-6 h-6"}) {
FILE: src/components/icons/Left.js
function Left (line 1) | function Left({className="w-6 h-6"}) {
FILE: src/components/icons/Plus.js
function Plus (line 1) | function Plus({className="w-6 h-6"}) {
FILE: src/components/icons/Right.js
function Right (line 1) | function Right({className="w-6 h-6"}) {
FILE: src/components/icons/ShoppingCart.js
function ShoppingCart (line 1) | function ShoppingCart({className = "w-6 h-6"}) {
FILE: src/components/icons/Trash.js
function Trash (line 1) | function Trash({className="w-6 h-6"}) {
FILE: src/components/layout/AddressInputs.js
function AddressInputs (line 1) | function AddressInputs({addressProps,setAddressProp,disabled=false}) {
FILE: src/components/layout/EditableImage.js
function EditableImage (line 4) | function EditableImage({link, setLink}) {
FILE: src/components/layout/Header.js
function AuthLinks (line 9) | function AuthLinks({status, userName}) {
function Header (line 36) | function Header() {
FILE: src/components/layout/Hero.js
function Hero (line 4) | function Hero() {
FILE: src/components/layout/HomeMenu.js
function HomeMenu (line 7) | function HomeMenu() {
FILE: src/components/layout/InfoBox.js
function InfoBox (line 1) | function InfoBox({children}) {
FILE: src/components/layout/MenuItemForm.js
function MenuItemForm (line 7) | function MenuItemForm({onSubmit,menuItem}) {
FILE: src/components/layout/MenuItemPriceProps.js
function MenuItemPriceProps (line 7) | function MenuItemPriceProps({name,addLabel,props,setProps}) {
FILE: src/components/layout/SectionHeaders.js
function SectionHeaders (line 1) | function SectionHeaders({subHeader,mainHeader}) {
FILE: src/components/layout/SuccessBox.js
function SuccessBox (line 1) | function SuccessBox({children}) {
FILE: src/components/layout/UserForm.js
function UserForm (line 7) | function UserForm({user,onSave}) {
FILE: src/components/layout/UserTabs.js
function UserTabs (line 5) | function UserTabs({isAdmin}) {
FILE: src/components/menu/AddToCartButton.js
function AddToCartButton (line 3) | function AddToCartButton({
FILE: src/components/menu/CartProduct.js
function CartProduct (line 5) | function CartProduct({product,onRemove}) {
FILE: src/components/menu/MenuItem.js
function MenuItem (line 8) | function MenuItem(menuItem) {
FILE: src/components/menu/MenuItemTile.js
function MenuItemTile (line 3) | function MenuItemTile({onAddToCart, ...item}) {
FILE: src/libs/datetime.js
function dbTimeForHuman (line 1) | function dbTimeForHuman(str) {
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (91K chars).
[
{
"path": ".eslintrc.json",
"chars": 40,
"preview": "{\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": ".gitignore",
"chars": 402,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "README.md",
"chars": 1382,
"preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
},
{
"path": "jsconfig.json",
"chars": 77,
"preview": "{\n \"compilerOptions\": {\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n }\n}\n"
},
{
"path": "next.config.js",
"chars": 328,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n images: {\n remotePatterns: [\n {\n protocol"
},
{
"path": "package.json",
"chars": 850,
"preview": "{\n \"name\": \"food-ordering-app\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"bui"
},
{
"path": "postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "src/app/api/auth/[...nextauth]/route.js",
"chars": 1731,
"preview": "import clientPromise from \"@/libs/mongoConnect\";\nimport {UserInfo} from \"@/models/UserInfo\";\nimport bcrypt from \"bcrypt\""
},
{
"path": "src/app/api/categories/route.js",
"chars": 1022,
"preview": "import {isAdmin} from \"@/app/api/auth/[...nextauth]/route\";\nimport {Category} from \"@/models/Category\";\nimport mongoose "
},
{
"path": "src/app/api/checkout/route.js",
"chars": 2332,
"preview": "import {authOptions} from \"@/app/api/auth/[...nextauth]/route\";\nimport {MenuItem} from \"@/models/MenuItem\";\nimport {Orde"
},
{
"path": "src/app/api/menu-items/route.js",
"chars": 1027,
"preview": "import {isAdmin} from \"@/app/api/auth/[...nextauth]/route\";\nimport {MenuItem} from \"@/models/MenuItem\";\nimport mongoose "
},
{
"path": "src/app/api/orders/route.js",
"chars": 698,
"preview": "import {authOptions, isAdmin} from \"@/app/api/auth/[...nextauth]/route\";\nimport {Order} from \"@/models/Order\";\nimport mo"
},
{
"path": "src/app/api/profile/route.js",
"chars": 1362,
"preview": "import {authOptions} from \"@/app/api/auth/[...nextauth]/route\";\nimport {User} from \"@/models/User\";\nimport {UserInfo} fr"
},
{
"path": "src/app/api/register/route.js",
"chars": 562,
"preview": "import {User} from \"@/models/User\";\nimport bcrypt from \"bcrypt\";\nimport mongoose from \"mongoose\";\n\nexport async function"
},
{
"path": "src/app/api/upload/route.js",
"chars": 1061,
"preview": "import {PutObjectCommand, S3Client} from \"@aws-sdk/client-s3\";\nimport uniqid from 'uniqid';\n\nexport async function POST("
},
{
"path": "src/app/api/users/route.js",
"chars": 344,
"preview": "import {isAdmin} from \"@/app/api/auth/[...nextauth]/route\";\nimport {User} from \"@/models/User\";\nimport mongoose from \"mo"
},
{
"path": "src/app/api/webhook/route.js",
"chars": 827,
"preview": "import {Order} from \"@/models/Order\";\n\nconst stripe = require('stripe')(process.env.STRIPE_SK);\n\nexport async function P"
},
{
"path": "src/app/cart/page.js",
"chars": 3742,
"preview": "'use client';\nimport {CartContext, cartProductPrice} from \"@/components/AppContext\";\nimport Trash from \"@/components/ico"
},
{
"path": "src/app/categories/page.js",
"chars": 4164,
"preview": "'use client';\nimport DeleteButton from \"@/components/DeleteButton\";\nimport UserTabs from \"@/components/layout/UserTabs\";"
},
{
"path": "src/app/globals.css",
"chars": 1183,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nsection.hero{\n @apply block md:grid;\n grid-template-co"
},
{
"path": "src/app/layout.js",
"chars": 904,
"preview": "import {AppProvider} from \"@/components/AppContext\";\nimport Header from \"@/components/layout/Header\";\nimport { Roboto } "
},
{
"path": "src/app/login/page.js",
"chars": 1553,
"preview": "'use client';\nimport {signIn} from \"next-auth/react\";\nimport Image from \"next/image\";\nimport {useState} from \"react\";\n\ne"
},
{
"path": "src/app/menu/page.js",
"chars": 1073,
"preview": "'use client';\nimport SectionHeaders from \"@/components/layout/SectionHeaders\";\nimport MenuItem from \"@/components/menu/M"
},
{
"path": "src/app/menu-items/edit/[id]/page.js",
"chars": 2748,
"preview": "'use client';\nimport DeleteButton from \"@/components/DeleteButton\";\nimport Left from \"@/components/icons/Left\";\nimport E"
},
{
"path": "src/app/menu-items/new/page.js",
"chars": 1733,
"preview": "'use client';\nimport Left from \"@/components/icons/Left\";\nimport Right from \"@/components/icons/Right\";\nimport EditableI"
},
{
"path": "src/app/menu-items/page.js",
"chars": 1707,
"preview": "'use client';\nimport Right from \"@/components/icons/Right\";\nimport UserTabs from \"@/components/layout/UserTabs\";\nimport "
},
{
"path": "src/app/orders/[id]/page.js",
"chars": 2513,
"preview": "'use client';\nimport {CartContext, cartProductPrice} from \"@/components/AppContext\";\nimport AddressInputs from \"@/compon"
},
{
"path": "src/app/orders/page.js",
"chars": 2306,
"preview": "'use client';\nimport SectionHeaders from \"@/components/layout/SectionHeaders\";\nimport UserTabs from \"@/components/layout"
},
{
"path": "src/app/page.js",
"chars": 1515,
"preview": "import Header from \"@/components/layout/Header\";\nimport Hero from \"@/components/layout/Hero\";\nimport HomeMenu from \"@/co"
},
{
"path": "src/app/profile/page.js",
"chars": 1986,
"preview": "'use client';\nimport EditableImage from \"@/components/layout/EditableImage\";\nimport InfoBox from \"@/components/layout/In"
},
{
"path": "src/app/register/page.js",
"chars": 2492,
"preview": "\"use client\";\nimport {signIn} from \"next-auth/react\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimpo"
},
{
"path": "src/app/users/[id]/page.js",
"chars": 1499,
"preview": "'use client';\nimport UserForm from \"@/components/layout/UserForm\";\nimport UserTabs from \"@/components/layout/UserTabs\";\n"
},
{
"path": "src/app/users/page.js",
"chars": 1447,
"preview": "'use client';\nimport UserTabs from \"@/components/layout/UserTabs\";\nimport {useProfile} from \"@/components/UseProfile\";\ni"
},
{
"path": "src/components/AppContext.js",
"chars": 1905,
"preview": "'use client';\nimport {SessionProvider} from \"next-auth/react\";\nimport {createContext, useEffect, useState} from \"react\";"
},
{
"path": "src/components/DeleteButton.js",
"chars": 954,
"preview": "import {useState} from \"react\";\n\nexport default function DeleteButton({label,onDelete}) {\n const [showConfirm, setShowC"
},
{
"path": "src/components/UseProfile.js",
"chars": 395,
"preview": "import {useEffect, useState} from \"react\";\n\nexport function useProfile() {\n const [data, setData] = useState(false);\n "
},
{
"path": "src/components/icons/Bars2.js",
"chars": 315,
"preview": "export default function Bars2({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none"
},
{
"path": "src/components/icons/ChevronDown.js",
"chars": 321,
"preview": "export default function ChevronDown({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill"
},
{
"path": "src/components/icons/ChevronUp.js",
"chars": 317,
"preview": "export default function ChevronUp({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\""
},
{
"path": "src/components/icons/Left.js",
"chars": 351,
"preview": "export default function Left({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\""
},
{
"path": "src/components/icons/Plus.js",
"chars": 308,
"preview": "export default function Plus({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\""
},
{
"path": "src/components/icons/Right.js",
"chars": 352,
"preview": "export default function Right({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none"
},
{
"path": "src/components/icons/ShoppingCart.js",
"chars": 563,
"preview": "export default function ShoppingCart({className = \"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" f"
},
{
"path": "src/components/icons/Trash.js",
"chars": 695,
"preview": "export default function Trash({className=\"w-6 h-6\"}) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none"
},
{
"path": "src/components/layout/AddressInputs.js",
"chars": 1433,
"preview": "export default function AddressInputs({addressProps,setAddressProp,disabled=false}) {\n const {phone, streetAddress, pos"
},
{
"path": "src/components/layout/EditableImage.js",
"chars": 1311,
"preview": "import Image from \"next/image\";\nimport toast from \"react-hot-toast\";\n\nexport default function EditableImage({link, setLi"
},
{
"path": "src/components/layout/Header.js",
"chars": 3511,
"preview": "'use client';\nimport {CartContext} from \"@/components/AppContext\";\nimport Bars2 from \"@/components/icons/Bars2\";\nimport "
},
{
"path": "src/components/layout/Hero.js",
"chars": 1184,
"preview": "import Right from \"@/components/icons/Right\";\nimport Image from \"next/image\";\n\nexport default function Hero() {\n return"
},
{
"path": "src/components/layout/HomeMenu.js",
"chars": 1278,
"preview": "'use client';\nimport SectionHeaders from \"@/components/layout/SectionHeaders\";\nimport MenuItem from \"@/components/menu/M"
},
{
"path": "src/components/layout/InfoBox.js",
"chars": 175,
"preview": "export default function InfoBox({children}) {\n return (\n <div className=\"text-center bg-blue-100 p-4 rounded-lg bord"
},
{
"path": "src/components/layout/MenuItemForm.js",
"chars": 2860,
"preview": "import Plus from \"@/components/icons/Plus\";\nimport Trash from \"@/components/icons/Trash\";\nimport EditableImage from \"@/c"
},
{
"path": "src/components/layout/MenuItemPriceProps.js",
"chars": 2416,
"preview": "import ChevronDown from \"@/components/icons/ChevronDown\";\nimport ChevronUp from \"@/components/icons/ChevronUp\";\nimport P"
},
{
"path": "src/components/layout/SectionHeaders.js",
"chars": 295,
"preview": "export default function SectionHeaders({subHeader,mainHeader}) {\n return (\n <>\n <h3 className=\"uppercase text-g"
},
{
"path": "src/components/layout/SuccessBox.js",
"chars": 180,
"preview": "export default function SuccessBox({children}) {\n return (\n <div className=\"text-center bg-green-100 p-4 rounded-lg "
},
{
"path": "src/components/layout/UserForm.js",
"chars": 2616,
"preview": "'use client';\nimport AddressInputs from \"@/components/layout/AddressInputs\";\nimport EditableImage from \"@/components/lay"
},
{
"path": "src/components/layout/UserTabs.js",
"chars": 1069,
"preview": "'use client';\nimport Link from \"next/link\";\nimport {usePathname} from \"next/navigation\";\n\nexport default function UserTa"
},
{
"path": "src/components/menu/AddToCartButton.js",
"chars": 678,
"preview": "import FlyingButton from 'react-flying-item'\n\nexport default function AddToCartButton({\n hasSizesOrExtras, onClick, bas"
},
{
"path": "src/components/menu/CartProduct.js",
"chars": 1240,
"preview": "import {cartProductPrice} from \"@/components/AppContext\";\nimport Trash from \"@/components/icons/Trash\";\nimport Image fro"
},
{
"path": "src/components/menu/MenuItem.js",
"chars": 4595,
"preview": "import {CartContext} from \"@/components/AppContext\";\nimport MenuItemTile from \"@/components/menu/MenuItemTile\";\nimport I"
},
{
"path": "src/components/menu/MenuItemTile.js",
"chars": 916,
"preview": "import AddToCartButton from \"@/components/menu/AddToCartButton\";\n\nexport default function MenuItemTile({onAddToCart, ..."
},
{
"path": "src/libs/datetime.js",
"chars": 89,
"preview": "export function dbTimeForHuman(str) {\n\n return str.replace('T', ' ').substring(0, 16);\n}"
},
{
"path": "src/libs/mongoConnect.js",
"chars": 1010,
"preview": "// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb\nimport { MongoClient "
},
{
"path": "src/models/Category.js",
"chars": 226,
"preview": "import {model, models, Schema} from \"mongoose\";\n\nconst CategorySchema = new Schema({\n name: {type:String, required:true"
},
{
"path": "src/models/MenuItem.js",
"chars": 516,
"preview": "import mongoose, {model, models, Schema} from \"mongoose\";\n\nconst ExtraPriceSchema = new Schema({\n name: String,\n price"
},
{
"path": "src/models/Order.js",
"chars": 358,
"preview": "import {model, models, Schema} from \"mongoose\";\n\nconst OrderSchema = new Schema({\n userEmail: String,\n phone: String,\n"
},
{
"path": "src/models/User.js",
"chars": 300,
"preview": "import {model, models, Schema} from \"mongoose\";\n\nconst UserSchema = new Schema({\n name: {type: String},\n email: {type:"
},
{
"path": "src/models/UserInfo.js",
"chars": 410,
"preview": "import {model, models, Schema} from \"mongoose\";\n\nconst UserInfoSchema = new Schema({\n email: {type: String, required: t"
},
{
"path": "tailwind.config.js",
"chars": 323,
"preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n content: [\n './src/pages/**/*.{js,ts,jsx,tsx,mdx}',\n"
},
{
"path": "tsconfig.json",
"chars": 674,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],\n \"paths\": {\n \"@/*"
}
]
About this extraction
This page contains the full source code of the dejwid/food-ordering GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (80.9 KB), approximately 22.8k tokens, and a symbol index with 63 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.