initial commit
This commit is contained in:
commit
ee8aedd857
1599 changed files with 652440 additions and 0 deletions
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
69
frontend/README.md
Normal file
69
frontend/README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
frontend/eslint.config.js
Normal file
23
frontend/eslint.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Recipe App</title>
|
||||
<!-- Vite injects your CSS/JS automatically -->
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4227
frontend/package-lock.json
generated
Normal file
4227
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
frontend/package.json
Normal file
35
frontend/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
10
frontend/src/App.css
Normal file
10
frontend/src/App.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
/* Import Tailwind layers */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* App-specific tweaks */
|
||||
.app-container {
|
||||
@apply p-4;
|
||||
}
|
||||
27
frontend/src/App.tsx
Normal file
27
frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
|
||||
import RecipeListView from "./components/RecipeListView"
|
||||
import RecipeDetailView from "./components/RecipeDetailView"
|
||||
import RecipeEditView from "./components/RecipeEditView"
|
||||
|
||||
/**
|
||||
* Main application component.
|
||||
* Defines routes for the recipe list, detail view, and edit form.
|
||||
*/
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
{/* Home page: list of recipes */}
|
||||
<Route path="/" element={<RecipeListView />} />
|
||||
|
||||
{/* Detail page: shows one recipe */}
|
||||
<Route path="/recipe/:id" element={<RecipeDetailView />} />
|
||||
|
||||
{/* Edit page: form to edit a recipe */}
|
||||
<Route path="/recipe/:id/edit" element={<RecipeEditView />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
67
frontend/src/components/IngredientListEditor.tsx
Normal file
67
frontend/src/components/IngredientListEditor.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useState } from "react"
|
||||
import type { Ingredient } from "../types/ingredient"
|
||||
|
||||
type IngredientListEditorProps = {
|
||||
ingredients: Ingredient[]
|
||||
onChange: (ingredients: Ingredient[]) => void
|
||||
}
|
||||
|
||||
export function IngredientListEditor({ ingredients, onChange }: IngredientListEditorProps) {
|
||||
const handleUpdate = (index: number, field: keyof Ingredient, value: string | number) => {
|
||||
const updated = ingredients.map((ing, i) =>
|
||||
i === index ? { ...ing, [field]: field === "amount" ? Number(value) : value } : ing
|
||||
)
|
||||
onChange(updated)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
onChange([...ingredients, { name: "", amount: 0, unit: "" }])
|
||||
}
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
onChange(ingredients.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Ingredients</h3>
|
||||
{ingredients.map((ing, index) => (
|
||||
<div key={index} className="flex gap-2 mb-2 items-center">
|
||||
<input
|
||||
type="number"
|
||||
className="border p-2 w-20"
|
||||
placeholder="Amount"
|
||||
value={ing.amount}
|
||||
onChange={e => handleUpdate(index, "amount", e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="border p-2 w-20"
|
||||
placeholder="Unit"
|
||||
value={ing.unit ?? ""}
|
||||
onChange={e => handleUpdate(index, "unit", e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="border p-2 flex-1"
|
||||
placeholder="Name"
|
||||
value={ing.name}
|
||||
onChange={e => handleUpdate(index, "name", e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-red-500 text-white rounded"
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
✖
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-green-500 text-white rounded"
|
||||
onClick={handleAdd}
|
||||
>
|
||||
➕ Add Ingredient
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
frontend/src/components/RecipeDetailView.tsx
Normal file
61
frontend/src/components/RecipeDetailView.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { useParams, Link } from "react-router-dom"
|
||||
import { recipes } from "../mock_data/recipes"
|
||||
|
||||
/**
|
||||
* Displays the full detail of a single recipe,
|
||||
* including its ingredients, instructions, and image.
|
||||
*/
|
||||
export default function RecipeDetailView() {
|
||||
// Extract recipe ID from route params
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const recipe = recipes.find((r) => r.id === id)
|
||||
|
||||
if (!recipe) {
|
||||
return <p className="p-6">Recipe not found.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-4">{recipe.title}</h1>
|
||||
|
||||
{/* Recipe image */}
|
||||
{recipe.imageUrl && (
|
||||
<img
|
||||
src={recipe.imageUrl}
|
||||
alt={recipe.title}
|
||||
className="w-full rounded-xl mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ingredients */}
|
||||
<h2 className="font-semibold">Ingredients</h2>
|
||||
<ul className="list-disc pl-6">
|
||||
{recipe.ingredients.map((ing, i) => (
|
||||
<li key={i}>
|
||||
{ing.amount} {ing.unit ?? ""} {ing.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Instructions */}
|
||||
<h2 className="text-xl font-semibold mb-2">Instructions</h2>
|
||||
<p className="mb-6">{recipe.instructions}</p>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-4">
|
||||
<Link
|
||||
to={`/recipe/${recipe.id}/edit`}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="bg-gray-300 px-4 py-2 rounded-lg hover:bg-gray-400"
|
||||
>
|
||||
Back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
frontend/src/components/RecipeEditView.tsx
Normal file
60
frontend/src/components/RecipeEditView.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useParams, useNavigate } from "react-router-dom"
|
||||
//import { useEffect, useState } from "react"
|
||||
//import type { Recipe } from "../types/recipe"
|
||||
import RecipeEditor from "./RecipeEditor"
|
||||
import { recipes } from "../mock_data/recipes"
|
||||
|
||||
|
||||
export default function RecipeEditView() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
//const [recipe, setRecipe] = useState<Recipe | null>(null)
|
||||
console.log("searching for recipe with id", id)
|
||||
const recipe = recipes.find((r) => r.id === id)
|
||||
|
||||
// Fetch recipe data when editing an existing one
|
||||
/*seEffect(() => {
|
||||
if (id) {
|
||||
fetch(`http://localhost:4000/recipes/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setRecipe(data))
|
||||
} else {
|
||||
// new recipe case
|
||||
setRecipe({
|
||||
id: "",
|
||||
title: "",
|
||||
ingredients: [],
|
||||
instructions: "",
|
||||
photoUrl: "",
|
||||
})
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const handleSave = async (updated: Recipe) => {
|
||||
if (updated.id) {
|
||||
await fetch(`http://localhost:4000/recipes/${updated.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updated),
|
||||
})
|
||||
} else {
|
||||
await fetch("http://localhost:4000/recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updated),
|
||||
})
|
||||
}
|
||||
navigate("/") // back to list
|
||||
}
|
||||
if (!recipe) return <div>Loading...</div>
|
||||
*/
|
||||
const handleSave = () => {
|
||||
console.log("Saving")
|
||||
navigate("/") // back to list
|
||||
}
|
||||
|
||||
if (!recipe) return <div>Oops, there's no recipe in RecipeEditView</div>
|
||||
console.log("opening recipe in edit mode", recipe.title, id)
|
||||
|
||||
return <RecipeEditor recipe={recipe} onSave={handleSave} />
|
||||
}
|
||||
61
frontend/src/components/RecipeEditor.tsx
Normal file
61
frontend/src/components/RecipeEditor.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { useState } from "react"
|
||||
import type { Recipe } from "../types/recipe"
|
||||
import type { Ingredient } from "../types/ingredient"
|
||||
import {IngredientListEditor} from "./IngredientListEditor"
|
||||
|
||||
type RecipeEditorProps = {
|
||||
recipe: Recipe
|
||||
onSave: (recipe: Recipe) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor component for managing a recipe, including title,
|
||||
* ingredients (with amount, unit, name), instructions, and image URL.
|
||||
*/
|
||||
export default function RecipeEditor({ recipe, onSave }: RecipeEditorProps) {
|
||||
const [draft, setDraft] = useState<Recipe>(recipe)
|
||||
|
||||
const updateIngredients = (ingredients: Ingredient[]) => {
|
||||
setDraft({ ...draft, ingredients })
|
||||
}
|
||||
if (!recipe) return <div>Oops, there's no recipe in RecipeEditor...</div>
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-2">
|
||||
{recipe.id ? "Edit Recipe" : "New Recipe"}
|
||||
</h2>
|
||||
|
||||
{/* Title */}
|
||||
<h3>Title</h3>
|
||||
<input
|
||||
className="border p-2 w-full mb-2"
|
||||
placeholder="Title"
|
||||
value={draft.title}
|
||||
onChange={e => setDraft({ ...draft, title: e.target.value })}
|
||||
/>
|
||||
|
||||
{/* Ingredient List */}
|
||||
<IngredientListEditor
|
||||
ingredients={draft.ingredients}
|
||||
onChange={updateIngredients}
|
||||
/>
|
||||
|
||||
<h3>Instructions</h3>
|
||||
{/* Instructions */}
|
||||
<textarea
|
||||
className="border p-2 w-full mb-2"
|
||||
placeholder="Instructions"
|
||||
value={draft.instructions}
|
||||
onChange={e => setDraft({ ...draft, instructions: e.target.value })}
|
||||
/>
|
||||
|
||||
{/* Save Button */}
|
||||
<button
|
||||
className="p-2 bg-green-500 text-white rounded"
|
||||
onClick={() => onSave(draft)}
|
||||
>
|
||||
💾 Save
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
frontend/src/components/RecipeListView.tsx
Normal file
39
frontend/src/components/RecipeListView.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { Link } from "react-router-dom"
|
||||
import { recipes } from "../mock_data/recipes"
|
||||
|
||||
/**
|
||||
* Displays a list of recipes in a grid layout.
|
||||
* Each recipe links to its detail view.
|
||||
*/
|
||||
export default function RecipeListView() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-bold mb-6">Recipes</h1>
|
||||
|
||||
{/* Grid of recipe cards */}
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
{recipes.map((recipe) => (
|
||||
<Link
|
||||
key={recipe.id}
|
||||
to={`/recipe/${recipe.id}`}
|
||||
className="block bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition"
|
||||
>
|
||||
{/* Thumbnail image */}
|
||||
{recipe.imageUrl && (
|
||||
<img
|
||||
src={recipe.imageUrl}
|
||||
alt={recipe.title}
|
||||
className="w-full h-40 object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Recipe title */}
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-semibold">{recipe.title}</h2>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
frontend/src/index.css
Normal file
72
frontend/src/index.css
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
19
frontend/src/mock_data/recipes.ts
Normal file
19
frontend/src/mock_data/recipes.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Recipe } from "../types/recipe"
|
||||
|
||||
/**
|
||||
* Mock data set with some sample recipes.
|
||||
* In a real application, this would be fetched from a backend.
|
||||
*/
|
||||
export const recipes: Recipe[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Spaghetti Bolognese",
|
||||
ingredients: [
|
||||
{ name: "Spaghetti", amount: 200, unit: "g" },
|
||||
{ name: "Ground Beef", amount: 300, unit: "g" },
|
||||
{ name: "Tomato Sauce", amount: 400, unit: "ml" }
|
||||
],
|
||||
instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
|
||||
photoUrl: "https://source.unsplash.com/400x300/?spaghetti"
|
||||
}
|
||||
]
|
||||
15
frontend/src/types/ingredient.ts
Normal file
15
frontend/src/types/ingredient.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Represents a single ingredient in a recipe.
|
||||
*/
|
||||
export interface Ingredient {
|
||||
/** Name of the ingredient (e.g. "Spaghetti") */
|
||||
name: string
|
||||
|
||||
/** Quantity required (e.g. 200, 1.5) */
|
||||
amount: number
|
||||
|
||||
/** Unit of measurement (e.g. "g", "tbsp", "cups").
|
||||
* Optional for cases like "1 egg".
|
||||
*/
|
||||
unit?: string
|
||||
}
|
||||
22
frontend/src/types/recipe.ts
Normal file
22
frontend/src/types/recipe.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Ingredient } from "./ingredient"
|
||||
|
||||
/**
|
||||
* Represents a recipe object in the application.
|
||||
*/
|
||||
export interface Recipe {
|
||||
/** Unique identifier for the recipe */
|
||||
id: string
|
||||
|
||||
/** Title of the recipe */
|
||||
title: string
|
||||
|
||||
/** List of ingredients with amount + unit */
|
||||
ingredients: Ingredient[]
|
||||
|
||||
/** Preparation instructions */
|
||||
instructions: string
|
||||
|
||||
/** Optional image URL for the recipe */
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
9
frontend/tailwind.config.js
Normal file
9
frontend/tailwind.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
27
frontend/tsconfig.app.json
Normal file
27
frontend/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src", "../backend/src/mock_data/recipes.ts"]
|
||||
}
|
||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
frontend/tsconfig.node.json
Normal file
25
frontend/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
frontend/vite.config.ts
Normal file
7
frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue