├── .dockerignore
├── .env
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .wakatime-project
├── Dockerfile
├── README.md
├── app
├── about
│ └── page.tsx
├── api
│ ├── file
│ │ └── [name]
│ │ │ └── route.ts
│ ├── recipes
│ │ ├── get-single-recipe
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── route.ts
│ └── search
│ │ └── route.ts
├── contact
│ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── not-found.tsx
├── page.tsx
├── recipes
│ ├── [id]
│ │ └── page.tsx
│ ├── create
│ │ └── page.tsx
│ ├── page.tsx
│ └── search
│ │ └── [q]
│ │ └── page.tsx
└── sitemap.ts
├── components
├── Footer.tsx
├── Header.tsx
├── HeaderBottom.tsx
├── LandingSection.tsx
├── LatestRecipes.tsx
├── RecipeCard.tsx
├── Search.tsx
├── SingleRecipeMain.tsx
├── SingleRecipeTopSection.tsx
└── ui
│ ├── BackButton.tsx
│ ├── Button.tsx
│ └── Skeleton.tsx
├── denizpaz.ir.conf
├── docker-compose.yml
├── environment.d.ts
├── lib
├── data.ts
└── mongodb.ts
├── models
├── index.ts
└── recipe.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── contactus.jpg
├── facebook-2.svg
├── facebook.png
├── header-recipe.png
├── instagram.png
├── landing-header-image.png
├── linkedin.png
├── logo.png
├── me.png
├── recipe-header.jpg
├── search-page.jpg
└── twitter.png
├── seeder.ts
├── styles
├── fonts.ts
└── fonts
│ ├── ttf
│ ├── Vazirmatn-Black.ttf
│ ├── Vazirmatn-Bold.ttf
│ ├── Vazirmatn-ExtraBold.ttf
│ ├── Vazirmatn-ExtraLight.ttf
│ ├── Vazirmatn-Light.ttf
│ ├── Vazirmatn-Medium.ttf
│ ├── Vazirmatn-Regular.ttf
│ ├── Vazirmatn-SemiBold.ttf
│ └── Vazirmatn-Thin.ttf
│ ├── variable
│ └── Vazirmatn[wght].ttf
│ └── webfonts
│ ├── Vazirmatn-Black.woff2
│ ├── Vazirmatn-Bold.woff2
│ ├── Vazirmatn-ExtraBold.woff2
│ ├── Vazirmatn-ExtraLight.woff2
│ ├── Vazirmatn-Light.woff2
│ ├── Vazirmatn-Medium.woff2
│ ├── Vazirmatn-Regular.woff2
│ ├── Vazirmatn-SemiBold.woff2
│ ├── Vazirmatn-Thin.woff2
│ └── Vazirmatn[wght].woff2
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.seeder.json
└── types
├── Components.ts
├── Generic.ts
├── Recipe.ts
└── mongodb.d.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | node_modules
4 | npm-debug.log
5 | README.md
6 | .next
7 | .git
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # DO NOT ADD SECRETS TO THIS FILE. This is a good place for defaults.
2 | # If you want to add secrets use `.env.local` instead.
3 |
4 | ENV_VARIABLE=production_server_only_variable
5 | NEXT_PUBLIC_ENV_VARIABLE=production_public_variable
6 | NEXT_PUBLIC_DB_LOCAL_URL="mongodb://localhost:27017/denizpaz"
7 | PORT=3000
8 | STORE_PATH=upload
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # DO NOT ADD SECRETS TO THIS FILE. This is a good place for defaults.
2 | # If you want to add secrets use `.env.local` instead.
3 |
4 | ENV_VARIABLE=production_server_only_variable
5 | NEXT_PUBLIC_ENV_VARIABLE=production_public_variable
6 | NEXT_PUBLIC_DB_LOCAL_URL="mongodb://localhost:27017/denizpaz"
7 | PORT=3000
8 | STORE_PATH=upload
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | /data
37 | public/uploads/recipes/*
38 | upload/*
--------------------------------------------------------------------------------
/.wakatime-project:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/.wakatime-project
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine
2 |
3 | RUN mkdir -p /app
4 |
5 | WORKDIR /app
6 |
7 | COPY package.json /app
8 |
9 | RUN npm install -g next
10 |
11 | RUN npm install
12 |
13 | COPY . /app
14 |
15 | RUN mkdir -p /app/.next/cache/images
16 | VOLUME /app/.next/cache/images
17 |
18 | RUN npm run build
19 |
20 | ENV DB_LOCAL_URL=mongodb://mongo:27017/denizpaz
21 |
22 | EXPOSE 3000
23 |
24 | CMD npm run start
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js App with Docker and MongoDB Integration
2 |
3 | This repository contains a Next.js web application that utilizes various features, including appdir, Docker for containerization, MongoDB for data storage, and custom ul components.
4 |
5 | ## Features
6 |
7 | - **Next.js Appdir Feature:** The Next.js app in this repository is organized using the appdir feature, allowing for a clean and structured project layout.
8 |
9 | - **Docker Support:** The application is containerized using Docker, which simplifies the setup process and ensures consistent deployment across different environments.
10 |
11 | - **MongoDB Integration:** MongoDB is used as the database for this application, enabling efficient and scalable data storage for the web application.
12 |
13 | - **Tailwindcss Components:** The repository includes Tailwindcss styles for components that can be easily reused throughout the application.
14 |
15 | ## Prerequisites
16 |
17 | Before you begin, make sure you have the following dependencies installed on your system:
18 |
19 | - Node.js and npm (Node Package Manager)
20 | - Docker
21 |
22 | ## Getting Started
23 |
24 | Follow the steps below to set up the Next.js app on your local machine:
25 |
26 | 1. Clone this repository to your local machine using the following command:
27 |
28 | ```bash
29 | git clone https://github.com/ehsanghaffar/nextjs-appdir-docker
30 | ```
31 |
32 | 2. Navigate to the project directory:
33 |
34 | ```bash
35 | cd nextjs-appdir-docker
36 | ```
37 |
38 | 3. Install the required dependencies:
39 |
40 | ```bash
41 | npm install
42 | ```
43 |
44 | 4. Create a `.env` file in the root directory and provide the necessary environment variables:
45 |
46 | ```
47 | MONGODB_URI=mongodb://your-mongodb-uri
48 | ```
49 |
50 | 5. Run the Next.js development server:
51 |
52 | ```bash
53 | npm run dev
54 | ```
55 |
56 | The application should now be running on `http://localhost:3000`.
57 |
58 | ## Docker Deployment
59 |
60 | To deploy the application using Docker, follow these steps:
61 |
62 | 1. Ensure Docker is installed and running on your system.
63 |
64 | 2. Build and run the Docker image using the provided Docker compose:
65 |
66 | ```bash
67 | docker compose up -d
68 | ```
69 |
70 | The application should now be accessible at `http://localhost:3000`.
71 |
72 | ## MongoDB Configuration
73 |
74 | Make sure you have a running MongoDB instance and obtain the connection URI. Replace the `MONGODB_URI` in the `.env` file with your MongoDB connection string.
75 |
76 | ## Custom UL Components
77 |
78 | The repository includes custom `ul` (unordered list) components that can be found in the `components` directory. You can easily use these components in your pages to display lists.
79 |
80 | ```jsx
81 | import React from 'react';
82 | import { CustomUl } from '../components';
83 |
84 | const YourPage = () => {
85 | return (
86 |
87 |
Your Page Title
88 |
89 |
90 | );
91 | };
92 |
93 | export default YourPage;
94 | ```
95 |
96 | Feel free to explore the code and make any modifications or enhancements you need for your project.
97 |
98 | ## License
99 |
100 | This project is licensed under the [MIT License](LICENSE).
101 |
102 | ---
103 |
104 | Happy coding! If you have any questions or need further assistance, feel free to reach out.
105 |
--------------------------------------------------------------------------------
/app/about/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const AboutUs: React.FC = () => {
4 | return (
5 |
6 |
7 |
8 |
About Us
9 |
10 |
11 | Welcome to Foodie Delights - Your Go-To Recipe App!
12 |
13 |
14 |
15 |
16 |
Cooker
17 |
18 | At Foodie Delights, we believe that food brings people together, creates memories, and sparks joy. Our mission is to inspire and empower both aspiring and seasoned chefs to explore the world of culinary delights right from the comfort of their homes. Whether you are a cooking enthusiast or just getting started in the kitchen, Foodie Delights has something for everyone.
19 |
20 |
21 |
22 | Our Recipe Collection
23 |
24 |
25 | We take pride in curating a diverse and extensive recipe collection that spans cuisines from all corners of the globe. Our team of passionate food experts works tirelessly to bring you the most delicious and authentic recipes that are easy to follow and recreate. From traditional family favorites to contemporary twists on classic dishes, there is always something new to explore.
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default AboutUs;
35 |
--------------------------------------------------------------------------------
/app/api/file/[name]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { existsSync } from "fs";
3 | import fs from "fs/promises";
4 | import path from "path";
5 | import mime from "mime/lite";
6 |
7 | export async function GET(
8 | req: NextRequest,
9 | { params }: { params: { name: string } }
10 | ) {
11 | const filePath = path.join(
12 | process.cwd(),
13 | process.env.STORE_PATH!,
14 | params.name
15 | );
16 | if (!existsSync(filePath)) {
17 | return NextResponse.json({ msg: "Not found" }, { status: 404 });
18 | }
19 |
20 | const mimeType = mime.getType(filePath);
21 | const fileStat = await fs.stat(filePath);
22 |
23 | const file = await fs.readFile(filePath);
24 |
25 | const headers: [string, string][] = [
26 | ["Content-Length", fileStat.size.toString()],
27 | ];
28 | if (mimeType) {
29 | headers.push(["Content-Type", mimeType]);
30 | }
31 | return new NextResponse(file, {
32 | headers,
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/app/api/recipes/get-single-recipe/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { Recipe } from "../../../../models";
3 |
4 | export const dynamic = 'force-dynamic'
5 | export async function GET(req: Request) {
6 | try {
7 | const { searchParams } = new URL(req.url);
8 | const id = searchParams.get('id')
9 |
10 | const recipe = await Recipe.findOne({
11 | _id: id
12 | });
13 |
14 | if (await recipe) {
15 | return NextResponse.json({ status: 200, data: recipe });
16 | } else {
17 | return NextResponse.json({ status: 204, success: false, message: 'No recipe found.' });
18 | }
19 | } catch (error) {
20 | console.log('Error in getting recipe by id:', error);
21 | return NextResponse.json({ status: 500, success: false, message: error });
22 | }
23 | }
--------------------------------------------------------------------------------
/app/api/recipes/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { connectToDatabase } from "../../../lib/mongodb";
3 | import { Recipe } from "../../../models";
4 | import { existsSync } from "fs";
5 | import fs from "fs/promises";
6 | import path from "path";
7 |
8 | connectToDatabase();
9 |
10 | export async function GET() {
11 | try {
12 | const recipes = Recipe.find();
13 | return NextResponse.json((await recipes).reverse());
14 | } catch (error) {
15 | console.log(error);
16 | return NextResponse.json('error', {
17 | status: 500
18 | });
19 | }
20 | }
21 |
22 | export async function POST(req: Request) {
23 | const data = await req.formData();
24 | const file: File | null = data.get('photo') as unknown as File;
25 |
26 | const fileArrayBuffer = await file.arrayBuffer();
27 | const destinationDirPath = path.join(process.cwd(), process.env.STORE_PATH!);
28 |
29 | if (!existsSync(destinationDirPath)) {
30 | await fs.mkdir(destinationDirPath, { recursive: true });
31 | }
32 |
33 | let name = data.get('name')
34 | var fileExtension = file.name.split('.').pop();
35 | let filename = `${name}.${fileExtension}`
36 | while (existsSync(path.join(destinationDirPath, filename))) {
37 | filename = `(1)` + filename;
38 | }
39 | await fs.writeFile(
40 | path.join(destinationDirPath, filename),
41 | Buffer.from(fileArrayBuffer)
42 | );
43 | try {
44 | const newRecipe = {
45 | name: data.get('name'),
46 | description: data.get('description'),
47 | ingredients: JSON.parse(data.get('ingredients') as string),
48 | steps: data.get('steps'),
49 | photo: `/api/file/${filename}`
50 | }
51 |
52 | const recipe = new Recipe(newRecipe);
53 | const save = await recipe.save();
54 | return NextResponse.json({ status: 200, data: save });
55 | } catch (error) {
56 | console.log(error);
57 | return NextResponse.json('error', {
58 | status: 500,
59 | });
60 | }
61 |
62 | }
63 |
64 | export async function DELETE(req: any) {
65 | const { id } = req.query;
66 | try {
67 | console.log("id", id)
68 | const recipe = await Recipe.findByIdAndRemove(id)
69 | console.log("re", recipe)
70 | return NextResponse.json({ status: 200, data: recipe })
71 | } catch (error) {
72 | return NextResponse.json({ status: 500, success: false, message: error });
73 | }
74 | }
--------------------------------------------------------------------------------
/app/api/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | export async function GET() {
4 | const user = {
5 | name: "ehsan"
6 | }
7 |
8 | return NextResponse.json({ user });
9 | }
--------------------------------------------------------------------------------
/app/api/search/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { connectToDatabase } from "../../../lib/mongodb";
3 | import { Recipe } from "../../../models";
4 |
5 | export const dynamic = 'force-dynamic'
6 | connectToDatabase()
7 |
8 | export async function GET(req: Request) {
9 | try {
10 | const { searchParams } = new URL(req.url);
11 | const query = searchParams.get('q')
12 | const recipes = await Recipe.find()
13 |
14 | const recipesData = recipes.filter((recipe) => {
15 | return recipe["name"].includes(query)
16 | })
17 | return NextResponse.json(recipesData);
18 | } catch (error) {
19 | console.log(error)
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/contact/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Contact = () => {
4 | return (
5 | <>
6 |
30 | >
31 | );
32 | };
33 |
34 | export default Contact;
35 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | } */
18 |
19 |
20 | html {
21 | line-height: 1.15;
22 | -webkit-text-size-adjust: 100%;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | }
28 |
29 | main {
30 | display: block;
31 | }
32 |
33 | h1 {
34 | font-size: 2em;
35 | margin: 0.67em 0;
36 | }
37 |
38 | hr {
39 | box-sizing: content-box;
40 | height: 0;
41 | overflow: visible;
42 | }
43 |
44 |
45 | a {
46 | background-color: transparent;
47 | }
48 |
49 | abbr[title] {
50 | border-bottom: none;
51 | text-decoration: underline;
52 | text-decoration: underline dotted;
53 | }
54 |
55 | b,
56 | strong {
57 | font-weight: bolder;
58 | }
59 |
60 |
61 | small {
62 | font-size: 80%;
63 | }
64 |
65 | sub,
66 | sup {
67 | font-size: 75%;
68 | line-height: 0;
69 | position: relative;
70 | vertical-align: baseline;
71 | }
72 |
73 | sub {
74 | bottom: -0.25em;
75 | }
76 |
77 | sup {
78 | top: -0.5em;
79 | }
80 |
81 | img {
82 | border-style: none;
83 | }
84 |
85 | button,
86 | input,
87 | optgroup,
88 | select,
89 | textarea {
90 | /* font-family: inherit; */
91 | font-size: 100%;
92 | line-height: 1.15;
93 | margin: 0;
94 | }
95 |
96 | button,
97 | input {
98 | overflow: visible;
99 | }
100 |
101 | button,
102 | select {
103 | text-transform: none;
104 | }
105 |
106 | button,
107 | [type="button"],
108 | [type="reset"],
109 | [type="submit"] {
110 | -webkit-appearance: button;
111 | }
112 |
113 | button::-moz-focus-inner,
114 | [type="button"]::-moz-focus-inner,
115 | [type="reset"]::-moz-focus-inner,
116 | [type="submit"]::-moz-focus-inner {
117 | border-style: none;
118 | padding: 0;
119 | }
120 |
121 | button:-moz-focusring,
122 | [type="button"]:-moz-focusring,
123 | [type="reset"]:-moz-focusring,
124 | [type="submit"]:-moz-focusring {
125 | outline: 1px dotted ButtonText;
126 | }
127 |
128 | fieldset {
129 | padding: 0.35em 0.75em 0.625em;
130 | }
131 |
132 | legend {
133 | box-sizing: border-box;
134 | color: inherit;
135 | display: table;
136 | max-width: 100%;
137 | padding: 0;
138 | /* 3 */
139 | white-space: normal;
140 | }
141 |
142 | progress {
143 | vertical-align: baseline;
144 | }
145 |
146 | textarea {
147 | overflow: auto;
148 | }
149 |
150 | [type="checkbox"],
151 | [type="radio"] {
152 | box-sizing: border-box;
153 | padding: 0;
154 | }
155 |
156 | [type="number"]::-webkit-inner-spin-button,
157 | [type="number"]::-webkit-outer-spin-button {
158 | height: auto;
159 | }
160 |
161 | [type="search"]::-webkit-search-decoration {
162 | -webkit-appearance: none;
163 | }
164 |
165 | ::-webkit-file-upload-button {
166 | -webkit-appearance: button;
167 | font: inherit;
168 | }
169 |
170 | details {
171 | display: block;
172 | }
173 |
174 | summary {
175 | display: list-item;
176 | }
177 |
178 | template {
179 | display: none;
180 | }
181 |
182 | [hidden] {
183 | display: none;
184 | }
185 |
186 | /* Start Project */
187 |
188 | html {
189 | box-sizing: border-box;
190 | }
191 |
192 | *,
193 | *:before,
194 | *:after {
195 | box-sizing: inherit;
196 | }
197 |
198 | .container {
199 | max-width: 80rem;
200 | margin-right: auto;
201 | margin-left: auto;
202 | padding-right: 1rem;
203 | padding-left: 1rem;
204 | }
205 |
206 | .section-title {
207 | font-size: 3rem;
208 | color: #e32a22;
209 | text-align: center;
210 | }
211 |
212 | .section-des {
213 | color: #525c60;
214 | font-size: 1.35rem;
215 | text-align: center;
216 | padding-bottom: 2rem;
217 | }
218 |
219 | /* header nav bar */
220 |
221 | #header-top {
222 | background-color: #f9f8f8;
223 | }
224 |
225 | .header-navbar {
226 | display: flex;
227 | justify-content: space-between;
228 | align-items: center;
229 | padding: 0.5rem 0;
230 | }
231 |
232 | .navbar-img {
233 | max-width: 10rem;
234 | max-height: 4rem;
235 | }
236 |
237 | .menu {
238 | display: flex;
239 | list-style-type: none;
240 | }
241 |
242 | .menu li {
243 | padding: 0 1.5rem;
244 | }
245 |
246 | .menu li:not(:last-child) {
247 | border-right: 0.1rem solid #86898a98;
248 | }
249 |
250 | .menu li a {
251 | transition: all 0.1s
252 | }
253 |
254 | .menu li a:hover {
255 | color: #e32a22;
256 | }
257 |
258 | .menu-links {
259 | text-decoration: none;
260 | color: #274C5B;
261 | font-size: 1.3rem;
262 | font-weight: bold;
263 | }
264 |
265 | /* main header */
266 |
267 | #header-bottom {
268 | display: flex;
269 | justify-content: space-around;
270 | padding: 4rem 0;
271 | }
272 |
273 | .header-right {
274 | padding-right: 8rem;
275 | /* background-image: url(/images/bobl.png); */
276 | background-position: center;
277 | background-repeat: no-repeat;
278 |
279 |
280 | }
281 |
282 | .header-des {
283 | color: #525c60;
284 | font-size: 1.35rem;
285 | font-weight: 600;
286 | }
287 |
288 |
289 | .header-link:hover {
290 | background-color: #12232b;
291 | }
292 |
293 | .header-link-img {
294 | transform: rotate(180deg);
295 | width: 1.7rem;
296 | padding-left: 0.4rem;
297 | }
298 |
299 | .header-left-img {
300 | max-width: 35rem;
301 | }
302 |
303 | /* recipes section */
304 |
305 | #recipes {
306 | background-color: #F9f8f8;
307 | padding: 1.5rem 0;
308 | text-align: center;
309 | }
310 |
311 | #recipes .section-des {
312 | margin: 0;
313 | }
314 |
315 | .recipes {
316 | display: flex;
317 | justify-content: space-between;
318 | flex-wrap: wrap;
319 | padding: 2rem 2rem;
320 | }
321 |
322 | .recipe {
323 | padding: 1rem 1rem 2rem 1rem;
324 | margin: 2rem 1rem;
325 | border-radius: 1rem;
326 | background-color: #ffffff;
327 | box-shadow: 2px 5px 4px 2px rgba(0, 0, 0, 0.2);
328 | transition: all 0.3s;
329 | }
330 |
331 | .recipe:hover {
332 | transform: translateY(-0.3rem);
333 | }
334 |
335 | .recipe-img {
336 | max-width: 14rem;
337 | height: auto;
338 | border-radius: 1rem;
339 | }
340 |
341 | .recipe-des {
342 | text-align: center;
343 | }
344 |
345 | .recipe-title {
346 | color: #274C5B;
347 | font-weight: bold;
348 | padding-bottom: 0.7rem;
349 | }
350 |
351 | .recipe-link {
352 | text-decoration: none;
353 | font-size: 1rem;
354 | color: #fff;
355 | background-color: #274C5B;
356 | padding: 0.5rem 2rem;
357 | border-radius: 0.5rem;
358 | margin-top: 1rem;
359 | transition: all 0.2s;
360 | }
361 |
362 | .recipe-link:hover {
363 | background-color: #12232b;
364 | }
365 | /*
366 | .recipe-link-img {
367 | transform: rotate(180deg);
368 | width: 1.3rem;
369 | padding-left: 0.4rem;
370 | } */
371 |
372 | .all-recipe-link {
373 | font-size: 1.7rem;
374 | color: #fff;
375 | background-color: #274C5B;
376 | padding: 1rem 2rem;
377 | border-radius: 0.8rem;
378 | cursor: pointer;
379 | text-decoration: none;
380 | margin: 3rem auto;
381 | transition: all 0.2s;
382 | }
383 |
384 | .all-recipe-link:hover {
385 | background-color: #12232b;
386 | }
387 |
388 | .all-recipe-link-img {
389 | width: 2rem;
390 | }
391 |
392 |
393 | /* about us section */
394 |
395 | /* #aboutus {
396 | background-image: url(/images/aboutus.jpg);
397 | background-repeat: no-repeat;
398 | background-size: cover;
399 | height: 100vh;
400 | padding: 1.5rem 0;
401 | } */
402 |
403 | #aboutus .section-title {
404 | margin: 0;
405 | }
406 |
407 | .aboutus-info {
408 | text-align: center;
409 | max-width: 35rem;
410 | margin: 0 auto;
411 | }
412 |
413 | .aboutus-info-title {
414 | font-weight: bold;
415 | font-size: 2rem;
416 | color: #274C5B;
417 | margin: 0.5rem 0
418 | }
419 |
420 | .aboutus-info-info {
421 | color: #525c60;
422 | font-size: 1.3rem;
423 | padding-bottom: 0.5rem;
424 | }
425 |
426 | .aboutus-counts {
427 | display: flex;
428 | justify-content: space-around;
429 | padding: 2rem 8rem;
430 | align-items: center;
431 | text-align: center;
432 | }
433 |
434 | .aboutus-count {
435 | background-color: #F1F1F1;
436 | width: 12rem;
437 | height: 12rem;
438 | border: 0.15rem solid #7EB693;
439 | border-radius: 10rem;
440 | display: flex;
441 | flex-direction: column;
442 | justify-content: center;
443 | transition: all 0.3s;
444 | }
445 |
446 | .aboutus-count:hover {
447 | transform: translateY(-0.3rem);
448 | }
449 |
450 |
451 |
452 | .aboutus-count-num {
453 | color: #274C5B;
454 | font-size: 3rem;
455 | font-weight: bold;
456 | margin: 0;
457 | }
458 |
459 | .aboutus-count-des {
460 | color: #274C5B;
461 | font-size: 1.1rem;
462 | font-weight: bold;
463 | margin: 0;
464 | }
465 |
466 | /* contactus section */
467 |
468 | #contactus {
469 | padding: 5.5rem 0;
470 | background-color: #f9f8f8;
471 | }
472 |
473 | .contactus {
474 | display: flex;
475 | justify-content: space-between;
476 | }
477 |
478 | .contactus-right .section-title {
479 | text-align: right;
480 | }
481 |
482 |
483 | .contactus-info {
484 | display: flex;
485 | align-items: center;
486 | margin-bottom: 2rem;
487 | }
488 |
489 | .contactus-info-info {
490 | margin-right: 0.8rem;
491 | }
492 |
493 | .contactus-info-info-title {
494 | margin: 0;
495 | padding-bottom: 0.5rem;
496 | font-weight: bold;
497 | font-size: 1.5rem;
498 | color: #274C5B;
499 | margin: 0;
500 | padding-bottom: 0.4rem;
501 | }
502 |
503 | .contactus-info-info-des {
504 | margin: 0;
505 | color: #525c60;
506 | transition: all 0.2s;
507 | }
508 |
509 | .contactus-info-info-des:hover {
510 | font-weight: bold;
511 | }
512 |
513 |
514 | .contactus-info-image {
515 | background-color: #F4F4F4;
516 | padding: 1rem;
517 | border-radius: 0.7rem;
518 | }
519 |
520 | .contactus-right-info-img {
521 | width: 2.5rem;
522 | }
523 |
524 | /* subscribe section */
525 |
526 | #subscribe {
527 | padding: 5rem 0 10rem 0;
528 | }
529 |
530 |
531 | /*
532 | .subscribe-img {
533 | background-image: url(/images/subsc.jpg);
534 | background-size: cover;
535 | height: 20rem;
536 | border-radius: 5rem;
537 | } */
538 |
539 | .subscribe-input {
540 | border: none;
541 | outline: none;
542 | width: 21rem;
543 | padding: 1.2rem 0.8rem;
544 | border-radius: 1.2rem;
545 | text-align: left;
546 | }
547 |
548 | .subscribe-input::placeholder {
549 | color: #585e612c;
550 | }
551 |
552 | .subscribe-btn {
553 | border: none;
554 | outline: none;
555 | background-color: #274C5B;
556 | color: #fff;
557 | padding: 1.2rem 3rem;
558 | border-radius: 1.2rem;
559 | margin-top: 7.7rem;
560 | margin-right: 10rem;
561 | cursor: pointer;
562 | transition: all 0.2s;
563 | }
564 |
565 | .subscribe-btn:hover {
566 | background-color: #12232b;
567 | }
568 |
569 |
570 |
571 | /* footer section */
572 |
573 | #footer {
574 | position: relative;
575 | background-color: #f9f8f8;
576 | }
577 |
578 | /* .footer .svg-bg {
579 | position: absolute;
580 | z-index: -1;
581 | bottom: 23.8rem;
582 | width: 100%;
583 | } */
584 |
585 | .footer {
586 | display: flex;
587 | justify-content: space-around;
588 | padding: 3rem 0;
589 | }
590 |
591 | .footer-title {
592 | color: #e32a22;
593 | font-size: 1.4rem;
594 | }
595 |
596 | .footer-pages ul {
597 | list-style-type: none;
598 | }
599 |
600 | .footer-pages ul li {
601 | padding: 0.7rem 0;
602 | }
603 |
604 | .footer-pages-links {
605 | text-decoration: none;
606 | color: #274C5B;
607 | transition: all 0.1s;
608 | }
609 |
610 | .footer-pages-links:hover {
611 | color: #e32a22;
612 | font-weight: bold;
613 | }
614 |
615 | /* -------------------------- */
616 |
617 | .footer-media {
618 | padding-top: 1.7rem;
619 | flex-basis: 30%;
620 | text-align: center;
621 | }
622 |
623 | .footer-img {
624 | max-width: 10rem;
625 | max-height: 4rem;
626 | text-align: center;
627 | margin-bottom: 1rem;
628 | }
629 |
630 |
631 | .footer-media-des {
632 | color: #274C5B;
633 | font-weight: bold;
634 | padding-bottom: 2rem;
635 | }
636 |
637 | .footer-media-medias {
638 | display: flex;
639 | justify-content: space-around;
640 | align-items: center;
641 | }
642 |
643 | .footer-media-media {
644 | /* padding: 1rem; */
645 | width: 4.5rem;
646 | height: 4.5rem;
647 | background-color: #f9f8f8;
648 | border-radius: 50%;
649 | box-shadow: 2px 5px 4px 2px rgba(0, 0, 0, 0.2);
650 | text-align: center;
651 | position: relative;
652 | overflow: hidden;
653 | color: #fd4848;
654 | }
655 |
656 | .footer-media-media .fa {
657 | line-height: 4.5rem;
658 | font-size: 2rem;
659 | transition: all 0.5s;
660 | }
661 |
662 | .footer-media-media::before {
663 | content: '';
664 | position: absolute;
665 | width: 100%;
666 | height: 100%;
667 | border-radius: 50%;
668 | background-color: #fd4848;
669 | top: -4rem;
670 | right: -3.5rem;
671 | }
672 |
673 | .footer-media-media:hover:before {
674 | animation: mtb 1s forwards;
675 | }
676 |
677 | .footer-media-media:hover .fa {
678 | color: #f9f8f8;
679 | transform: scale(1.1);
680 | }
681 |
682 | @keyframes mtb {
683 | 0% {
684 | top: -100px;
685 | right: -80px;
686 | }
687 |
688 | 70% {
689 | top: 20px;
690 | right: 20px;
691 | }
692 |
693 | 100% {
694 | top: 0;
695 | right: 0;
696 | }
697 | }
698 |
699 | /* ------------------------- */
700 |
701 | .footer-contactus-sub {
702 | font-weight: bold;
703 | color: #274C5B;
704 | margin: 0;
705 | padding-bottom: 0.4rem;
706 | }
707 |
708 | .footer-contactus-des {
709 | color: #525c60;
710 | margin: 0;
711 | padding-bottom: 1.3rem;
712 | }
713 |
714 |
715 | /* ----------------------- */
716 |
717 | .copyright {
718 | border-top: 0.15rem solid #525c603d;
719 | }
720 |
721 | .copyright-text {
722 | font-size: 1rem;
723 | color: #525c6080;
724 | text-align: center;
725 | margin: 0;
726 | padding: 0.7rem 0;
727 | }
728 |
729 | /* ------------------------------------------------------------------------------------------------------------------------------------------------ */
730 | /* all recipe page */
731 |
732 | #header-pic {
733 | /* background-image: url(/images/header-recipe.png); */
734 | background-position: center;
735 | background-repeat: no-repeat;
736 | width: 100%;
737 | height: 19.375rem;
738 | text-align: center;
739 | }
740 |
741 | .header-pic-text {
742 | margin: 0;
743 | color: #274C5B;
744 | font-size: 4.25rem;
745 | padding-top: 4rem;
746 | padding-bottom: 3rem;
747 | }
748 |
749 | .search-box {
750 | display: inline-block;
751 | background-color: #fff;
752 | padding: 0.7rem 2rem;
753 | border-radius: 10rem;
754 | border: 0.1rem solid #96a2a794;
755 | box-shadow: 2px 5px 4px 2px rgba(0, 0, 0, 0.3);
756 | }
757 |
758 | .search-box input[type="text"] {
759 | padding: 0.5rem 1rem;
760 | width: 35rem;
761 | border: none;
762 | outline: none;
763 | font-weight: bold;
764 | }
765 |
766 | .search-box button {
767 | background-color: #fff;
768 | border: none;
769 | outline: none;
770 | cursor: pointer;
771 | font-size: 1.2rem;
772 | padding: 0.5rem 0.6rem;
773 | border-radius: 10rem;
774 | transition: 0.2s;
775 | }
776 |
777 | .search-box button:hover {
778 | background-color: #f3f3f3;
779 | }
780 |
781 | /* ------------------------------------------------------------------------------------------------------------------------------------------------ */
782 | /* recipe page */
783 |
784 | #recipe-header {
785 | position: relative;
786 | }
787 |
788 | .recipe-header-img {
789 | width: 100%;
790 | height: 400px;
791 | }
792 |
793 | .pRecipe-title {
794 | background-color: #fff;
795 | width: 70%;
796 | border-top-right-radius: 2.5rem;
797 | border-top-left-radius: 2.5rem;
798 | padding: 1.5rem 1rem;
799 | top: 8rem;
800 | right: 15%;
801 | position: absolute;
802 | }
803 |
804 | .back-link-img {
805 | width: 2rem;
806 | padding-right: 0.4rem;
807 | }
808 |
809 | .pRecipe-title .section-title {
810 | font-weight: 900;
811 | margin: 0;
812 | color: #274C5B;
813 | padding: 1rem 0;
814 | }
815 |
816 | .pRecipe-title .section-des {
817 | font-size: 1.1rem;
818 | line-height: 2rem;
819 | font-weight: bold;
820 | margin: 0;
821 | padding: 0;
822 | }
823 |
824 | .pRecipe {
825 | display: flex;
826 | flex-direction: column;
827 | align-items: center;
828 | }
829 |
830 | .pRecipe-top {
831 | margin-top: 2rem;
832 | display: flex;
833 | justify-content: space-between;
834 | align-items: center;
835 | margin-top: 5rem;
836 | width: 70%;
837 | }
838 |
839 | .pRecipe-img {
840 | max-width: 26rem;
841 | border-radius: 1rem;
842 | }
843 |
844 | .pRecipe-mavad ul li {
845 | font-size: 1.3rem;
846 | font-weight: bold;
847 | padding: 0.4rem 0;
848 | }
849 |
850 | .pRecipe-bottom .section-title {
851 | text-align: right;
852 | font-weight: bold;
853 | margin: 0;
854 | }
855 |
856 | .pRecipe-bottom {
857 | width: 70%;
858 | margin-top: 4rem;
859 | }
860 |
861 | .pRecipe-bottom ul li {
862 | font-size: 1.2rem;
863 | font-weight: 500;
864 | line-height: 2rem;
865 | padding: 0.6rem 0;
866 | }
867 |
868 |
869 |
870 |
871 |
872 |
873 | /* Project Responsive */
874 |
875 | @media only screen and (max-width: 1200px) {
876 | .container {
877 | max-width: 960px;
878 | }
879 | }
880 |
881 | @media only screen and (max-width: 992px) {
882 | .container {
883 | max-width: 720px;
884 | }
885 | }
886 |
887 | @media only screen and (max-width: 768px) {
888 | .container {
889 | max-width: 540px;
890 | }
891 | }
892 |
893 | @media only screen and (max-width: 576px) {
894 | .container {
895 | max-width: 100%;
896 | }
897 | }
898 |
899 | .glow {
900 | top: -10%;
901 | left: -10%;
902 | width: 120%;
903 | height: 120%;
904 | border-radius: 100%;
905 | }
906 |
907 | .glow-1 {
908 | animation: glow1 4s linear infinite;
909 | }
910 |
911 | .glow-2 {
912 | animation: glow2 4s linear infinite;
913 | animation-delay: 100ms;
914 | }
915 |
916 | .glow-3 {
917 | animation: glow3 4s linear infinite;
918 | animation-delay: 200ms;
919 | }
920 |
921 | .glow-4 {
922 | animation: glow4 4s linear infinite;
923 | animation-delay: 300ms;
924 | }
925 |
926 | @keyframes glow1 {
927 | 0% {
928 | transform: translate(10%, 10%) scale(1);
929 | }
930 | 25% {
931 | transform: translate(-10%, 10%) scale(1);
932 | }
933 | 50% {
934 | transform: translate(-10%, -10%) scale(1);
935 | }
936 | 75% {
937 | transform: translate(10%, -10%) scale(1);
938 | }
939 | 100% {
940 | transform: translate(10%, 10%) scale(1);
941 | }
942 | }
943 |
944 | @keyframes glow2 {
945 | 0% {
946 | transform: translate(-10%, -10%) scale(1);
947 | }
948 | 25% {
949 | transform: translate(10%, -10%) scale(1);
950 | }
951 | 50% {
952 | transform: translate(10%, 10%) scale(1);
953 | }
954 | 75% {
955 | transform: translate(-10%, 10%) scale(1);
956 | }
957 | 100% {
958 | transform: translate(-10%, -10%) scale(1);
959 | }
960 | }
961 |
962 | @keyframes glow3 {
963 | 0% {
964 | transform: translate(-10%, 10%) scale(1);
965 | }
966 | 25% {
967 | transform: translate(-10%, -10%) scale(1);
968 | }
969 | 50% {
970 | transform: translate(10%, -10%) scale(1);
971 | }
972 | 75% {
973 | transform: translate(10%, 10%) scale(1);
974 | }
975 | 100% {
976 | transform: translate(-10%, 10%) scale(1);
977 | }
978 | }
979 |
980 | @keyframes glow4 {
981 | 0% {
982 | transform: translate(10%, -10%) scale(1);
983 | }
984 | 25% {
985 | transform: translate(10%, 10%) scale(1);
986 | }
987 | 50% {
988 | transform: translate(-10%, 10%) scale(1);
989 | }
990 | 75% {
991 | transform: translate(-10%, -10%) scale(1);
992 | }
993 | 100% {
994 | transform: translate(10%, -10%) scale(1);
995 | }
996 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { library } from '@fortawesome/fontawesome-svg-core';
2 | import { fab } from "@fortawesome/free-brands-svg-icons";
3 | import { fas } from "@fortawesome/free-solid-svg-icons";
4 | import Footer from "../components/Footer";
5 | import Navbar from "../components/Header";
6 | import vazirmatn from "../styles/fonts";
7 | import "./globals.css";
8 |
9 | library.add(fab, fas)
10 |
11 | // export const metadata: Metadata = {
12 | // title: "Recipe App",
13 | // description: "Welcome to Recpie app",
14 | // applicationName: "RecipeApp",
15 | // authors:[
16 | // {
17 | // name: "Ehsan Ghaffar",
18 | // url: "https://ehsanghaffarii.ir"
19 | // }
20 | // ],
21 | // creator: "Ehsan Ghaffar",
22 | // verification: {
23 | // google: "aG69rfEfYwvFjNKS3C-jUj60PsqRr2LO9lHyKw0wNFE"
24 | // },
25 | // openGraph: {
26 | // title: 'Nextjs Appdir Recipe App',
27 | // description: 'Recipe App created with next.js 13.4',
28 | // type: 'article',
29 | // publishedTime: '2023-01-01T00:00:00.000Z',
30 | // authors: ['Ehsan Ghaffar', 'Eindev'],
31 | // },
32 |
33 | // };
34 |
35 |
36 | export default function RootLayout({
37 | children,
38 | }: {
39 | children: React.ReactNode;
40 | }) {
41 | return (
42 |
43 |
47 |
48 | {children}
49 |
50 |
51 |
52 | );
53 | }
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function NotFound() {
4 | return (
5 | <>
6 |
7 |
8 |
۴۰۴
9 |
10 |
11 | Sorry !
12 |
13 |
14 |
15 | Page do not exist
16 |
17 |
21 | Home page
22 |
23 |
24 |
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import Link from "next/link";
3 | import HeaderBottom from "../components/HeaderBottom";
4 | import LandingSection from "../components/LandingSection";
5 | import LatestRecipes from "../components/LatestRecipes";
6 | import Button from "../components/ui/Button";
7 | import { Metadata } from "next";
8 |
9 | export const metadata: Metadata = {
10 | title: "Recipe App",
11 | description: "Welcome to Recpie app",
12 | applicationName: "RecipeApp",
13 | authors:[
14 | {
15 | name: "Ehsan Ghaffar",
16 | url: "https://ehsanghaffarii.ir"
17 | }
18 | ],
19 | creator: "Ehsan Ghaffar",
20 | verification: {
21 | google: "aG69rfEfYwvFjNKS3C-jUj60PsqRr2LO9lHyKw0wNFE"
22 | },
23 | openGraph: {
24 | title: 'Nextjs Appdir Recipe App',
25 | description: 'Recipe App created with next.js 13.4',
26 | type: 'article',
27 | publishedTime: '2023-01-01T00:00:00.000Z',
28 | authors: ['Ehsan Ghaffar', 'Eindev'],
29 | },
30 |
31 | };
32 |
33 | export default function Home() {
34 | return (
35 | <>
36 |
37 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 | All recipes
51 |
52 |
53 |
54 |
55 |
56 |
57 | >
58 | );
59 | }
--------------------------------------------------------------------------------
/app/recipes/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { useParams } from "next/navigation";
3 | import React, { useEffect, useState } from "react";
4 | import SingleRecipeMain from "../../../components/SingleRecipeMain";
5 | import SingleRecipeTopSection from "../../../components/SingleRecipeTopSection";
6 | import { IRecipCreate } from "../../../models/recipe";
7 |
8 | const SingleRecipe = () => {
9 | const { id } = useParams();
10 | const [singleRecipe, setSingleRecipe] = useState(
11 | undefined
12 | );
13 |
14 | const onDeleteRecipe = async (id: string) => {
15 | try {
16 | const response = await fetch(`/api/recipes/${id}`, {
17 | method: "DELETE"
18 | })
19 | const data = response.json()
20 | console.log("deleted", data);
21 | } catch (error) {
22 | console.log("er",error);
23 | }
24 | }
25 |
26 | useEffect(() => {
27 | const get_recipe_by_id = async (id: string) => {
28 | try {
29 | const res = await fetch(`/api/recipes/get-single-recipe?id=${id}`, {
30 | method: "GET",
31 | });
32 | const data = await res.json();
33 | setSingleRecipe(data.data);
34 | } catch (error) {
35 | console.log("Error in getting product by ID (service) =>", error);
36 | }
37 | };
38 | get_recipe_by_id(id);
39 | }, []);
40 |
41 | return (
42 | <>
43 |
44 |
49 |
50 |
55 |
56 | {/* onDeleteRecipe(id)}>
57 | Remove
58 | */}
59 |
60 | >
61 | );
62 | };
63 |
64 | export default SingleRecipe;
--------------------------------------------------------------------------------
/app/recipes/create/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { CheckIcon, InboxArrowDownIcon } from "@heroicons/react/24/solid";
3 | import { ChangeEvent, FormEvent, useState } from "react";
4 | import Button from "../../../components/ui/Button";
5 | import { IRecipCreate, Ingredients } from "../../../models/recipe";
6 |
7 | let nextId = 0;
8 |
9 | // تعریف کامپوننت CreateRecipe
10 | export default function CreateRecipe() {
11 | // استفاده از useState برای تعریف و مدیریت وضعیت های مورد نیاز
12 | const [recipe, setRecipe] = useState({
13 | name: "",
14 | description: "",
15 | ingredients: [],
16 | photo: null,
17 | steps: "",
18 | });
19 | const [singleIngredient, setSingleIngredient] = useState("");
20 | const [ingredients, setIngredients] = useState([]);
21 | const [selectedPhoto, setSelectedPhoto] = useState(null);
22 | const [loading, setLoading] = useState(false)
23 | const [apiMessage, setApiMessage] = useState("")
24 |
25 | // تابع handlePhotoChange برای مدیریت تغییرات در ورودی عکس
26 | const handlePhotoChange = (event: ChangeEvent) => {
27 | const file = event.target.files && event.target.files[0];
28 | if (file) {
29 | setSelectedPhoto(file);
30 | }
31 | };
32 |
33 | // تابع handleAddIngredient برای اضافه کردن ماده اولیه جدید به لیست مواد اولیه
34 | function handleAddIngredient() {
35 | const insertAt = 1; // میتواند هر شاخصی باشد
36 | const nextIngredients = [
37 | // موارد قبل از شاخص درج:
38 | ...ingredients.slice(0, insertAt),
39 | // ماده اولیه جدید:
40 | { id: nextId++, name: singleIngredient },
41 | // موارد بعد از شاخص درج:
42 | ...ingredients.slice(insertAt),
43 | ];
44 | // console.log("next", nextIngredients);
45 |
46 | setIngredients(nextIngredients);
47 | setSingleIngredient("");
48 | }
49 |
50 | // تابع saveRecipe برای ارسال دادهها به سرور و ذخیره رسپی جدید
51 | const saveRecipe = async (event: FormEvent) => {
52 | event.preventDefault();
53 | setLoading(true)
54 | if (selectedPhoto) {
55 | const formData = new FormData();
56 | formData.append("photo", selectedPhoto);
57 | formData.append("name", recipe.name);
58 | formData.append("description", recipe.description);
59 | formData.append("ingredients", JSON.stringify(ingredients));
60 | formData.append("steps", recipe.steps);
61 |
62 | const options = {
63 | method: "POST",
64 | body: formData,
65 | };
66 | try {
67 | const saveRecip = await fetch("/api/recipes", options);
68 | if (saveRecip.status === 200) {
69 | setLoading(false)
70 | console.log(saveRecip.json());
71 | setApiMessage("رسپی ساخته شد!")
72 |
73 | setTimeout(() => {
74 | setApiMessage("")
75 | }, 4000);
76 | }
77 | } catch (error) {
78 | setLoading(false)
79 | console.log(error);
80 | setApiMessage("مشکلی در ساخت رسپی به وجود آمد")
81 |
82 | setTimeout(() => {
83 | setApiMessage("")
84 | }, 4000);
85 | }
86 | }
87 | };
88 |
89 | // بخش رابط کاربری کامپوننت CreateRecipe
90 | return (
91 | <>
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | اضافه کردن رسپی جدید
100 |
101 | {/*
102 | */}
103 |
104 |
105 |
106 |
107 |
207 | {apiMessage &&
208 |
209 |
پیام سیستم:
210 |
{apiMessage}
211 |
212 | }
213 |
214 |
215 |
216 | >
217 | );
218 | }
219 |
--------------------------------------------------------------------------------
/app/recipes/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LatestRecipes from "../../components/LatestRecipes";
3 | import SingleRecipeTopSection from "../../components/SingleRecipeTopSection";
4 | import { SearchComponent } from "@/components/Search";
5 |
6 | const Recipes = () => {
7 | return (
8 | <>
9 |
14 |
15 |
22 | >
23 | );
24 | };
25 |
26 | export default Recipes;
27 |
--------------------------------------------------------------------------------
/app/recipes/search/[q]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import RecipeCard from '@/components/RecipeCard';
3 | import { SearchComponent } from '@/components/Search'
4 | import SingleRecipeTopSection from '@/components/SingleRecipeTopSection'
5 | import { GetAllRecipesResponse, Recipe } from '@/types/Recipe';
6 | import { useParams } from 'next/navigation';
7 | import React, { useEffect, useState } from 'react'
8 |
9 | const SearchPage = () => {
10 | const { q } = useParams();
11 | console.log("q", q);
12 | const [searches, setSearches] = useState()
13 |
14 | useEffect(() => {
15 | const getSearches = async () => {
16 | try {
17 | const response = await fetch(`/api/search/?q=${q}`, {
18 | method: "GET"
19 | })
20 | if (response.ok) {
21 | const data = await response.json()
22 | setSearches(data)
23 | }
24 | } catch (error) {
25 | console.log(error);
26 | }
27 | }
28 | getSearches()
29 | },[])
30 |
31 | return (
32 | <>
33 |
38 |
39 |
40 | {
41 | searches?.length === 0 &&
Nothing founded ...
42 | }
43 | {
44 | searches && searches?.map((recipes: GetAllRecipesResponse) => (
45 |
46 | ))
47 | }
48 |
49 | >
50 | )
51 | }
52 |
53 | export default SearchPage
--------------------------------------------------------------------------------
/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next'
2 |
3 | export default function sitemap(): MetadataRoute.Sitemap {
4 | return [
5 | {
6 | url: 'https://denizpaz.ir',
7 | lastModified: new Date(),
8 | },
9 | {
10 | url: 'https://denizpaz.ir/about',
11 | lastModified: new Date(),
12 | },
13 | {
14 | url: 'https://denizpaz.ir/contact',
15 | lastModified: new Date(),
16 | },
17 | {
18 | url: 'https://denizpaz.ir/recipes',
19 | lastModified: new Date(),
20 | },
21 | ]
22 | }
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | const Footer: React.FC = () => {
5 | return (
6 |
16 | );
17 | };
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import BackButton from "./ui/BackButton";
4 |
5 | const Navbar: React.FC = () => {
6 | return (
7 |
8 |
9 |
13 |
14 |
15 |
16 | {" "}
17 | Home{" "}
18 |
19 |
20 |
21 |
22 |
23 | {" "}
24 | Recipes{" "}
25 |
26 |
27 |
28 |
29 |
30 | {" "}
31 | About{" "}
32 |
33 |
34 |
35 |
36 | {" "}
37 | Contact{" "}
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default Navbar;
55 |
--------------------------------------------------------------------------------
/components/HeaderBottom.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import HeaderLeftImage from '../public/landing-header-image.png'
4 | import Image from "next/image";
5 | import { ArrowRightIcon } from "@heroicons/react/24/solid";
6 |
7 | const LandingHero: React.FC = () => {
8 |
9 | return (
10 |
11 |
12 |
Recipe App
13 |
14 | Discover, Cook, and Savor Delicious Recipes!
15 |
16 |
17 | Are you a food enthusiast always on the lookout for exciting new recipes to try? Look no further! Foodie Delights is here to fulfill all your culinary desires with a vast collection of mouthwatering recipes from around the world.
18 |
19 |
25 |
26 | Lets Go
27 |
28 |
29 |
30 |
31 |
32 | {/*
*/}
33 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default LandingHero
47 |
--------------------------------------------------------------------------------
/components/LandingSection.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | interface LandingSectionProps {
4 | title: string;
5 | subTitle: string;
6 | children: React.ReactNode;
7 | rootClasses: string;
8 | }
9 |
10 | const LandingSection = ({
11 | title,
12 | subTitle,
13 | children,
14 | rootClasses,
15 | }: LandingSectionProps) => {
16 | return (
17 | <>
18 |
19 |
20 |
21 |
{title}
22 |
23 | {subTitle}
24 |
25 |
26 |
27 | {children}
28 |
29 |
30 | >
31 | );
32 | };
33 |
34 | export default LandingSection;
35 |
--------------------------------------------------------------------------------
/components/LatestRecipes.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect, useState } from "react";
3 | import { GetAllRecipesResponse } from "../types/Recipe";
4 | import RecipeCard from "./RecipeCard";
5 | import SkeletonCard from "./ui/Skeleton";
6 |
7 | interface LatestRecipesProps {
8 | limitNumber: number;
9 | }
10 |
11 | const LatestRecipes = ({ limitNumber }: LatestRecipesProps) => {
12 | const [allRecipes, setAllRecipes] = useState<
13 | GetAllRecipesResponse[] | undefined
14 | >(undefined);
15 | const [loading, setLoading] = useState(true);
16 |
17 | const get_all_recipes = async () => {
18 | setLoading(true);
19 | try {
20 | const res = await fetch("/api/recipes", {
21 | method: "GET",
22 | });
23 | const data = await res.json();
24 | setAllRecipes(data);
25 | setLoading(false);
26 | } catch (error) {
27 | setLoading(false);
28 | console.log("Error in getting all recipes (service) =>", error);
29 | }
30 | };
31 |
32 | useEffect(() => {
33 | get_all_recipes();
34 | }, []);
35 |
36 | return (
37 | <>
38 |
39 | {loading &&
40 | Array.from({ length: limitNumber || 4 }, (item, index) => (
41 |
42 |
43 |
44 | ))}
45 | {!loading &&
46 | allRecipes
47 | ?.slice(0, limitNumber ? limitNumber : 4)
48 | ?.map((cake) =>
)}
49 |
50 | >
51 | );
52 | };
53 |
54 | export default LatestRecipes;
--------------------------------------------------------------------------------
/components/RecipeCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import React from "react";
4 |
5 | import Button from "./ui/Button";
6 | import { Recipe } from "../types/Recipe";
7 | import { ArrowRightIcon } from "@heroicons/react/24/solid";
8 |
9 | interface RecipeCardProps {
10 | recipe: Recipe | any;
11 | }
12 |
13 | const RecipeCard = ({ recipe }: RecipeCardProps) => {
14 | return (
15 | <>
16 |
17 |
18 |
23 |
24 |
25 | {recipe?.name}
26 |
27 |
28 | {recipe?.description.substring(0, 60)}...
29 |
30 |
31 |
35 | Recipe page
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | export default RecipeCard;
47 |
--------------------------------------------------------------------------------
/components/Search.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { ChangeEvent, FormEvent, useState } from 'react';
3 | import { useRouter } from 'next/navigation';
4 |
5 | interface SearchComponentProps {
6 | classes: string
7 | }
8 |
9 | export function SearchComponent({classes}: SearchComponentProps) {
10 | const [searchQuery, setSearchQuery] = useState('');
11 | const router = useRouter()
12 |
13 | const handleChange = (event: ChangeEvent) => {
14 | setSearchQuery(event.target.value);
15 | };
16 |
17 | const handleSubmit = async (event: FormEvent) => {
18 | event.preventDefault();
19 | router.push(`/recipes/search/${searchQuery}`)
20 |
21 | };
22 |
23 | return (
24 | <>
25 |
26 |
27 |
28 |
29 |
37 | Search
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | >
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/SingleRecipeMain.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Ingredients } from "../models/recipe";
3 | import { CheckCircleIcon } from "@heroicons/react/24/solid";
4 |
5 | interface SingleRecipeMainProps {
6 | recipePhoto?: string | any;
7 | recipeIngredients?: Ingredients[];
8 | recipeSteps?: string;
9 | }
10 |
11 | const SingleRecipeMain = ({
12 | recipePhoto,
13 | recipeIngredients,
14 | recipeSteps,
15 | }: SingleRecipeMainProps) => {
16 | return (
17 | <>
18 |
19 |
20 |
21 |
22 | Ingredients:
23 |
24 | {recipeIngredients?.map((ing) => (
25 |
26 |
30 | {ing.name}
31 |
32 | ))}
33 |
34 |
35 |
42 |
43 |
44 |
45 |
46 | Cooking steps:
47 |
48 |
51 |
52 |
53 | >
54 | );
55 | };
56 |
57 | export default SingleRecipeMain;
--------------------------------------------------------------------------------
/components/SingleRecipeTopSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | interface SingleRecipeTopSectionProps {
3 | recipeName?: string;
4 | recipeDesc?: string;
5 | sectionBackground?: string;
6 | }
7 |
8 | const SingleRecipeTopSection = ({
9 | recipeName,
10 | recipeDesc,
11 | sectionBackground,
12 | }: SingleRecipeTopSectionProps) => {
13 | return (
14 | <>
15 |
16 |
17 |
23 |
24 |
25 |
{recipeName}
26 |
{recipeDesc}
27 |
28 |
29 | >
30 | );
31 | };
32 |
33 | export default SingleRecipeTopSection;
34 |
--------------------------------------------------------------------------------
/components/ui/BackButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { ArrowRightIcon } from "@heroicons/react/24/solid";
3 | import { usePathname, useRouter } from "next/navigation";
4 | import React from "react";
5 |
6 | const BackButton = () => {
7 | const router = useRouter();
8 | const pathName = usePathname();
9 | return (
10 | <>
11 | {pathName !== "/" && (
12 | router.back()}
14 | className=" grid grid-cols-2 gap-2 items-center px-4 py-1 rounded"
15 | >
16 | Back
17 |
18 |
19 | )}
20 | >
21 | );
22 | };
23 |
24 | export default BackButton;
25 |
--------------------------------------------------------------------------------
/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { ButtonHTMLAttributes } from "react";
3 |
4 | export interface ButtonProps extends ButtonHTMLAttributes {
5 | status?: "primary" | "info" | "danger";
6 | }
7 |
8 | export default function Button({
9 | status,
10 | children,
11 | className,
12 | ...rest
13 | }: ButtonProps) {
14 | return (
15 |
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/ui/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SkeletonCard = () => {
4 | return (
5 | <>
6 | {/* */}
16 | >
17 | );
18 | };
19 |
20 | export default SkeletonCard;
21 |
--------------------------------------------------------------------------------
/denizpaz.ir.conf:
--------------------------------------------------------------------------------
1 | server {
2 | if ($host = denizpaz.ir) {
3 | return 301 https://$host$request_uri;
4 | } # managed by Certbot
5 |
6 |
7 |
8 | # listen 80 default_server;
9 | server_name denizpaz.ir;
10 | return 301 https://denizpaz.ir$request_uri;
11 |
12 |
13 |
14 | }
15 |
16 | server {
17 | # listen 80 default_server;
18 | # listen [::]:80 default_server;
19 | # server_name denizpaz.ir;
20 | listen 443 ssl;
21 | server_name denizpaz.ir;
22 | ssl_certificate /etc/letsencrypt/live/denizpaz.ir/fullchain.pem; # managed by Certbot
23 | ssl_certificate_key /etc/letsencrypt/live/denizpaz.ir/privkey.pem; # managed by Certbot
24 | ssl_protocols TLSv1.1 TLSv1.2;
25 |
26 | # logging
27 | access_log /var/log/nginx/denizpaz.ir.access.log;
28 | error_log /var/log/nginx/denizpaz.ir.error.log warn;
29 |
30 | # reverse proxy
31 | location / {
32 | proxy_pass http://127.0.0.1:3000;
33 | # include myconfig/proxy.conf;
34 | proxy_set_header X-Forwarded-Port 443;
35 | proxy_set_header X-Forwarded-Ssl on;
36 |
37 | }
38 |
39 | # additional config
40 | # include myconfig/general.conf;
41 |
42 | }
43 |
44 |
45 | server {
46 | listen 80;
47 | listen [::]:80;
48 | server_name *.denizpaz.ir;
49 | return 301 https://denizpaz.ir$request_uri;
50 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | web:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | args:
8 | ENV_VARIABLE: ${ENV_VARIABLE}
9 | NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
10 | restart: always
11 | volumes:
12 | - ./:/app/
13 | - /app/node_modules
14 | - /app/.next
15 | ports:
16 | - "${PORT}:${PORT}"
17 | environment:
18 | DB_LOCAL_URL: mongodb://mongo:27017/denizpaz
19 | depends_on:
20 | - mongo
21 | networks:
22 | - MONGO
23 |
24 | mongo:
25 | container_name: mongo
26 | image: mongo:latest
27 | volumes:
28 | - type: volume
29 | source: MONGO_DATA
30 | target: /data/db
31 | - type: volume
32 | source: MONGO_CONFIG
33 | target: /data/configdb
34 | ports:
35 | - "27017:27017"
36 | networks:
37 | - MONGO
38 |
39 | networks:
40 | MONGO:
41 | name: MONGO
42 |
43 | volumes:
44 | MONGO_DATA:
45 | name: MONGO_DATA
46 | MONGO_CONFIG:
47 | name: MONGO_CONFIG
--------------------------------------------------------------------------------
/environment.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | export interface ProcessEnv {
3 | readonly ENV_VARIABLE: string
4 | readonly NEXT_PUBLIC_ENV_VARIABLE: string
5 | readonly NEXT_PUBLIC_DB_LOCAL_URL: string
6 |
7 | readonly DEVELOPMENT_ENV_VARIABLE: string
8 | readonly NEXT_PUBLIC_DEVELOPMENT_ENV_VARIABLE: string
9 |
10 | readonly ENV_LOCAL_VARIABLE: string
11 | readonly NEXT_PUBLIC_ENV_LOCAL_VARIABLE: string
12 |
13 | readonly PRODUCTION_ENV_VARIABLE: string
14 | readonly NEXT_PUBLIC_PRODUCTION_ENV_VARIABLE: string
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/data.ts:
--------------------------------------------------------------------------------
1 | import { IRecipCreate } from "@/models/recipe";
2 |
3 | export const seedData: IRecipCreate[] = [
4 | {
5 | name: 'Chocolate Cake',
6 | description: 'A delicious chocolate cake recipe',
7 | ingredients: [
8 | { id: 1, name: 'Flour' },
9 | { id: 2, name: 'Sugar' },
10 | { id: 3, name: 'Cocoa Powder' },
11 | { id: 4, name: 'Eggs' },
12 | { id: 5, name: 'Butter' },
13 | { id: 6, name: 'Milk' },
14 | { id: 7, name: 'Vanilla Extract' },
15 | ],
16 | photo: 'contactus.jpg',
17 | steps: '1. Preheat oven to 350°F. 2. Mix dry ingredients. 3. Mix wet ingredients. 4. Combine and mix until smooth. 5. Bake for 30 minutes.\n 1. Preheat oven to 350°F. 2. Mix dry ingredients. 3. Mix wet ingredients. 4. Combine and mix until smooth. 5. Bake for 30 minutes',
18 | },
19 | {
20 | name: 'Spaghetti Bolognese',
21 | description: 'Classic Italian pasta dish',
22 | ingredients: [
23 | { id: 8, name: 'Spaghetti' },
24 | { id: 9, name: 'Ground Beef' },
25 | { id: 10, name: 'Tomato Sauce' },
26 | { id: 11, name: 'Onion' },
27 | { id: 12, name: 'Garlic' },
28 | { id: 13, name: 'Olive Oil' },
29 | { id: 14, name: 'Salt' },
30 | { id: 15, name: 'Pepper' },
31 | ],
32 | photo: 'contactus.jpg',
33 | steps: '1. Cook spaghetti according to package instructions. 2. Brown ground beef in olive oil. 3. Add chopped onion and garlic. 4. Stir in tomato sauce and seasonings. 5. Simmer for 20 minutes. 6. Serve over cooked spaghetti.',
34 | },
35 | {
36 | name: 'Just Cake',
37 | description: 'Classic Cake',
38 | ingredients: [
39 | { id: 16, name: 'Onion' },
40 | { id: 17, name: 'Garlic' },
41 | { id: 18, name: 'Olive Oil' },
42 | { id: 19, name: 'Salt' },
43 | { id: 20, name: 'Pepper' },
44 | ],
45 | photo: 'contactus.jpg',
46 | steps: '1. Cook spaghetti according to package instructions. 2. Brown ground beef in olive oil. 3. Add chopped onion and garlic. 4. Stir in tomato sauce and seasonings. 5. Simmer for 20 minutes. 6. Serve over cooked spaghetti.',
47 | },
48 | ];
49 |
--------------------------------------------------------------------------------
/lib/mongodb.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { connect, connection } from 'mongoose'
2 |
3 | const options: any = {
4 | useUnifiedTopology: true,
5 | useNewUrlParser: true
6 | }
7 |
8 | export const connectToDatabase = async () => {
9 | if (!connection.readyState) {
10 |
11 | await mongoose.connect(process.env.NEXT_PUBLIC_DB_LOCAL_URL, options)
12 | .then((c) => {
13 | console.log("Mongo Connected: ")
14 | })
15 | .catch((error) => {
16 | console.log("error: ", error)
17 | })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Recipe, type IRecipe } from './recipe'
--------------------------------------------------------------------------------
/models/recipe.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Schema } from 'mongoose'
2 |
3 | export interface IRecipe extends Document {
4 | name: string;
5 | description: string;
6 | ingredients?: Ingredients[];
7 | photo: File | null | string;
8 | steps: string;
9 | }
10 |
11 | export interface Ingredients {
12 | id: number;
13 | name: string;
14 | }
15 |
16 | export type IRecipCreate = Pick
17 |
18 | const RecipeSchema: Schema = new Schema({
19 | name: {
20 | type: String,
21 | required: true
22 | },
23 | description: {
24 | type: String,
25 | required: true
26 | },
27 | ingredients: [
28 | new Schema({
29 | id: Number,
30 | name: String
31 | },
32 | { _id: false }
33 | )
34 | ],
35 | photo: {
36 | type: String
37 | },
38 | steps: {
39 | type: String
40 | }
41 | })
42 |
43 | export const Recipe = mongoose.models.Recipe || mongoose.model('Recipe', RecipeSchema)
44 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | // output: 'standalone',
5 | }
6 |
7 | module.exports = nextConfig
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-appdir-docker",
3 | "version": "0.1.0",
4 | "private": true,
5 | "author": {
6 | "name": "Ehsan Ghaffar",
7 | "email": "ghafari.5000@gmail.com",
8 | "url": "https://ehsanghaffarii.ir"
9 | },
10 | "repository": {
11 | "url": "https://github.com/ehsanghaffar/nextjs-appdir-docker"
12 | },
13 | "keywords": [
14 | "nextjs",
15 | "appdir",
16 | "docker",
17 | "next 13.4",
18 | "recipe",
19 | "persian",
20 | "mongoose",
21 | "mongodb",
22 | "tailwindcss"
23 | ],
24 | "scripts": {
25 | "dev": "next dev",
26 | "build": "next build",
27 | "start": "next start",
28 | "lint": "next lint",
29 | "launch": "npm run build && npm run start",
30 | "server-build": "next build && tsc --project tsconfig.server.json",
31 | "server-start": "cross-env NODE_ENV=production node dist/server.js",
32 | "launch-server": "npm run server-build && npm run server-start",
33 | "data:import": "ts-node --project tsconfig.seeder.json seeder.ts",
34 | "data:destroy": "ts-node --project tsconfig.seeder.json seeder.ts -d"
35 | },
36 | "dependencies": {
37 | "@fortawesome/free-brands-svg-icons": "^6.4.0",
38 | "@fortawesome/free-solid-svg-icons": "^6.4.0",
39 | "@fortawesome/react-fontawesome": "^0.2.0",
40 | "@heroicons/react": "^2.0.18",
41 | "@types/node": "20.2.5",
42 | "@types/react": "18.2.8",
43 | "@types/react-dom": "18.2.4",
44 | "autoprefixer": "10.4.14",
45 | "cross-env": "^7.0.3",
46 | "date-fns": "^2.30.0",
47 | "dotenv": "^16.4.4",
48 | "eslint": "8.42.0",
49 | "eslint-config-next": "^14.1.0",
50 | "mime": "^3.0.0",
51 | "mongoose": "^7.2.3",
52 | "next": "^14.1.1",
53 | "postcss": "8.4.24",
54 | "react": "^18.2.0",
55 | "react-dom": "^18.2.0",
56 | "tailwindcss": "3.3.2",
57 | "tailwindcss-animate": "^1.0.6",
58 | "typescript": "5.1.3",
59 | "uuid": "^9.0.0",
60 | "vazirmatn": "^33.0.3"
61 | },
62 | "devDependencies": {
63 | "@tailwindcss/forms": "^0.5.3",
64 | "@types/mime": "^3.0.1",
65 | "@types/uuid": "^9.0.2"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/contactus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/contactus.jpg
--------------------------------------------------------------------------------
/public/facebook-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/facebook.png
--------------------------------------------------------------------------------
/public/header-recipe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/header-recipe.png
--------------------------------------------------------------------------------
/public/instagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/instagram.png
--------------------------------------------------------------------------------
/public/landing-header-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/landing-header-image.png
--------------------------------------------------------------------------------
/public/linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/linkedin.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/logo.png
--------------------------------------------------------------------------------
/public/me.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/me.png
--------------------------------------------------------------------------------
/public/recipe-header.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/recipe-header.jpg
--------------------------------------------------------------------------------
/public/search-page.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/search-page.jpg
--------------------------------------------------------------------------------
/public/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/public/twitter.png
--------------------------------------------------------------------------------
/seeder.ts:
--------------------------------------------------------------------------------
1 | import { seedData } from "./lib/data";
2 | import { Recipe } from "./models";
3 | import mongoose from "mongoose";
4 |
5 | import dotenv from "dotenv";
6 |
7 | // MongoDB connection options
8 | const connectionOptions: any = {
9 | useUnifiedTopology: true,
10 | useNewUrlParser: true,
11 | };
12 |
13 | dotenv.config({ path: __dirname + "/.env" });
14 |
15 | const DB_URL = process.env.NEXT_PUBLIC_DB_LOCAL_URL
16 |
17 | // Connect to MongoDB
18 | mongoose
19 | .connect(DB_URL as string, connectionOptions)
20 | .then(() => console.log("Connected to MongoDB"))
21 | .catch((error) => console.log("Failed to connect to MongoDB:", error.message));
22 |
23 | // Import data from seedData
24 | const importData = async () => {
25 | try {
26 | // Delete previous data to avoid duplication
27 | await Recipe.deleteMany();
28 | // Insert new data from seedData
29 | await Recipe.insertMany(seedData);
30 |
31 | console.log("Data imported successfully");
32 |
33 | process.exit();
34 | } catch (error) {
35 | console.error("Failed to import data:", error);
36 | process.exit(1);
37 | }
38 | };
39 |
40 | // Destroy all data in Recipe collection
41 | const destroyData = async () => {
42 | try {
43 | await Recipe.deleteMany();
44 |
45 | console.log("All data destroyed successfully");
46 | process.exit();
47 | } catch (error) {
48 | console.error("Failed to destroy data:", error);
49 | process.exit(1);
50 | }
51 | };
52 |
53 | // Check command-line argument to determine import or destroy operation
54 | if (process.argv[2] == "-d") {
55 | destroyData();
56 | } else {
57 | importData();
58 | }
59 |
--------------------------------------------------------------------------------
/styles/fonts.ts:
--------------------------------------------------------------------------------
1 | import localFont from 'next/font/local';
2 |
3 | // define a custom local font where GreatVibes-Regular.ttf is stored in the styles folder
4 | const vazirmatn = localFont({ src: './fonts/variable/Vazirmatn[wght].ttf' });
5 |
6 | export default vazirmatn
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-Black.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-Bold.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-ExtraBold.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-ExtraLight.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-Light.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-Medium.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-Regular.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-SemiBold.ttf
--------------------------------------------------------------------------------
/styles/fonts/ttf/Vazirmatn-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/ttf/Vazirmatn-Thin.ttf
--------------------------------------------------------------------------------
/styles/fonts/variable/Vazirmatn[wght].ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/variable/Vazirmatn[wght].ttf
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-Black.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-Bold.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-ExtraBold.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-ExtraLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-ExtraLight.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-Light.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-Medium.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-Regular.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-SemiBold.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn-Thin.woff2
--------------------------------------------------------------------------------
/styles/fonts/webfonts/Vazirmatn[wght].woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehsanghaffar/nextjs-appdir-docker/a7b7684aea6905e1155d3fe7901882ac43014a1f/styles/fonts/webfonts/Vazirmatn[wght].woff2
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | fontFamily: {
10 | vazirmatn: ['Vazirmatn', 'sans-serif']
11 | },
12 | extend: {
13 | backgroundImage: {
14 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
15 | 'gradient-conic':
16 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
17 | },
18 | },
19 | },
20 | plugins: [],
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "baseUrl": ".",
22 | "plugins": [
23 | {
24 | "name": "next"
25 | }
26 | ],
27 | "paths": {
28 | "@/*": [
29 | "./*"
30 | ],
31 | "@/fonts": [
32 | "./styles/fonts"
33 | ]
34 | }
35 | },
36 | "include": [
37 | "next-env.d.ts",
38 | "**/*.ts",
39 | "**/*.tsx",
40 | "**/*.js",
41 | "**/*.jsx",
42 | ".next/types/**/*.ts",
43 | ],
44 | "exclude": [
45 | "node_modules"
46 | ]
47 | }
--------------------------------------------------------------------------------
/tsconfig.seeder.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS"
5 | }
6 | }
--------------------------------------------------------------------------------
/types/Components.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { colors } from "./Generic";
3 |
4 | export type buttonVariant = "filled" | "outlined" | "gradient" | "text";
5 | export type buttonSize = "sm" | "md" | "lg";
6 | export type buttonType = "submit" | "reset" | undefined;
7 | export type buttonColor = "white" | colors;
8 | export type className = string;
9 | export type children = ReactNode;
--------------------------------------------------------------------------------
/types/Generic.ts:
--------------------------------------------------------------------------------
1 | export type colors =
2 | | "blue-gray"
3 | | "gray"
4 | | "brown"
5 | | "deep-orange"
6 | | "orange"
7 | | "amber"
8 | | "yellow"
9 | | "lime"
10 | | "light-green"
11 | | "green"
12 | | "teal"
13 | | "cyan"
14 | | "light-blue"
15 | | "blue"
16 | | "indigo"
17 | | "deep-purple"
18 | | "purple"
19 | | "pink"
20 | | "red";
21 |
22 | export type WithChildren> = T & {
23 | children: React.ReactNode | React.ReactNode[] | JSX.Element | JSX.Element[];
24 | };
--------------------------------------------------------------------------------
/types/Recipe.ts:
--------------------------------------------------------------------------------
1 | export interface Recipe {
2 | id: string;
3 | name: string;
4 | description: string;
5 | image: string;
6 | ingredients: Array;
7 | steps: Array;
8 | time: string;
9 | }
10 | export interface GetAllRecipesResponse {
11 | _id: string;
12 | name: string;
13 | description: string;
14 | ingredients: any[];
15 | __v: number;
16 | }
17 |
--------------------------------------------------------------------------------
/types/mongodb.d.ts:
--------------------------------------------------------------------------------
1 | import { MongoClient } from 'mongodb'
2 |
3 | declare global {
4 | var _mongoClientPromise: Promise
5 | }
6 |
--------------------------------------------------------------------------------