@@ -41,12 +76,61 @@ export default function RecipeFinder() {
if (!user) return null;
+ const handleLogout = () => {
+ logout();
+ router.replace("/");
+ }
const handleProfil = () => {
setOpen(false);
console.log("Profil ansehen");
router.push("/profile");
};
+ const handleAdd = (ingredient: IngredientInput) => {
+ setIngredients(prev => [...prev, ingredient]);
+ };
+
+ const handleRemoveIngredient = (name: string) => {
+ setIngredients(prev => prev.filter(z => z.name !== name));
+ }
+
+ const handleEditSave = (name: string) => {
+ const amountNum = parseFloat(editAmount);
+ if (!editAmount || isNaN(amountNum) || amountNum <= 0) return;
+ setIngredients(prev => prev.map(i =>
+ i.name === name ? { ...i, amount: amountNum, unit: editUnit } : i
+ ));
+ setEditingIngredient(null);
+ };
+
+ const handleEditStart = (i: IngredientInput) => {
+ setEditingIngredient(i.name);
+ setEditAmount(String(i.amount));
+ setEditUnit(i.unit);
+ };
+
+ const handleSearch = async () => {
+ if (ingredients.length === 0) {
+ setSearchError("Bitte mindestens eine Zutat hinzufügen.");
+ return;
+ }
+ setSearchError("");
+ setSearching(true);
+ try {
+ const res = await fetchWithAuth('/recipes/search', {
+ method: "POST",
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify({zutaten: ingredients, servings}),
+ });
+ const data = await res.json();
+ setResults(data.rezepte ?? []);
+ } catch {
+ setSearchError("Suche fehlgeschlagen.");
+ } finally {
+ setSearching(false);
+ }
+ };
+
return (
{/* Navbar */}
@@ -58,9 +142,7 @@ export default function RecipeFinder() {
@@ -85,7 +167,7 @@ export default function RecipeFinder() {
Profil ansehen
- {/* Dashboard Content */}
-
- Dashboard
- Willkommen zurück, {user.name}!
+
);
}
\ No newline at end of file
diff --git a/project/frontend/app/recipeFinder/popup.tsx b/project/frontend/app/recipeFinder/popup.tsx
new file mode 100644
index 0000000..ec2f81b
--- /dev/null
+++ b/project/frontend/app/recipeFinder/popup.tsx
@@ -0,0 +1,81 @@
+import { useState } from "react";
+import "./style.css";
+
+const EINHEITEN = ["Stück", "g", "kg", "ml", "l", "EL", "TL", "Prise"];
+
+export interface IngredientInput {
+ name: string;
+ amount: number;
+ unit: string;
+}
+
+interface Props {
+ ingredients: IngredientInput[];
+ onAdd: (ingredient: IngredientInput) => void;
+ servings: number;
+ onServingsChange: (s: number) => void;
+}
+
+export default function AddIngredientsPopup({ ingredients, onAdd}: Props) {
+ const [ingredientName, setIngredientName] = useState("");
+ const [ingredientAmount, setIngredientAmount] = useState("");
+ const [ingredientUnit, setIngredientUnit] = useState("Stück");
+ const [inputError, setInputError] = useState("");
+
+ const handleAddIngredient = () => {
+ const trimmedName = ingredientName.trim();
+ const amountNum = parseFloat(ingredientAmount);
+
+ if (!trimmedName) { setInputError("Bitte eine Zutat eingeben."); return; }
+ if (!ingredientAmount || isNaN(amountNum) || amountNum <= 0) { setInputError("Bitte eine gültige Menge eingeben."); return; }
+ if (ingredients.some(z => z.name.toLowerCase() === trimmedName.toLowerCase())) {
+ setInputError("Diese Zutat wurde bereits hinzugefügt.");
+ return;
+ }
+
+ onAdd({ name: trimmedName, amount: amountNum, unit: ingredientUnit });
+ setIngredientName("");
+ setIngredientAmount("");
+ setIngredientUnit("Stück");
+ setInputError("");
+ };
+
+ return (
+
+
Zutaten Hinzufügen
+
+
+ setIngredientName(e.target.value)}
+ onKeyDown={e => e.key === "Enter" && handleAddIngredient()}
+ placeholder="z.B. Tomaten"
+ className="popup__input"
+ />
+ setIngredientAmount(e.target.value)}
+ onKeyDown={e => e.key === "Enter" && handleAddIngredient()}
+ placeholder="Menge"
+ min={0}
+ className="popup__input popup__input--amount"
+ />
+
+
+
+ {inputError &&
{inputError}
}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/project/frontend/app/recipeFinder/style.css b/project/frontend/app/recipeFinder/style.css
index 888974b..fdc8996 100644
--- a/project/frontend/app/recipeFinder/style.css
+++ b/project/frontend/app/recipeFinder/style.css
@@ -71,4 +71,406 @@
opacity: 1;
transform: translateY(0);
}
+}
+
+/* ── AddIngredientsPopup ───────────────────────────────────── */
+.popup {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.popup__title {
+ font-size: 20px;
+ font-weight: 600;
+ color: #111;
+ font-family: system-ui;
+ text-align: center;
+ margin: 0;
+}
+
+.popup__fields {
+ display: flex;
+ gap: 10px;
+}
+
+.popup__input {
+ flex: 1;
+ padding: 10px 12px;
+ border-radius: 6px;
+ border: 1px solid #ddd;
+ font-size: 14px;
+ font-family: system-ui;
+ outline: none;
+ transition: border-color 0.15s;
+ background: #f3f3f5;
+ box-sizing: border-box;
+}
+
+.popup__input:focus {
+ border-color: #030213;
+ background: #fff;
+}
+
+.popup__input--amount {
+ flex: 0 0 90px;
+ width: 90px;
+ min-width: 0;
+}
+
+.popup__select {
+ padding: 10px 12px;
+ border-radius: 6px;
+ border: 1px solid #ddd;
+ font-size: 14px;
+ font-family: system-ui;
+ outline: none;
+ background: #f3f3f5;
+ cursor: pointer;
+ transition: border-color 0.15s;
+}
+
+.popup__select:focus {
+ border-color: #030213;
+ background: #fff;
+}
+
+.popup__error {
+ font-size: 13px;
+ color: #b91c1c;
+ font-family: system-ui;
+ text-align: center;
+ margin: 0;
+}
+
+.popup__btn {
+ display: block;
+ margin: 0 auto;
+ padding: 10px 32px;
+ background: #030213;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-family: system-ui;
+ cursor: pointer;
+ transition: background-color 0.15s;
+}
+
+.popup__btn:hover {
+ background: rgba(3, 2, 19, 0.85);
+}
+
+/* ── RecipeFinder Layout ───────────────────────────────────── */
+.finder-layout {
+ display: flex;
+ height: calc(100vh - 57px);
+}
+
+.finder-sidebar {
+ width: 300px;
+ flex-shrink: 0;
+ border-right: 1px solid #e5e7eb;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ overflow-y: auto;
+ background: #fff;
+}
+
+.finder-sidebar__section {
+ padding: 20px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.finder-sidebar__title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #6b7280;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0 0 12px 0;
+ font-family: system-ui;
+}
+
+.finder-sidebar__add-btn {
+ flex: 1;
+ padding: 10px;
+ background: #030213;
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 14px;
+ font-family: system-ui;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ transition: background 0.15s;
+}
+
+.finder-sidebar__add-btn:hover {
+ background: rgba(3, 2, 19, 0.85);
+}
+
+.finder-sidebar__ingredient-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-top: 12px;
+}
+
+.finder-sidebar__ingredient {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 10px;
+ background: #f3f3f5;
+ border-radius: 8px;
+ font-size: 13px;
+ font-family: system-ui;
+}
+
+.finder-sidebar__ingredient-name {
+ font-weight: 500;
+ color: #111;
+}
+
+.finder-sidebar__ingredient-amount {
+ color: #6b7280;
+ font-size: 12px;
+}
+
+.finder-sidebar__ingredient-remove {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #9ca3af;
+ display: flex;
+ align-items: center;
+ padding: 0;
+ transition: color 0.1s;
+}
+
+.finder-sidebar__ingredient-remove:hover {
+ color: #ef4444;
+}
+
+.finder-sidebar__empty {
+ font-size: 13px;
+ color: #9ca3af;
+ font-family: system-ui;
+ margin: 8px 0 0 0;
+}
+
+/* Personenanzahl */
+.finder-sidebar__persons {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.finder-sidebar__persons-btn {
+ width: 32px;
+ height: 32px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ background: #fff;
+ font-size: 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.1s;
+}
+
+.finder-sidebar__persons-btn:hover {
+ background: #f3f4f6;
+}
+
+.finder-sidebar__persons-count {
+ font-size: 22px;
+ font-weight: 600;
+ font-family: system-ui;
+ color: #111;
+ min-width: 24px;
+ text-align: center;
+}
+
+.finder-sidebar__persons-label {
+ font-size: 12px;
+ color: #6b7280;
+ font-family: system-ui;
+}
+
+/* Suchen Button */
+.finder-sidebar__search-btn {
+ width: 100%;
+ padding: 12px;
+ background: #030213;
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 600;
+ font-family: system-ui;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ transition: background 0.15s;
+}
+
+.finder-sidebar__search-btn:hover:not(:disabled) {
+ background: rgba(3, 2, 19, 0.85);
+}
+
+.finder-sidebar__search-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+/* Rezepte Bereich */
+.finder-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px;
+ background: #fafafa;
+}
+
+.finder-content__empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #9ca3af;
+ font-size: 14px;
+ font-family: system-ui;
+}
+
+.finder-content__grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 16px;
+}
+
+.finder-content__card {
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 12px;
+ padding: 16px;
+ font-family: system-ui;
+}
+
+.finder-content__card-name {
+ font-size: 15px;
+ font-weight: 600;
+ color: #111;
+ margin: 0;
+}
+
+.finder-sidebar__clear-btn {
+ flex-shrink: 0;
+ padding: 10px;
+ background: #fff;
+ color: #6b7280;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.15s, color 0.15s;
+}
+
+.finder-sidebar__clear-btn:hover {
+ background: #fef2f2;
+ color: #ef4444;
+ border-color: #ef4444;
+}
+.finder-sidebar__ingredient-right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.finder-sidebar__ingredient-amount {
+ color: #6b7280;
+ font-size: 12px;
+ cursor: pointer;
+ padding: 2px 6px;
+ border-radius: 4px;
+ transition: background 0.1s;
+}
+
+.finder-sidebar__ingredient-amount:hover {
+ background: #e5e7eb;
+ color: #111;
+}
+
+.finder-sidebar__ingredient-edit {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.finder-sidebar__edit-input {
+ width: 52px;
+ padding: 3px 6px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 12px;
+ font-family: system-ui;
+ outline: none;
+}
+
+.finder-sidebar__edit-input:focus {
+ border-color: #030213;
+}
+
+.finder-sidebar__edit-select {
+ padding: 3px 4px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 12px;
+ font-family: system-ui;
+ background: #fff;
+ outline: none;
+}
+
+.finder-sidebar__edit-select:focus {
+ border-color: #030213;
+}
+
+.finder-sidebar__edit-save {
+ background: #030213;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ padding: 3px 7px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.finder-sidebar__edit-save:hover {
+ background: rgba(3, 2, 19, 0.8);
+}
+
+.finder-sidebar__edit-cancel {
+ background: none;
+ color: #9ca3af;
+ border: 1px solid #e5e7eb;
+ border-radius: 6px;
+ padding: 3px 7px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: color 0.1s;
+}
+
+.finder-sidebar__edit-cancel:hover {
+ color: #ef4444;
+ border-color: #ef4444;
}
\ No newline at end of file
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index 03361db..cae660e 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -1,6 +1,3 @@
-// lib/auth.ts – Auth-Kontext mit Access + Refresh Token
-// ──────────────────────────────────────────────────────
-
"use client";
import {
@@ -13,7 +10,7 @@ import {
type ReactNode,
} from "react";
-const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
+const API_URL = "http://localhost:3000";
// ── Typen ─────────────────────────────────────────────────────
export interface User {
@@ -49,6 +46,7 @@ function saveTokens(accessToken: string, refreshToken: string) {
function clearTokens() {
sessionStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
+ localStorage.removeItem("ingredients");
}
// ── API-Aufrufe ───────────────────────────────────────────────
@@ -125,7 +123,7 @@ async function fetchWithAuth(url: string, options: RequestInit = {}): Promise