set up demo mode

This commit is contained in:
fred 2025-08-05 18:11:03 +00:00
parent 9aac6e0eff
commit 04bfdd0c8f
10 changed files with 93 additions and 16 deletions

View file

@ -10,7 +10,7 @@ app.use(express.json());
// ####### ROUTES ####### // ####### ROUTES #######
app.get("/backend/test", async (req, res) => { app.get("/backend/test", async (req, res) => {
console.log("test"); console.log("test");
res.json({ test: "test" }); res.json({ env: process.env.NODE_ENV });
}); });
// ### GET ALL RECIPES ### // ### GET ALL RECIPES ###
app.get("/backend/recipes", async (req, res) => { app.get("/backend/recipes", async (req, res) => {
@ -81,6 +81,7 @@ app.get("/backend/recipe/:id", async (req, res) => {
// ### ADD RECIPE ### // ### ADD RECIPE ###
app.post("/backend/add-recipe", async (req, res) => { app.post("/backend/add-recipe", async (req, res) => {
if (process.env.NODE_ENV === 'demo') { return; };
const { name, author, cuisine, stars, ingredients, steps, prep_minutes, cook_minutes } = req.body; const { name, author, cuisine, stars, ingredients, steps, prep_minutes, cook_minutes } = req.body;
try { try {
const [id] = await db("recipes").insert( const [id] = await db("recipes").insert(
@ -118,6 +119,7 @@ app.post("/backend/add-recipe", async (req, res) => {
// ### SET STARS ### // ### SET STARS ###
app.post("/backend/set-stars", async (req, res) => { app.post("/backend/set-stars", async (req, res) => {
if (process.env.NODE_ENV === 'demo') { return; };
const { id, stars } = req.body; const { id, stars } = req.body;
try { try {
await db("recipes").where({ id: id }).update({ stars: stars }); await db("recipes").where({ id: id }).update({ stars: stars });
@ -130,6 +132,7 @@ app.post("/backend/set-stars", async (req, res) => {
// ### DELETE RECIPE ### // ### DELETE RECIPE ###
app.delete("/backend/delete-recipe", async (req, res) => { app.delete("/backend/delete-recipe", async (req, res) => {
if (process.env.NODE_ENV === 'demo') { return; };
const { id } = req.body; const { id } = req.body;
try { try {
await db("recipe_steps").where({ recipe_id: id }).del(); await db("recipe_steps").where({ recipe_id: id }).del();

View file

@ -20,6 +20,25 @@ module.exports = {
}, },
}, },
demo: {
client: "postgresql",
connection: {
host: "db",
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
pool: {
min: 2,
max: 10,
},
migrations: {
tableName: "knex_migrations",
directory: "./migrations",
},
},
production: { production: {
client: "postgresql", client: "postgresql",
connection: { connection: {

View file

@ -3,8 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node backendServer.js", "dev": "node backendServer.js",
"demo": "node backendServer.js",
"production": "node backendServer.js" "production": "node backendServer.js"
}, },
"keywords": [], "keywords": [],

View file

@ -1,6 +1,6 @@
services: services:
db: db:
container_name: recipes_postgres container_name: recipes_postgres_${ID}
image: docker.io/library/postgres:17 image: docker.io/library/postgres:17
# restart: always # restart: always
env_file: env_file:
@ -15,13 +15,13 @@ services:
- ./postgres/db:/var/lib/postgresql/data - ./postgres/db:/var/lib/postgresql/data
backend: backend:
image: recipes_backend image: recipes_backend
container_name: recipes_backend container_name: recipes_backend_${ID}
build: build:
context: ./backend context: ./backend
args: args:
NODE_ENV: ${NODE_ENV} NODE_ENV: ${NODE_ENV}
ports: ports:
- "3000:3000" - "${BACKEND_PORT}:3000"
volumes: volumes:
- ./backend:/usr/src/app - ./backend:/usr/src/app
environment: environment:
@ -31,13 +31,13 @@ services:
- DB_NAME=${DB_NAME} - DB_NAME=${DB_NAME}
frontend: frontend:
image: recipes_frontend image: recipes_frontend
container_name: recipes_frontend container_name: recipes_frontend_${ID}
build: build:
context: ./backend context: ./backend
args: args:
NODE_ENV: ${NODE_ENV} NODE_ENV: ${NODE_ENV}
ports: ports:
- "8081:80" - "${FRONTEND_PORT}:80"
volumes: volumes:
- ./frontend:/usr/src/app - ./frontend:/usr/src/app
environment: environment:

View file

@ -6,6 +6,7 @@
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0 --port 80", "dev": "vite --host 0.0.0.0 --port 80",
"production": "vite --host 0.0.0.0 --port 80", "production": "vite --host 0.0.0.0 --port 80",
"demo": "vite --host 0.0.0.0 --port 80",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"

View file

@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { type RecipeSmall } from "../types/Recipe" import { type RecipeSmall } from "../types/Recipe"
import Modal from '../components/Modal.tsx' import Modal from '../components/Modal.tsx'
import DemoModal from '../components/DemoModal.tsx'
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
interface CookbookRecipeTileProps { interface CookbookRecipeTileProps {
@ -9,13 +10,19 @@ interface CookbookRecipeTileProps {
} }
function CookbookRecipeTile({ recipe, handleDelete }: CookbookRecipeTileProps) { function CookbookRecipeTile({ recipe, handleDelete }: CookbookRecipeTileProps) {
const [isModalOpen, setIsModalOpen] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false);
const [showDemoModal, setShowDemoModal] = useState(false);
const openModal = () => { setIsModalOpen(true) }; const openModal = () => { setShowConfirmModal(true) };
const closeModal = () => { setIsModalOpen(false) }; const closeModal = () => { setShowConfirmModal(false) };
const confirmDelete = () => { const confirmDelete = () => {
if (process.env.NODE_ENV === 'demo') {
closeModal();
setShowDemoModal(true);
} else {
handleDelete(recipe.id); handleDelete(recipe.id);
closeModal(); closeModal();
}
}; };
@ -30,12 +37,19 @@ function CookbookRecipeTile({ recipe, handleDelete }: CookbookRecipeTileProps) {
</button> </button>
</div> </div>
<Modal <Modal
isOpen={isModalOpen} isOpen={showConfirmModal}
onClose={closeModal} onClose={closeModal}
message="Are you sure you want to delete this recipe?" message="Are you sure you want to delete this recipe?"
confirmAction={confirmDelete} confirmAction={confirmDelete}
cancelAction={closeModal} cancelAction={closeModal}
/> />
{showDemoModal && (
<DemoModal
isOpen={showDemoModal}
onClose={() => setShowDemoModal(false)}
closeModal={() => setShowDemoModal(false)}
/>
)}
</div> </div>
); );
}; };

View file

@ -0,0 +1,28 @@
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="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-amber-200 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-blue-600" 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-amber-600 rounded-md m-4 pt-1 pb-1 pr-2 pl-2" onClick={closeModal}>OK</button>
</div>
</div>
</div>
);
};
export default DemoModal;

View file

@ -4,6 +4,7 @@ import { useNavigate } from "react-router-dom";
import AddBulkIngredients from "../components/AddBulkIngredients.tsx" import AddBulkIngredients from "../components/AddBulkIngredients.tsx"
import AddBulkSteps from "../components/AddBulkSteps.tsx" import AddBulkSteps from "../components/AddBulkSteps.tsx"
import StarRating from "../components/StarRating.tsx" import StarRating from "../components/StarRating.tsx"
import DemoModal from '../components/DemoModal.tsx'
// import { type Step } from "../types/Recipe"; // import { type Step } from "../types/Recipe";
interface Step { interface Step {
@ -23,9 +24,14 @@ function AddRecipe() {
const [stars, setStars] = useState(0); const [stars, setStars] = useState(0);
const [prepMinutes, setPrepMinutes] = useState(5); const [prepMinutes, setPrepMinutes] = useState(5);
const [cookMinutes, setCookMinutes] = useState(5); const [cookMinutes, setCookMinutes] = useState(5);
const [showDemoModal, setShowDemoModal] = useState(false);
const addRecipeForm = async (event: React.FormEvent) => { const addRecipeForm = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
if (process.env.NODE_ENV === 'demo') {
setShowDemoModal(true);
return;
}
const stepsHash = Object.fromEntries( const stepsHash = Object.fromEntries(
steps.map(step => [step.step_number, step.instruction]) steps.map(step => [step.step_number, step.instruction])
); );
@ -139,6 +145,13 @@ function AddRecipe() {
submit submit
</button> </button>
</form> </form>
{showDemoModal && (
<DemoModal
isOpen={showDemoModal}
onClose={() => setShowDemoModal(false)}
closeModal={() => setShowDemoModal(false)}
/>
)}
</div> </div>
) )
} }

View file

@ -1,4 +1,4 @@
const baseUrl = process.env.NODE_ENV === 'production' const baseUrl = process.env.NODE_ENV !== 'dev'
? '/' ? '/'
: 'http://localhost:3000/'; : 'http://localhost:3000/';

View file

@ -5,7 +5,6 @@ import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: "ec683cee72d30c5030.fredzernia.com", allowedHosts: ["recipe-prod.fredzernia.com", "recipe-demo.fredzernia.com"],
allowedHosts: ["ec683cee72d30c5030.fredzernia.com"],
}, },
}); });