backend docker refactor and frontend cleanup
This commit is contained in:
parent
96443b6afe
commit
63e33a45b4
14 changed files with 130 additions and 113 deletions
|
|
@ -1,15 +1,27 @@
|
|||
FROM node:22-slim
|
||||
FROM node:22-slim AS base
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY package*.json tsconfig*.json prisma.config.ts ./
|
||||
COPY prisma ./prisma
|
||||
COPY src ./src
|
||||
|
||||
RUN apt-get update -y && apt-get install -y openssl
|
||||
RUN npm install --omit=dev
|
||||
RUN npm run prisma:generate
|
||||
|
||||
RUN if [ "$NODE_ENV" = "dev" ]; then npm install; else npm install --omit=dev; fi
|
||||
FROM base AS builder
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM base
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
# COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY prisma.config.ts /app/
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD npm run $NODE_ENV
|
||||
CMD ["npm", "run", "prod"]
|
||||
|
|
|
|||
13
backend/Dockerfile.dev
Normal file
13
backend/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:22-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN apt-get update -y && apt-get install -y openssl
|
||||
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
|
@ -4,8 +4,9 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon ./src/index.ts",
|
||||
"production": "npx tsc && node ./dist/index.js",
|
||||
"ci": "tsc"
|
||||
"build": "tsc",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prod": "node ./dist/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ export const getRecipeById = async (
|
|||
};
|
||||
|
||||
export const addRecipe = async (req: Request, res: Response): Promise<void> => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.log(req.body);
|
||||
const createdRecipe = await model.addRecipe(req.body);
|
||||
|
|
@ -73,9 +70,6 @@ export const updateRecipe = async (
|
|||
): Promise<void> => {
|
||||
console.log(req.body);
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updatedRecipe = await model.updateRecipe(req.body, id);
|
||||
res.status(201).json(updatedRecipe);
|
||||
|
|
@ -89,9 +83,6 @@ export const updateRecipe = async (
|
|||
};
|
||||
|
||||
export const setStars = async (req: Request, res: Response): Promise<void> => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
return;
|
||||
}
|
||||
const id = parseInt(req.body.id, 10);
|
||||
const stars = parseInt(req.body.stars, 10);
|
||||
try {
|
||||
|
|
@ -110,9 +101,6 @@ export const deleteRecipe = async (
|
|||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
return;
|
||||
}
|
||||
const id = parseInt(req.body.id, 10);
|
||||
try {
|
||||
await model.deleteRecipe(id);
|
||||
|
|
|
|||
45
docker-compose.dev.yaml
Normal file
45
docker-compose.dev.yaml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
services:
|
||||
db:
|
||||
container_name: recipes_db_dev
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
ports:
|
||||
- "${DB_PORT}:5432"
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
backend:
|
||||
container_name: recipes_backend_dev
|
||||
image: recipes_backend
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "${BACKEND_PORT}:3000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./logs:/logs
|
||||
environment:
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
frontend:
|
||||
container_name: recipes_frontend_dev
|
||||
image: recipes_frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=dev
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
services:
|
||||
db:
|
||||
container_name: recipes_postgres_${ID}
|
||||
container_name: recipes_db
|
||||
image: docker.io/library/postgres:17
|
||||
# restart: always
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
@ -14,34 +14,29 @@ services:
|
|||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
backend:
|
||||
image: recipes_backend
|
||||
container_name: recipes_backend_${ID}
|
||||
container_name: recipes_backend
|
||||
image: forgejo.fredzernia.com/fred/recipes_backend:latest
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: ./backend
|
||||
args:
|
||||
NODE_ENV: ${NODE_ENV}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:3000"
|
||||
volumes:
|
||||
- ./backend:/usr/src/app
|
||||
- ./logs:/logs
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DATABASE_URL=${PRISMA_DB_URL}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
frontend:
|
||||
image: recipes_frontend
|
||||
container_name: recipes_frontend_${ID}
|
||||
container_name: recipes_frontend
|
||||
image: forgejo.fredzernia.com/fred/recipes_frontend:latest
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: ./backend
|
||||
args:
|
||||
NODE_ENV: ${NODE_ENV}
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
volumes:
|
||||
- ./frontend:/usr/src/app
|
||||
- ./dist/recipes_frontend:/usr/src/app/dist
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
|
|
|
|||
7
example.env
Normal file
7
example.env
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
DB_PORT=5432
|
||||
DB_USER=recipe_app
|
||||
DB_PASSWORD=password
|
||||
DB_NAME=recipe_app
|
||||
BACKEND_PORT=3000
|
||||
FRONTEND_PORT=8080
|
||||
DATABASE_URL='postgres://recipe_app:password@db:5432/'
|
||||
13
frontend/Dockerfile.dev
Normal file
13
frontend/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install;
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD npm run dev
|
||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
|
|
@ -87,6 +87,7 @@
|
|||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -1471,6 +1472,7 @@
|
|||
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
|
|
@ -1481,6 +1483,7 @@
|
|||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
|
|
@ -1541,6 +1544,7 @@
|
|||
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
|
|
@ -1792,6 +1796,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -1992,6 +1997,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
|
|
@ -2026,9 +2032,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001727",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
|
||||
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
|
||||
"version": "1.0.30001766",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2320,6 +2326,7 @@
|
|||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -3392,6 +3399,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -3568,6 +3576,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -3577,6 +3586,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
|
|
@ -4094,6 +4104,7 @@
|
|||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4153,6 +4164,7 @@
|
|||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -4245,6 +4257,7 @@
|
|||
"integrity": "sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
|
|
@ -4335,6 +4348,7 @@
|
|||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
"react-router-dom": "^7.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface DemoModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const DemoModal = ({ isOpen, onClose, closeModal }: DemoModalProps) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className={`z-50 modal-overlay fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center`} onClick={onClose}>
|
||||
<div className="modal-content bg-[var(--color-primaryBg)] p-12 rounded-md shadow-md" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-msg">
|
||||
<p>Thanks for checking out my app! Database write operations are disabled in demo mode.</p>
|
||||
<p><a className="text-[var(--color-textLink)]" href="mailto:access@fredzernia.com">access@fredzernia.com</a> to request access to the production build</p>
|
||||
<p>Find out more about this app <Link to={'/about'} className="text-blue-600">here</Link></p>
|
||||
</div>
|
||||
<div className="modal-buttons">
|
||||
<button className="bg-[var(--color-buttonBg)] rounded-md m-4 pt-1 pb-1 pr-2 pl-2" onClick={closeModal}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoModal;
|
||||
|
|
@ -1,20 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { addRecipe } from "../services/frontendApi.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import RecipeForm from "../components/RecipeForm.tsx";
|
||||
import DemoModal from "../components/DemoModal.tsx";
|
||||
import { type Recipe } from "../types/Recipe.ts"
|
||||
|
||||
function AddRecipe() {
|
||||
const [showDemoModal, setShowDemoModal] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const addRecipeForm = async (recipeData: Recipe) => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
setShowDemoModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await addRecipe(recipeData);
|
||||
navigate(`/recipe/${data.id}`);
|
||||
};
|
||||
|
|
@ -22,13 +14,6 @@ function AddRecipe() {
|
|||
return (
|
||||
<div className="add-recipe-card page-outer">
|
||||
<RecipeForm onSubmit={addRecipeForm} />
|
||||
{showDemoModal && (
|
||||
<DemoModal
|
||||
isOpen={showDemoModal}
|
||||
onClose={() => setShowDemoModal(false)}
|
||||
closeModal={() => setShowDemoModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
|
|||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { getRecipeById, updateRecipe } from "../services/frontendApi.js";
|
||||
import RecipeForm from "../components/RecipeForm.tsx";
|
||||
import DemoModal from "../components/DemoModal.tsx";
|
||||
import { type Recipe } from "../types/Recipe.ts"
|
||||
|
||||
function EditRecipe() {
|
||||
|
|
@ -10,7 +9,6 @@ function EditRecipe() {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const [recipe, setRecipe] = useState<Recipe>();
|
||||
const [showDemoModal, setShowDemoModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRecipe = async () => {
|
||||
|
|
@ -22,11 +20,6 @@ function EditRecipe() {
|
|||
}, [id]);
|
||||
|
||||
const updateRecipeForm = async (recipeData: Recipe) => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
setShowDemoModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRecipe(id, recipeData);
|
||||
navigate(`/recipe/${id}`);
|
||||
};
|
||||
|
|
@ -36,13 +29,6 @@ function EditRecipe() {
|
|||
{recipe && (
|
||||
<RecipeForm onSubmit={updateRecipeForm} initialData={recipe} />
|
||||
)}
|
||||
{showDemoModal && (
|
||||
<DemoModal
|
||||
isOpen={showDemoModal}
|
||||
onClose={() => setShowDemoModal(false)}
|
||||
closeModal={() => setShowDemoModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from "../services/frontendApi.js";
|
||||
import { type Recipe } from "../types/Recipe";
|
||||
import Modal from "../components/Modal.tsx";
|
||||
import DemoModal from "../components/DemoModal.tsx";
|
||||
import StarRating from "../components/StarRating.tsx";
|
||||
import TimeDisplay from "../components/TimeDisplay.tsx";
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ function RecipePage() {
|
|||
const [stars, setStars] = useState<number>(0);
|
||||
const [initialStars, setInitialStars] = useState<number | null>(null);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [showDemoModal, setShowDemoModal] = useState(false);
|
||||
const { id } = useParams();
|
||||
const isWebSource =
|
||||
recipe && recipe.details && recipe.details.author
|
||||
|
|
@ -38,13 +36,8 @@ function RecipePage() {
|
|||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
closeModal();
|
||||
setShowDemoModal(true);
|
||||
} else {
|
||||
handleDelete(recipe.details.id);
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -113,13 +106,13 @@ function RecipePage() {
|
|||
<div className="flex relative justify-between">
|
||||
<div className="invisible-buttons">
|
||||
<button
|
||||
onClick={() => {}}
|
||||
onClick={() => { }}
|
||||
className="invisible ar-button py-1 px-1 rounded mr-2 self-start"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {}}
|
||||
onClick={() => { }}
|
||||
className="invisible ar-button py-1 px-1 rounded self-start"
|
||||
>
|
||||
🗑️
|
||||
|
|
@ -220,13 +213,6 @@ function RecipePage() {
|
|||
confirmAction={confirmDelete}
|
||||
cancelAction={closeModal}
|
||||
/>
|
||||
{showDemoModal && (
|
||||
<DemoModal
|
||||
isOpen={showDemoModal}
|
||||
onClose={() => setShowDemoModal(false)}
|
||||
closeModal={() => setShowDemoModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue