├── src
├── .prettierrc.json
├── app
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistVF.woff
│ │ └── GeistMonoVF.woff
│ ├── error.tsx
│ ├── page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ ├── menu
│ │ ├── page.tsx
│ │ └── [category]
│ │ │ └── page.tsx
│ └── meal
│ │ └── [idMeal]
│ │ └── page.tsx
├── utils
│ └── api.ts
├── components
│ ├── MenuGrid.tsx
│ ├── MealCard.tsx
│ ├── CategoryCard.tsx
│ ├── MenuCard.tsx
│ ├── ScrollToTop.tsx
│ ├── MobileNavBar.tsx
│ ├── BackButton.tsx
│ ├── Sidebar.tsx
│ ├── Filter.tsx
│ ├── Pagination.tsx
│ ├── ImageSlider.tsx
│ ├── CategoriesCarousel.tsx
│ ├── MealNavigation.tsx
│ ├── RandomMeals.tsx
│ ├── Footer.tsx
│ └── NavBar.tsx
└── hooks
│ ├── useFetchMeals.ts
│ ├── useFetchCategories.ts
│ └── useFetchMealDetails.ts
├── desk.png
├── mobile.png
├── tablet.png
├── .eslintrc.json
├── desk-article.png
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── custom.md
│ ├── feature_request.md
│ └── bug_report.md
└── PULL_REQUEST_TEMPLATE
├── postcss.config.mjs
├── next.config.mjs
├── workflow
└── github
│ └── food.yml
├── .gitignore
├── tailwind.config.ts
├── tsconfig.json
├── SECURITY.md
├── package.json
├── .prettierrc.json
├── LICENSE
├── CONTRIBUTING.md
├── README.md
└── CODE_OF_CONDUCT.md
/src/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/desk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/desk.png
--------------------------------------------------------------------------------
/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/mobile.png
--------------------------------------------------------------------------------
/tablet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/tablet.png
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/desk-article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/desk-article.png
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [frau-azadeh]
4 |
--------------------------------------------------------------------------------
/src/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/src/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/src/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frau-azadeh/Just-Food/HEAD/src/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: ["www.themealdb.com"],
6 | },
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/src/app/error.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React from 'react'
3 |
4 | const error = () => {
5 | return (
6 |
7 |
!Oops,Please try a gain
8 |
9 | )
10 | }
11 |
12 | export default error
--------------------------------------------------------------------------------
/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const api = axios.create({
4 | baseURL: "https://www.themealdb.com/api/json/v1/1", // Base URL for all API requests
5 | timeout: 5000, // Optional: Set a timeout for requests
6 | });
7 |
8 | export default api;
9 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CategoriesCarousel from "@/components/CategoriesCarousel";
3 | import ImageSlider from "@/components/ImageSlider";
4 | import RandomMeals from "@/components/RandomMeals";
5 |
6 | const Page: React.FC = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default Page;
17 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0a0a0a;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
23 | @layer utilities {
24 | .text-balance {
25 | text-wrap: balance;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/workflow/github/food.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | push:
4 | branches: ["master"]
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | lint:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4.2.1
13 |
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 20
17 |
18 | - name: Install Dependencies
19 | run: npm i
20 |
21 | - name: Prettier
22 | run: npm run prettier:check
23 |
24 | - name: Lint
25 | run: npm run lint
26 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please provide a brief description of the changes you made.
4 |
5 | ## Related Issue
6 |
7 | If this pull request is related to an issue, please link it here.
8 |
9 | ## Checklist
10 |
11 | - [ ] My code follows the style guidelines of this project.
12 | - [ ] I have performed a self-review of my code.
13 | - [ ] I have commented my code, particularly in hard-to-understand areas.
14 | - [ ] I have made corresponding changes to the documentation.
15 | - [ ] My changes generate no new warnings.
16 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | animation: {
12 | scroll: "scrollLeft 15s linear infinite",
13 | },
14 | keyframes: {
15 | scrollLeft: {
16 | "0%": { transform: "translateX(0%)" },
17 | "100%": { transform: "translateX(-50%)" },
18 | },
19 | },
20 | },
21 | },
22 | plugins: [],
23 | };
24 |
25 | export default config;
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/src/components/MenuGrid.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MenuCard from "./MenuCard";
3 |
4 | type Category = {
5 | idCategory: string;
6 | strCategory: string;
7 | strCategoryThumb: string;
8 | strCategoryDescription: string;
9 | };
10 |
11 | type MenuGridProps = {
12 | categories: Category[];
13 | };
14 |
15 | const MenuGrid: React.FC = ({ categories }) => {
16 | return (
17 |
18 |
19 | {categories.map((category) => (
20 |
21 | ))}
22 |
23 |
24 | );
25 | };
26 |
27 | export default MenuGrid;
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "just-food",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "prettier:check": "prettier --check .",
11 | "prettier:fix": "prettier --write ."
12 | },
13 | "dependencies": {
14 | "@trivago/prettier-plugin-sort-imports": "^5.2.2",
15 | "axios": "^1.7.9",
16 | "next": "14.2.20",
17 | "react": "^18",
18 | "react-dom": "^18",
19 | "react-error-boundary": "^6.0.0",
20 | "react-icons": "^5.4.0",
21 | "swiper": "^11.1.15"
22 | },
23 | "devDependencies": {
24 | "@types/node": "^20",
25 | "@types/react": "^18",
26 | "@types/react-dom": "^18",
27 | "@types/swiper": "^5.4.3",
28 | "eslint": "^8",
29 | "eslint-config-next": "14.2.20",
30 | "postcss": "^8",
31 | "tailwindcss": "^3.4.1",
32 | "typescript": "^5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/hooks/useFetchMeals.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import api from "../utils/api";
3 |
4 | type Meal = {
5 | idMeal: string;
6 | strMeal: string;
7 | strMealThumb: string;
8 | };
9 |
10 | export const useFetchMeals = (category: string | undefined) => {
11 | const [meals, setMeals] = useState([]);
12 | const [loading, setLoading] = useState(true);
13 | const [error, setError] = useState(null);
14 |
15 | useEffect(() => {
16 | const fetchMeals = async () => {
17 | try {
18 | const response = await api.get(`/filter.php?c=${category}`);
19 | setMeals(response.data.meals);
20 | } catch (err) {
21 | setError("Failed to fetch meals");
22 | console.error(err);
23 | } finally {
24 | setLoading(false);
25 | }
26 | };
27 |
28 | if (category) fetchMeals();
29 | }, [category]);
30 |
31 | return { meals, loading, error };
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/MealCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import Image from "next/image"; // ایمپورت Image از next/image
4 |
5 | type MealProps = {
6 | idMeal: string;
7 | strMeal: string;
8 | strMealThumb: string;
9 | };
10 |
11 | const MealCard: React.FC = ({ idMeal, strMeal, strMealThumb }) => {
12 | return (
13 |
14 |
15 |
16 |
23 |
24 |
{strMeal}
25 |
26 |
27 | );
28 | };
29 |
30 | export default MealCard;
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/hooks/useFetchCategories.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import api from "@/utils/api";
3 |
4 | type Category = {
5 | idCategory: string;
6 | strCategory: string;
7 | strCategoryThumb: string;
8 | strCategoryDescription: string;
9 | };
10 |
11 | export const useFetchCategories = () => {
12 | const [categories, setCategories] = useState([]);
13 | const [loading, setLoading] = useState(true);
14 | const [error, setError] = useState(null);
15 |
16 | useEffect(() => {
17 | const fetchCategories = async () => {
18 | try {
19 | const response = await api.get("/categories.php"); // Use base URL + endpoint
20 | setCategories(response.data.categories);
21 | } catch (err) {
22 | setError("Failed to fetch categories");
23 | console.error(err);
24 | } finally {
25 | setLoading(false);
26 | }
27 | };
28 |
29 | fetchCategories();
30 | }, []);
31 |
32 | return { categories, loading, error };
33 | };
34 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["@trivago/prettier-plugin-sort-imports"],
3 | "importOrder": [
4 | "^(vite|@vitejs)(/(.*))?$",
5 | "^(react|react-dom)(/(.*))?$",
6 | "^react-router(/(.*))?$",
7 | "^react-error-boundary(/(.*))?$",
8 | "^(immer|use-immer)(/(.*))?$",
9 | "^@tanstack(/(.*))?$",
10 | "^@dnd-kit(/(.*))?$",
11 | "^react-toastify(/(.*))?$",
12 | "^(react-hook-form|@hookform/resolvers|zod)(/(.*))?$",
13 | "^clsx(/(.*))?$",
14 | "",
15 | "^@/api",
16 | "^@/components",
17 | "^@/context",
18 | "^@/data",
19 | "^@/dto",
20 | "^@/hooks",
21 | "^@/icons",
22 | "^@/layouts",
23 | "^@/modals",
24 | "^@/pages",
25 | "^@/providers",
26 | "^@/reducers",
27 | "^@/schemas",
28 | "^@/stores",
29 | "^@/types",
30 | "^@/utils",
31 | "^@/styles",
32 | "^(\\.|\\.\\.)/(.(?!.css))*$",
33 | "\\.css$"
34 | ],
35 | "importOrderSeparation": true,
36 | "importOrderSortSpecifiers": true,
37 | "importOrderGroupNamespaceSpecifiers": true
38 | }
39 |
--------------------------------------------------------------------------------
/src/hooks/useFetchMealDetails.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import api from "../utils/api";
3 |
4 | type MealDetails = {
5 | idMeal: string;
6 | strMeal: string;
7 | strMealThumb: string;
8 | strInstructions: string;
9 | strCategory: string;
10 | strArea: string;
11 | [key: string]: string | undefined;
12 | };
13 |
14 | export const useFetchMealDetails = (idMeal: string | undefined) => {
15 | const [meal, setMeal] = useState(null);
16 | const [loading, setLoading] = useState(true);
17 | const [error, setError] = useState(null);
18 |
19 | useEffect(() => {
20 | const fetchMealDetails = async () => {
21 | try {
22 | const response = await api.get(`/lookup.php?i=${idMeal}`);
23 | setMeal(response.data.meals[0]);
24 | } catch (err) {
25 | setError("Failed to fetch meal details");
26 | console.error(err);
27 | } finally {
28 | setLoading(false);
29 | }
30 | };
31 |
32 | if (idMeal) fetchMealDetails();
33 | }, [idMeal]);
34 |
35 | return { meal, loading, error };
36 | };
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Azadeh Sharifi Soltani
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/CategoryCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Link from "next/link";
5 | import Image from "next/image"; // ایمپورت Image از next/image
6 |
7 | type CategoryCardProps = {
8 | category: {
9 | strCategory: string;
10 | strCategoryThumb: string;
11 | strCategoryDescription: string;
12 | };
13 | };
14 |
15 | const CategoryCard: React.FC = ({ category }) => {
16 | return (
17 |
21 |
28 |
29 | {category.strCategory}
30 |
31 |
32 | {category.strCategoryDescription.slice(0, 80)}...
33 |
34 |
35 | );
36 | };
37 |
38 | export default CategoryCard;
39 |
--------------------------------------------------------------------------------
/src/components/MenuCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image"; // ایمپورت Image از next/image
3 |
4 | type Category = {
5 | idCategory: string;
6 | strCategory: string;
7 | strCategoryThumb: string;
8 | strCategoryDescription: string;
9 | };
10 |
11 | type MenuCardProps = {
12 | category: Category;
13 | };
14 |
15 | const MenuCard: React.FC = ({ category }) => {
16 | return (
17 |
18 |
19 |
26 |
27 |
28 | {category.strCategory}
29 |
30 |
31 | {category.strCategoryDescription.slice(0, 80)}...
32 |
33 |
34 | );
35 | };
36 |
37 | export default MenuCard;
38 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 | import Footer from "@/components/Footer";
5 | import Navbar from "@/components/NavBar";
6 | import ScrollToTop from "@/components/ScrollToTop";
7 | import MobileNavBar from "@/components/MobileNavBar";
8 |
9 | const geistSans = localFont({
10 | src: "./fonts/GeistVF.woff",
11 | variable: "--font-geist-sans",
12 | weight: "100 900",
13 | });
14 | const geistMono = localFont({
15 | src: "./fonts/GeistMonoVF.woff",
16 | variable: "--font-geist-mono",
17 | weight: "100 900",
18 | });
19 |
20 | export const metadata: Metadata = {
21 | title: "Just Food",
22 | description: "Just Food",
23 | };
24 |
25 | export default function RootLayout({
26 | children,
27 | }: Readonly<{
28 | children: React.ReactNode;
29 | }>) {
30 | return (
31 |
32 |
35 |
36 | {children}
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/ScrollToTop.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 | import { FaArrowUp } from "react-icons/fa";
5 |
6 | const ScrollToTop = () => {
7 | const [isVisible, setIsVisible] = useState(false);
8 |
9 | const handleScroll = () => {
10 | if (window.scrollY > 300) {
11 | setIsVisible(true);
12 | } else {
13 | setIsVisible(false);
14 | }
15 | };
16 |
17 | const scrollToTop = () => {
18 | window.scrollTo({
19 | top: 0,
20 | behavior: "smooth",
21 | });
22 | };
23 |
24 | useEffect(() => {
25 | window.addEventListener("scroll", handleScroll);
26 | return () => {
27 | window.removeEventListener("scroll", handleScroll);
28 | };
29 | }, []);
30 |
31 | return (
32 |
33 | {isVisible && (
34 |
39 |
40 |
41 | )}
42 |
43 | );
44 | };
45 |
46 | export default ScrollToTop;
47 |
--------------------------------------------------------------------------------
/src/components/MobileNavBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { FaPhone, FaInstagram, FaLinkedin } from "react-icons/fa";
5 | import { SiTelegram } from "react-icons/si";
6 |
7 | const MobileNavBar: React.FC = () => {
8 | return (
9 |
44 | );
45 | };
46 |
47 | export default MobileNavBar;
48 |
--------------------------------------------------------------------------------
/src/components/BackButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { memo, useEffect, useState, useCallback } from "react";
5 |
6 | interface BackButtonProps {
7 | threshold?: number; // چند پیکسل اسکرول بشه تا دکمه دیده بشه
8 | }
9 |
10 | const BackButtonComponent: React.FC = ({
11 | threshold = 100,
12 | }) => {
13 | const router = useRouter();
14 | const [isVisible, setIsVisible] = useState(false);
15 |
16 | useEffect(() => {
17 | const handleScroll = () => {
18 | const currentScrollPosition = window.scrollY;
19 | const shouldShow = currentScrollPosition > threshold;
20 |
21 | // فقط وقتی مقدار عوض بشه state رو تغییر بده
22 | setIsVisible((prev) => (prev !== shouldShow ? shouldShow : prev));
23 | };
24 |
25 | // بررسی اولیه
26 | handleScroll();
27 |
28 | window.addEventListener("scroll", handleScroll, { passive: true });
29 | return () => window.removeEventListener("scroll", handleScroll);
30 | }, [threshold]);
31 |
32 | const goBack = useCallback(() => {
33 | router.back();
34 | }, [router]);
35 |
36 | if (!isVisible) return null;
37 |
38 | return (
39 |
46 | Back
47 |
48 | );
49 | };
50 |
51 | // memo پیچیده شد
52 | export default memo(BackButtonComponent);
53 |
--------------------------------------------------------------------------------
/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Link from "next/link";
5 | import Image from "next/image"; // استفاده از Image به جای img
6 |
7 | type Meal = {
8 | idMeal: string;
9 | strMeal: string;
10 | strMealThumb: string;
11 | };
12 |
13 | interface SidebarProps {
14 | randomMeals: Meal[]; // حذف sameCategoryMeals و currentCategory اگر استفاده نمیشوند
15 | }
16 |
17 | const Sidebar: React.FC = ({ randomMeals }) => {
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | const MealList: React.FC<{ title: string; meals: Meal[] }> = ({
26 | title,
27 | meals,
28 | }) => (
29 |
30 |
{title}
31 |
32 | {meals.map((meal) => (
33 |
38 |
45 |
{meal.strMeal}
46 |
47 | ))}
48 |
49 |
50 | );
51 |
52 | export default Sidebar;
53 |
--------------------------------------------------------------------------------
/src/components/Filter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | type FilterProps = {
6 | categories: string[];
7 | filteredCategories: string[];
8 | handleFilterChange: (category: string) => void;
9 | showAll: boolean;
10 | toggleShowAll: () => void;
11 | };
12 |
13 | const Filter: React.FC = ({
14 | categories,
15 | filteredCategories,
16 | handleFilterChange,
17 | showAll,
18 | toggleShowAll,
19 | }) => {
20 | const displayedCategories = showAll ? categories : categories.slice(0, 3);
21 |
22 | return (
23 |
24 |
25 | Category
26 |
27 | {displayedCategories.map((category) => (
28 |
32 | handleFilterChange(category)}
37 | className="form-checkbox w-5 h-5 text-blue-600 border-gray-300 rounded"
38 | />
39 |
40 | {category}
41 |
42 |
43 | ))}
44 |
45 | {categories.length > 3 && (
46 |
50 | {showAll ? "less" : "more"}
51 |
52 | )}
53 |
54 | );
55 | };
56 |
57 | export default Filter;
58 |
--------------------------------------------------------------------------------
/src/components/Pagination.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | interface PaginationProps {
6 | currentPage: number;
7 | totalPages: number;
8 | onPageChange: (page: number) => void;
9 | }
10 |
11 | const Pagination: React.FC = ({
12 | currentPage,
13 | totalPages,
14 | onPageChange,
15 | }) => {
16 | return (
17 |
18 |
onPageChange(currentPage - 1)}
25 | disabled={currentPage === 1}
26 | >
27 | Previous
28 |
29 |
30 |
31 | {Array.from({ length: totalPages }, (_, index) => (
32 | onPageChange(index + 1)}
40 | >
41 | {index + 1}
42 |
43 | ))}
44 |
45 |
46 |
onPageChange(currentPage + 1)}
53 | disabled={currentPage === totalPages}
54 | >
55 | Next
56 |
57 |
58 | );
59 | };
60 |
61 | export default Pagination;
62 |
--------------------------------------------------------------------------------
/src/app/menu/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState } from "react";
4 | import { useFetchCategories } from "../../hooks/useFetchCategories";
5 | import Filter from "@/components/Filter";
6 | import CategoryCard from "@/components/CategoryCard";
7 | import BackButton from "@/components/BackButton";
8 |
9 | const MenuPage: React.FC = () => {
10 | const { categories, loading, error } = useFetchCategories();
11 | const [filteredCategories, setFilteredCategories] = useState([]);
12 | const [showAll, setShowAll] = useState(false);
13 |
14 | const handleFilterChange = (category: string) => {
15 | const updatedCategories = filteredCategories.includes(category)
16 | ? filteredCategories.filter((cat) => cat !== category)
17 | : [...filteredCategories, category];
18 |
19 | setFilteredCategories(updatedCategories);
20 | };
21 |
22 | const toggleShowAll = () => {
23 | setShowAll((prev) => !prev);
24 | };
25 |
26 | const filteredData = filteredCategories.length
27 | ? categories.filter((cat) => filteredCategories.includes(cat.strCategory))
28 | : categories;
29 |
30 | if (loading) return Loading categories...
;
31 | if (error) return {error}
;
32 |
33 | return (
34 |
35 |
36 | {filteredData.map((category) => (
37 |
38 | ))}
39 |
40 |
cat.strCategory)}
42 | filteredCategories={filteredCategories}
43 | handleFilterChange={handleFilterChange}
44 | showAll={showAll}
45 | toggleShowAll={toggleShowAll}
46 | />
47 |
48 |
49 | );
50 | };
51 |
52 | export default MenuPage;
53 |
--------------------------------------------------------------------------------
/src/components/ImageSlider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 | import Image from "next/image"; // ایمپورت Image از next/image
5 | import api from "@/utils/api";
6 |
7 | const ImageSlider = () => {
8 | const [images, setImages] = useState([]);
9 | const [currentIndex, setCurrentIndex] = useState(0);
10 |
11 | useEffect(() => {
12 | const fetchImages = async () => {
13 | try {
14 | const response = await api.get("/categories.php");
15 | const fetchedImages = response.data.categories.map(
16 | (category: { strCategoryThumb: string }) => category.strCategoryThumb,
17 | );
18 | setImages(fetchedImages);
19 | } catch (error) {
20 | console.error("Error fetching images:", error);
21 | }
22 | };
23 |
24 | fetchImages();
25 | }, []);
26 |
27 | useEffect(() => {
28 | const interval = setInterval(() => {
29 | setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length);
30 | }, 5000);
31 | return () => clearInterval(interval);
32 | }, [images.length]);
33 |
34 | if (images.length === 0) {
35 | return Loading...
;
36 | }
37 |
38 | return (
39 |
40 | {images.map((image, index) => (
41 |
47 |
54 |
55 | ))}
56 |
57 | );
58 | };
59 |
60 | export default ImageSlider;
61 |
--------------------------------------------------------------------------------
/src/components/CategoriesCarousel.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 | import Link from "next/link";
5 | import Image from "next/image";
6 | import api from "@/utils/api";
7 |
8 | // تعریف تایپ برای دادهها
9 | interface Category {
10 | strCategory: string;
11 | strCategoryThumb: string;
12 | }
13 |
14 | const CategoriesCarousel = () => {
15 | const [categories, setCategories] = useState([]);
16 |
17 | useEffect(() => {
18 | const fetchCategories = async () => {
19 | try {
20 | const response = await api.get("/categories.php");
21 | setCategories(response.data.categories || []);
22 | } catch (error) {
23 | console.error("Error fetching categories:", error);
24 | }
25 | };
26 |
27 | fetchCategories();
28 | }, []);
29 |
30 | if (!categories || categories.length === 0) {
31 | return Loading...
;
32 | }
33 |
34 | return (
35 |
36 |
37 |
38 | {categories.concat(categories).map((category, index) => (
39 |
43 |
44 |
51 |
52 | {category.strCategory}
53 |
54 |
55 |
56 | ))}
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default CategoriesCarousel;
64 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 🚀 Contributing to Our Project
2 |
3 | We are thrilled that you are interested in contributing to our project. Your support and involvement are what make our community strong and our project better. Whether you're fixing a bug, adding a feature, or simply improving documentation, every contribution is valuable. 💪
4 |
5 | ## 📝 How to Contribute
6 |
7 | ### 1. Fork the Repository
8 |
9 | Click the _Fork_ button at the top right of this page to create a copy of the repository under your GitHub account.
10 |
11 | ### 2. Clone Your Fork
12 |
13 | bash
14 | git clone https://github.com/frau-azadeh/Just-Food.git
15 | cd REPOSITORY_NAME
16 |
17 | ### 3. Create a Branch
18 |
19 | Create a new branch for your changes:
20 |
21 | git checkout -b your-feature-name
22 |
23 | ### 4. Make Your Changes
24 |
25 | Edit the code, fix bugs, or improve the documentation. Remember to write clean and well-documented code.
26 |
27 | ### 5. Commit Your Changes
28 |
29 | git add .
30 | git commit -m "Brief description of your changes"
31 |
32 | ### 6. Push to Your Fork
33 |
34 | git push origin your-feature-name
35 |
36 | ### 7. Create a Pull Request
37 |
38 | Go to the original repository, click on _Pull Requests, and select \*\*New Pull Request_. Choose your branch and submit the PR for review.
39 |
40 | ## ✅ Contribution Guidelines
41 |
42 | - Ensure your code follows the existing style and conventions.
43 | - Write clear, concise commit messages.
44 | - Provide a detailed description of your changes in the pull request.
45 | - Test your changes before submitting.
46 |
47 | ## 🌟 Additional Ways to Contribute
48 |
49 | - Report issues 🐛
50 | - Suggest new features ✨
51 | - Improve documentation 📖
52 | - Share the project with others 💬
53 |
54 | ## 🤝 Code of Conduct
55 |
56 | Please note that we follow a [Code of Conduct](CODE_OF_CONDUCT.md) to ensure a welcoming and inclusive environment.
57 |
58 | ## 💬 Need Help?
59 |
60 | If you need any help or have questions, feel free to open an issue or reach out to us. We’re happy to help! 🚀
61 |
62 | Thank you for helping us make this project better! ❤️
63 |
--------------------------------------------------------------------------------
/src/app/menu/[category]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState } from "react";
4 | import { useParams } from "next/navigation";
5 | import { useFetchMeals } from "../../../hooks/useFetchMeals";
6 | import Link from "next/link";
7 | import BackButton from "@/components/BackButton";
8 | import CategoriesCarousel from "@/components/CategoriesCarousel";
9 | import Pagination from "@/components/Pagination";
10 |
11 | const MealsPage: React.FC = () => {
12 | const { category } = useParams();
13 | const categoryString = Array.isArray(category) ? category[0] : category;
14 |
15 | const { meals, loading, error } = useFetchMeals(categoryString);
16 |
17 | const [currentPage, setCurrentPage] = useState(1);
18 | const itemsPerPage = 9;
19 |
20 | if (loading) return Loading meals...
;
21 | if (error) return {error}
;
22 |
23 | const indexOfLastItem = currentPage * itemsPerPage;
24 | const indexOfFirstItem = indexOfLastItem - itemsPerPage;
25 | const currentMeals = meals.slice(indexOfFirstItem, indexOfLastItem);
26 |
27 | const totalPages = Math.ceil(meals.length / itemsPerPage);
28 |
29 | return (
30 | <>
31 |
32 |
33 |
34 | Meals in {categoryString}
35 |
36 |
37 | {currentMeals.map((meal) => (
38 |
39 |
40 |
45 |
46 | {meal.strMeal}
47 |
48 |
49 |
50 | ))}
51 |
52 |
53 |
setCurrentPage(page)}
57 | />
58 |
59 |
60 |
61 | >
62 | );
63 | };
64 |
65 | export default MealsPage;
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🍔 Just Food
2 |
3 | A beautifully designed Food Menu Application built with Next.js, TypeScript, and TailwindCSS, where users can explore various food categories, view meal details, and filter their choices seamlessly.
4 |
5 | ## 📸 Screenshots
6 |
7 | ### 💻 Desktop View
8 |
9 | A clean and modern design for desktop users:
10 |
11 | 
12 |
13 | 
14 |
15 | ---
16 |
17 | ### 📊 Tablet View
18 |
19 | Perfectly scales for tablets and mid-sized devices:
20 |
21 | 
22 |
23 | ---
24 |
25 | ### 📱 Mobile View
26 |
27 | Fully responsive and optimized for mobile devices:
28 |
29 | 
30 |
31 | ## 🚀 Live Demo
32 |
33 | Check out the live application here: [Just Food](https://just-food5.vercel.app/)
34 |
35 | ## ✨ Features
36 |
37 | 🗂️ Dynamic Categories: Browse food categories fetched dynamically from an API.
38 |
39 | 🥗 Filter Options: Interactive filter to refine the displayed categories.
40 |
41 | 📜 Meal Details: Detailed view of each meal, including ingredients and instructions.
42 |
43 | 🌐 Responsive Design: Fully responsive UI for mobile, tablet, and desktop devices.
44 |
45 | 🎨 Styled with TailwindCSS: Clean and modern UI powered by TailwindCSS.
46 |
47 | ⚡ Optimized Performance: Built using Next.js 14 and App Router.
48 |
49 | ## 🛠️ Technologies Used
50 |
51 | Framework: Next.js 14
52 |
53 | Styling: TailwindCSS
54 |
55 | TypeScript: For strong typing and better developer experience.
56 |
57 | Axios: For fetching data from the TheMealDB API.
58 |
59 |
60 | # 🔧 Installation and Setup
61 |
62 | Follow these steps to set up the project locally:
63 |
64 | 1.Clone the repository:
65 | git clone https://github.com/your-username/just-food.git
66 | cd just-food
67 |
68 | 2.Install dependencies:
69 | npm install
70 |
71 | 3.Run the development server:
72 | npm run dev
73 |
74 | 4.Open the app in your browser:
75 | http://localhost:3000
76 |
77 |
78 | # 🗺️ API Integration
79 |
80 |
81 | This project uses the TheMealDB API to fetch:
82 | Categories: /categories.php
83 | Meals by Category: /filter.php?c={category}
84 | Meal Details: /lookup.php?i={mealId}
85 |
86 | # 🤝 Contributing
87 |
88 | 🌻 Azadeh Sharifi Soltani
89 |
90 | Feel free to contribute to this project by submitting a pull request or opening an issue!
91 | Made with 💻, ☕, and 🌻 by Azadeh Sharifi Soltani
92 |
--------------------------------------------------------------------------------
/src/components/MealNavigation.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useEffect } from "react";
4 | import { useParams, useRouter } from "next/navigation";
5 | import api from "@/utils/api";
6 |
7 | interface Meal {
8 | idMeal: string;
9 | strMeal: string;
10 | }
11 |
12 | const MealNavigation: React.FC = () => {
13 | const { idMeal } = useParams();
14 | const router = useRouter();
15 | const [meals, setMeals] = useState([]);
16 | const [currentIndex, setCurrentIndex] = useState(-1);
17 |
18 | useEffect(() => {
19 | const fetchMeals = async () => {
20 | try {
21 | const mealResponse = await api.get(`/lookup.php?i=${idMeal}`);
22 | const meal = mealResponse.data.meals[0];
23 |
24 | const categoryResponse = await api.get(
25 | `/filter.php?c=${meal.strCategory}`,
26 | );
27 | const mealList = categoryResponse.data.meals;
28 |
29 | setMeals(mealList);
30 |
31 | const index = mealList.findIndex((m: Meal) => m.idMeal === idMeal);
32 | setCurrentIndex(index);
33 | } catch (error) {
34 | console.error("Error fetching meals:", error);
35 | }
36 | };
37 |
38 | fetchMeals();
39 | }, [idMeal]);
40 |
41 | const handleNavigation = (direction: "prev" | "next") => {
42 | const newIndex = direction === "prev" ? currentIndex - 1 : currentIndex + 1;
43 |
44 | if (newIndex >= 0 && newIndex < meals.length) {
45 | router.push(`/meal/${meals[newIndex].idMeal}`);
46 | }
47 | };
48 |
49 | if (meals.length === 0 || currentIndex === -1) {
50 | return Loading navigation...
;
51 | }
52 |
53 | return (
54 |
55 | handleNavigation("prev")}
57 | disabled={currentIndex === 0}
58 | className={`px-4 py-2 rounded-lg bg-[#7f1d1d] text-white font-medium transition ${
59 | currentIndex === 0 ? "opacity-50 cursor-not-allowed" : ""
60 | }`}
61 | >
62 | Previous
63 |
64 |
65 | {currentIndex + 1} of {meals.length}
66 |
67 | handleNavigation("next")}
69 | disabled={currentIndex === meals.length - 1}
70 | className={`px-4 py-2 rounded-lg bg-[#7f1d1d] text-white font-medium transition ${
71 | currentIndex === meals.length - 1
72 | ? "opacity-50 cursor-not-allowed"
73 | : ""
74 | }`}
75 | >
76 | Next
77 |
78 |
79 | );
80 | };
81 |
82 | export default MealNavigation;
83 |
--------------------------------------------------------------------------------
/src/components/RandomMeals.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 | import Link from "next/link";
5 | import Image from "next/image"; // ایمپورت Image از next/image
6 | import axios from "axios";
7 |
8 | interface Meal {
9 | idMeal: string;
10 | strMeal: string;
11 | strMealThumb: string;
12 | strInstructions: string;
13 | }
14 |
15 | const RandomMeals: React.FC = () => {
16 | const [meals, setMeals] = useState([]);
17 | const [loading, setLoading] = useState(true);
18 |
19 | useEffect(() => {
20 | const fetchRandomMeals = async () => {
21 | setLoading(true);
22 | try {
23 | const promises = Array.from({ length: 3 }, () =>
24 | axios.get<{ meals: Meal[] }>(
25 | "https://www.themealdb.com/api/json/v1/1/random.php",
26 | ),
27 | );
28 |
29 | const responses = await Promise.all(promises);
30 | const fetchedMeals = responses.map(
31 | (response) => response.data.meals[0],
32 | );
33 | setMeals(fetchedMeals);
34 | } catch (error) {
35 | console.error("Error fetching meals:", error);
36 | } finally {
37 | setLoading(false);
38 | }
39 | };
40 |
41 | fetchRandomMeals();
42 | }, []);
43 |
44 | if (loading) {
45 | return (
46 |
49 | );
50 | }
51 |
52 | if (!meals || meals.length === 0) {
53 | return (
54 |
55 |
No meals found.
56 |
57 | );
58 | }
59 |
60 | return (
61 |
62 |
63 | {meals.map((meal, index) => (
64 |
70 |
71 |
78 |
79 |
80 |
81 | {meal.strMeal}
82 |
83 |
84 | {meal.strInstructions.slice(0, 300)}...
85 |
86 |
90 | Learn More →
91 |
92 |
93 |
94 | ))}
95 |
96 |
97 | );
98 | };
99 | export default RandomMeals;
100 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { FaFacebook, FaTwitter, FaInstagram, FaLinkedin } from "react-icons/fa";
5 |
6 | const Footer: React.FC = () => {
7 | return (
8 |
108 | );
109 | };
110 |
111 | export default Footer;
112 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | designweb.azadeh@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/src/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useEffect } from "react";
4 | import { FaBars, FaTimes, FaSearch } from "react-icons/fa";
5 | import { IoMdRestaurant } from "react-icons/io";
6 | import Link from "next/link";
7 | import api from "@/utils/api";
8 |
9 | const Navbar: React.FC = () => {
10 | const [isOpen, setIsOpen] = useState(false);
11 | const [searchQuery, setSearchQuery] = useState("");
12 | const [categories, setCategories] = useState([]);
13 | const [filteredCategories, setFilteredCategories] = useState([]);
14 |
15 | const toggleMenu = () => {
16 | setIsOpen(!isOpen);
17 | };
18 |
19 | useEffect(() => {
20 | const fetchCategories = async () => {
21 | try {
22 | const response = await api.get("/categories.php");
23 | const categoryNames = response.data.categories.map(
24 | (category: { strCategory: string }) => category.strCategory,
25 | );
26 | setCategories(categoryNames);
27 | } catch (error) {
28 | console.error("Error fetching categories:", error);
29 | }
30 | };
31 |
32 | fetchCategories();
33 | }, []);
34 |
35 | useEffect(() => {
36 | if (searchQuery) {
37 | const filtered = categories.filter((category) =>
38 | category.toLowerCase().includes(searchQuery.toLowerCase()),
39 | );
40 | setFilteredCategories(filtered);
41 | } else {
42 | setFilteredCategories([]);
43 | }
44 | }, [searchQuery, categories]);
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 | Just Food
53 |
54 |
55 |
56 |
57 |
75 | {filteredCategories.length > 0 && (
76 |
77 | {filteredCategories.map((category) => (
78 | {
82 | setSearchQuery("");
83 | setIsOpen(false);
84 | window.location.href = `/menu/${category}`;
85 | }}
86 | >
87 | {category}
88 |
89 | ))}
90 |
91 | )}
92 |
93 |
94 |
98 | {isOpen ? : }
99 |
100 |
101 | {/* Links for Desktop */}
102 |
103 |
104 | Menu
105 |
106 |
107 | About Us
108 |
109 |
110 | Contact
111 |
112 |
113 |
114 |
115 | {isOpen && (
116 |
117 |
118 |
123 | Menu
124 |
125 |
130 | About Us
131 |
132 |
137 | Contact
138 |
139 |
140 |
141 |
159 | {filteredCategories.length > 0 && (
160 |
161 | {filteredCategories.map((category) => (
162 | {
166 | setSearchQuery("");
167 | setIsOpen(false);
168 | window.location.href = `/menu/${category}`;
169 | }}
170 | >
171 | {category}
172 |
173 | ))}
174 |
175 | )}
176 |
177 |
178 |
179 | )}
180 |
181 | );
182 | };
183 |
184 | export default Navbar;
185 |
--------------------------------------------------------------------------------
/src/app/meal/[idMeal]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useEffect } from "react";
4 | import { useParams } from "next/navigation";
5 | import { useFetchMealDetails } from "../../../hooks/useFetchMealDetails";
6 | import api from "../../../utils/api";
7 | import Link from "next/link";
8 | import BackButton from "@/components/BackButton";
9 | import MealNavigation from "@/components/MealNavigation";
10 |
11 | // تعریف اینترفیسها برای دادهها
12 | interface Meal {
13 | idMeal: string;
14 | strMeal: string;
15 | strMealThumb: string;
16 | strCategory?: string;
17 | strArea?: string;
18 | strInstructions?: string;
19 | [key: string]: string | undefined; // برای فیلدهای دیگر
20 | }
21 |
22 | interface Category {
23 | strCategory: string;
24 | }
25 |
26 | const MealDetailsPage: React.FC = () => {
27 | const { idMeal } = useParams();
28 | const mealId = Array.isArray(idMeal) ? idMeal[0] : idMeal;
29 |
30 | const { meal, loading, error } = useFetchMealDetails(mealId);
31 |
32 | const [sameCategoryMeals, setSameCategoryMeals] = useState([]);
33 | const [randomMeals, setRandomMeals] = useState([]);
34 |
35 | useEffect(() => {
36 | if (meal?.strCategory) {
37 | const fetchSameCategoryMeals = async () => {
38 | try {
39 | const response = await api.get(`/filter.php?c=${meal.strCategory}`);
40 | setSameCategoryMeals(response.data.meals.slice(0, 3));
41 | } catch (err) {
42 | console.error("Error fetching same category meals:", err);
43 | }
44 | };
45 |
46 | const fetchRandomMeals = async () => {
47 | try {
48 | const response = await api.get(`/categories.php`);
49 | const categories: Category[] = response.data.categories;
50 |
51 | const randomCategories = categories
52 | .map((category) => category.strCategory)
53 | .filter((category) => category !== meal.strCategory)
54 | .sort(() => Math.random() - 0.5)
55 | .slice(0, 3);
56 |
57 | const randomMealsPromises = randomCategories.map((category) =>
58 | api.get(`/filter.php?c=${category}`),
59 | );
60 |
61 | const randomMealsResponses = await Promise.all(randomMealsPromises);
62 |
63 | const randomMealsData = randomMealsResponses
64 | .map((res) => res.data.meals[0])
65 | .filter(Boolean); // حذف مقادیر null یا undefined
66 |
67 | setRandomMeals(randomMealsData);
68 | } catch (err) {
69 | console.error("Error fetching random meals:", err);
70 | }
71 | };
72 |
73 | fetchSameCategoryMeals();
74 | fetchRandomMeals();
75 | }
76 | }, [meal]);
77 |
78 | if (loading) return Loading meal details...
;
79 | if (error) return {error}
;
80 | if (!meal) return No meal found
;
81 |
82 | const ingredients = Object.keys(meal)
83 | .filter((key) => key.startsWith("strIngredient") && meal[key])
84 | .map((key) => meal[key]);
85 |
86 | const instructions = meal.strInstructions?.split(". ") || [];
87 |
88 | return (
89 |
90 |
91 |
97 |
98 |
99 | {meal.strMeal}
100 |
101 |
102 |
103 |
104 |
105 |
106 | Category
107 |
108 |
{meal.strCategory}
109 |
110 |
111 |
112 | Cuisine
113 |
114 |
{meal.strArea}
115 |
116 |
117 |
118 | Ingredients
119 |
120 |
121 | {ingredients.map((ingredient, index) => (
122 |
126 | {ingredient}
127 |
128 | ))}
129 |
130 |
131 | Instructions
132 |
133 |
134 | {instructions.map((step, index) => (
135 |
136 | {step}.
137 |
138 | ))}
139 |
140 |
141 |
142 |
143 |
144 |
145 | Latest in {meal.strCategory}
146 |
147 |
148 | {sameCategoryMeals.map((sameMeal) => (
149 |
154 |
159 |
160 | {sameMeal.strMeal}
161 |
162 |
163 | ))}
164 |
165 |
166 |
167 |
168 | Explore Other Categories
169 |
170 |
171 | {randomMeals.map((randomMeal) => (
172 |
177 |
182 |
183 | {randomMeal.strMeal}
184 |
185 |
186 | ))}
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | );
195 | };
196 |
197 | export default MealDetailsPage;
198 |
--------------------------------------------------------------------------------