diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php index 00b1b00..4cf558d 100644 --- a/ai/LocalAIApi.php +++ b/ai/LocalAIApi.php @@ -48,7 +48,107 @@ class LocalAIApi $payload['model'] = $cfg['default_model']; } - return self::request($options['path'] ?? null, $payload, $options); + $resp = self::request($options['path'] ?? null, $payload, $options); + + if ($resp['success'] && isset($resp['data']['ai_request_id']) && isset($resp['data']['status']) && $resp['data']['status'] === 'queued') { + return self::awaitResponse($resp['data']['ai_request_id'], $options); + } + + return $resp; + } + + /** + * Poll the status of an asynchronous AI request. + * + * @param string|int $requestId + * @param array $options + * @return array + */ + public static function awaitResponse($requestId, array $options = []): array + { + $interval = (int) ($options['poll_interval'] ?? 5); + $timeout = (int) ($options['poll_timeout'] ?? 300); + $start = time(); + + while (time() - $start < $timeout) { + $statusResp = self::fetchStatus($requestId, $options); + if (!$statusResp['success']) { + return $statusResp; + } + + $data = $statusResp['data'] ?? []; + $status = $data['status'] ?? 'unknown'; + + if ($status === 'completed' || $status === 'success') { + return $statusResp; + } + + if ($status === 'failed' || $status === 'cancelled') { + return [ + 'success' => false, + 'error' => $status, + 'message' => $data['error'] ?? "AI request $status", + 'data' => $data + ]; + } + + sleep($interval); + } + + return [ + 'success' => false, + 'error' => 'timeout', + 'message' => 'Timed out waiting for AI response.' + ]; + } + + /** + * Fetch the status of an AI request. + * + * @param string|int $requestId + * @param array $options + * @return array + */ + public static function fetchStatus($requestId, array $options = []): array + { + $cfg = self::config(); + $projectId = $cfg['project_id']; + $path = "/projects/{$projectId}/ai-request/{$requestId}/status"; + + $ch = curl_init(self::buildUrl($path, $cfg['base_url'])); + + $headers = [ + 'Accept: application/json', + $cfg['project_header'] . ': ' . $cfg['project_uuid'] + ]; + + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $cfg['verify_tls'] ?? true); + + $responseBody = curl_exec($ch); + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if (file_exists(__DIR__ . '/../api/scan_debug.log')) { + file_put_contents(__DIR__ . '/../api/scan_debug.log', date('Y-m-d H:i:s') . " fetchStatus($requestId) status: $status, body: $responseBody" . PHP_EOL, FILE_APPEND); + } + + $decoded = json_decode($responseBody, true); + if ($status >= 200 && $status < 300) { + return [ + 'success' => true, + 'status' => $status, + 'data' => $decoded + ]; + } + + return [ + 'success' => false, + 'status' => $status, + 'error' => $decoded['error'] ?? 'Failed to fetch status' + ]; } /** @@ -134,6 +234,11 @@ class LocalAIApi } $body = json_encode($payload, JSON_UNESCAPED_UNICODE); + if (file_exists(__DIR__ . '/../api/scan_debug.log')) { + // Log a truncated version of the body if it's too large (images are large) + $logBody = strlen($body) > 1000 ? substr($body, 0, 1000) . '... [TRUNCATED]' : $body; + file_put_contents(__DIR__ . '/../api/scan_debug.log', date('Y-m-d H:i:s') . " AI Request to $url: " . $logBody . PHP_EOL, FILE_APPEND); + } if ($body === false) { return [ 'success' => false, @@ -211,6 +316,11 @@ class LocalAIApi return ''; } + // If the payload contains a 'response' object (typical for status checks), use it. + if (isset($payload['response']) && is_array($payload['response'])) { + $payload = $payload['response']; + } + if (!empty($payload['output']) && is_array($payload['output'])) { $combined = ''; foreach ($payload['output'] as $item) { diff --git a/ai/config.php b/ai/config.php index 1ba1596..da9318f 100644 --- a/ai/config.php +++ b/ai/config.php @@ -46,7 +46,7 @@ return [ 'project_id' => $projectId, 'project_uuid' => $projectUuid, 'project_header' => 'project-uuid', - 'default_model' => 'gpt-5', + 'default_model' => 'gpt-4o-mini', 'timeout' => 30, 'verify_tls' => true, ]; diff --git a/api/auth_helper.php b/api/auth_helper.php new file mode 100644 index 0000000..93d7202 --- /dev/null +++ b/api/auth_helper.php @@ -0,0 +1,23 @@ +prepare("SELECT id, email, shopping_list FROM users WHERE id = ?"); + $stmt->execute([get_logged_in_user_id()]); + $user = $stmt->fetch(); + if ($user) { + if ($user['shopping_list']) { + $user['shopping_list'] = json_decode($user['shopping_list'], true); + } + echo json_encode(['success' => true, 'logged_in' => true, 'user' => $user]); + } else { + logout_user(); + echo json_encode(['success' => true, 'logged_in' => false]); + } + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } +} else { + echo json_encode(['success' => true, 'logged_in' => false]); +} diff --git a/api/delete_recipe.php b/api/delete_recipe.php index 0e9bce4..e06bf7f 100644 --- a/api/delete_recipe.php +++ b/api/delete_recipe.php @@ -1,6 +1,6 @@ prepare("SELECT user_id FROM recipes WHERE id = ?"); + $stmt->execute([$recipeId]); + $recipe = $stmt->fetch(); + + if ($recipe && $recipe['user_id'] !== null && $recipe['user_id'] != $userId) { + echo json_encode(['success' => false, 'error' => 'Unauthorized to delete this recipe.']); + exit; + } + $stmt = $pdo->prepare("DELETE FROM recipes WHERE id = ?"); $stmt->execute([$recipeId]); diff --git a/api/get_recipes.php b/api/get_recipes.php index e1fe415..d0f51f1 100644 --- a/api/get_recipes.php +++ b/api/get_recipes.php @@ -1,14 +1,27 @@ query('SELECT * FROM recipes ORDER BY created_at DESC'); + $userId = get_logged_in_user_id(); + + if ($userId) { + $recipes_stmt = $pdo->prepare('SELECT * FROM recipes WHERE user_id = ? OR user_id IS NULL ORDER BY created_at DESC'); + $recipes_stmt->execute([$userId]); + } else { + $recipes_stmt = $pdo->query('SELECT * FROM recipes WHERE user_id IS NULL ORDER BY created_at DESC'); + } $recipes = $recipes_stmt->fetchAll(); - $ingredients_stmt = $pdo->query('SELECT * FROM ingredients'); - $all_ingredients = $ingredients_stmt->fetchAll(); + $recipeIds = array_column($recipes, 'id'); + $all_ingredients = []; + if (!empty($recipeIds)) { + $placeholders = implode(',', array_fill(0, count($recipeIds), '?')); + $ingredients_stmt = $pdo->prepare("SELECT * FROM ingredients WHERE recipe_id IN ($placeholders)"); + $ingredients_stmt->execute($recipeIds); + $all_ingredients = $ingredients_stmt->fetchAll(); + } $ingredients_by_recipe = []; foreach ($all_ingredients as $ingredient) { diff --git a/api/login.php b/api/login.php new file mode 100644 index 0000000..d2f3b6d --- /dev/null +++ b/api/login.php @@ -0,0 +1,28 @@ + false, 'error' => 'Email and password are required.']); + exit; +} + +try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + login_user($user['id']); + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false, 'error' => 'Invalid email or password.']); + } +} catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]); +} diff --git a/api/logout.php b/api/logout.php new file mode 100644 index 0000000..f4e01cd --- /dev/null +++ b/api/logout.php @@ -0,0 +1,6 @@ + true]); diff --git a/api/register.php b/api/register.php new file mode 100644 index 0000000..4082168 --- /dev/null +++ b/api/register.php @@ -0,0 +1,40 @@ + false, 'error' => 'Email and password are required.']); + exit; +} + +if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success' => false, 'error' => 'Invalid email format.']); + exit; +} + +try { + $pdo = db(); + + // Check if user already exists + $stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?"); + $stmt->execute([$email]); + if ($stmt->fetch()) { + echo json_encode(['success' => false, 'error' => 'Email already registered.']); + exit; + } + + $password_hash = password_hash($password, PASSWORD_DEFAULT); + $stmt = $pdo->prepare("INSERT INTO users (email, password_hash) VALUES (?, ?)"); + $stmt->execute([$email, $password_hash]); + + $user_id = $pdo->lastInsertId(); + login_user($user_id); + + echo json_encode(['success' => true]); +} catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]); +} diff --git a/api/save_recipe.php b/api/save_recipe.php index 422f7ca..8ec93b3 100644 --- a/api/save_recipe.php +++ b/api/save_recipe.php @@ -1,6 +1,6 @@ false, 'error' => 'Invalid input.']); exit; @@ -55,16 +57,22 @@ try { $recipeId = $data['id']; $category = !empty($data['category']) ? $data['category'] : 'No category'; + // Check if recipe belongs to user + $stmt = $pdo->prepare("SELECT user_id, image_url FROM recipes WHERE id = ?"); + $stmt->execute([$recipeId]); + $existing = $stmt->fetch(); + + if (!$existing || ($existing['user_id'] !== null && $existing['user_id'] != $userId)) { + throw new Exception('Unauthorized to update this recipe.'); + } + // Fetch existing image URL if a new one isn't uploaded if ($imageUrl === null) { - $stmt = $pdo->prepare("SELECT image_url FROM recipes WHERE id = ?"); - $stmt->execute([$recipeId]); - $existing = $stmt->fetch(); - $imageUrl = $existing ? $existing['image_url'] : null; + $imageUrl = $existing['image_url']; } - $stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ?, category = ?, image_url = ? WHERE id = ?"); - $stmt->execute([$data['name'], $data['guests'], $category, $imageUrl, $recipeId]); + $stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ?, category = ?, image_url = ?, user_id = ? WHERE id = ?"); + $stmt->execute([$data['name'], $data['guests'], $category, $imageUrl, $userId, $recipeId]); // Easiest way to handle ingredients is to delete old ones and insert new ones $stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?"); @@ -73,8 +81,8 @@ try { } else { // Insert new recipe $category = !empty($data['category']) ? $data['category'] : 'No category'; - $stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category, image_url) VALUES (?, ?, ?, ?)"); - $stmt->execute([$data['name'], $data['guests'], $category, $imageUrl]); + $stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category, image_url, user_id) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$data['name'], $data['guests'], $category, $imageUrl, $userId]); $recipeId = $pdo->lastInsertId(); } diff --git a/api/save_shopping_list.php b/api/save_shopping_list.php new file mode 100644 index 0000000..0d01b73 --- /dev/null +++ b/api/save_shopping_list.php @@ -0,0 +1,21 @@ + false, 'error' => 'Not authenticated']); + exit; +} + +try { + $data = json_decode(file_get_contents('php://input'), true); + $shoppingList = $data['shopping_list'] ?? null; + + $pdo = db(); + $stmt = $pdo->prepare("UPDATE users SET shopping_list = ? WHERE id = ?"); + $stmt->execute([json_encode($shoppingList), get_logged_in_user_id()]); + + echo json_encode(['success' => true]); +} catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/scan_debug.log b/api/scan_debug.log new file mode 100644 index 0000000..d0d6b77 --- /dev/null +++ b/api/scan_debug.log @@ -0,0 +1,20 @@ +2026-01-26 20:36:16 Processing image: type=image/jpeg, size=85908 bytes +2026-01-26 20:36:16 Data URL prefix: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD... +2026-01-26 20:36:17 AI Request to https://flatlogic.com/projects/35604/ai-request: {"model":"gpt-4o","input":[{"role":"user","content":[{"type":"text","text":"Analyze this image. If it's a photo of a dish, identify what it is and generate a possible recipe for it. If it's a photo of a written recipe, extract the information. \nExtract or generate the following information in JSON format:\n- name: The name of the dish\/recipe\n- category: One of 'Drinks', 'Breakfast', 'Dinner', 'Appetizers'\n- ingredients: An array of objects, each with:\n - name: Name of the ingredient\n - quantity: Numeric quantity (float)\n - unit: Unit of measurement (e.g., 'g', 'kg', 'ml', 'l', 'piece', 'pack')\n- guests: The default number of guests\/servings the recipe is for (integer)\n\nImportant: \n1. The quantities should be for 1 person if possible, or for the number of guests specified. If you're generating a recipe for a dish, default to 1 or 2 guests.\n2. Return ONLY the JSON object.\n3. If you can't determine something, provide a best guess or null."},{"type":"image_url","imag... [TRUNCATED] +2026-01-26 20:36:17 fetchStatus(1149) status: 200, body: {"status":"failed","error":"the server responded with status 400"} +2026-01-26 20:36:17 AI Scan Error: failed +2026-01-26 20:36:27 Processing image: type=image/jpeg, size=85908 bytes +2026-01-26 20:36:27 Data URL prefix: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD... +2026-01-26 20:36:27 AI Request to https://flatlogic.com/projects/35604/ai-request: {"model":"gpt-4o","input":[{"role":"user","content":[{"type":"text","text":"Analyze this image. If it's a photo of a dish, identify what it is and generate a possible recipe for it. If it's a photo of a written recipe, extract the information. \nExtract or generate the following information in JSON format:\n- name: The name of the dish\/recipe\n- category: One of 'Drinks', 'Breakfast', 'Dinner', 'Appetizers'\n- ingredients: An array of objects, each with:\n - name: Name of the ingredient\n - quantity: Numeric quantity (float)\n - unit: Unit of measurement (e.g., 'g', 'kg', 'ml', 'l', 'piece', 'pack')\n- guests: The default number of guests\/servings the recipe is for (integer)\n\nImportant: \n1. The quantities should be for 1 person if possible, or for the number of guests specified. If you're generating a recipe for a dish, default to 1 or 2 guests.\n2. Return ONLY the JSON object.\n3. If you can't determine something, provide a best guess or null."},{"type":"image_url","imag... [TRUNCATED] +2026-01-26 20:36:27 fetchStatus(1150) status: 200, body: {"status":"failed","error":"the server responded with status 400"} +2026-01-26 20:36:27 AI Scan Error: failed +2026-01-26 20:36:43 Processing image: type=image/jpeg, size=85908 bytes +2026-01-26 20:36:43 Data URL prefix: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD... +2026-01-26 20:36:43 AI Request to https://flatlogic.com/projects/35604/ai-request: {"model":"gpt-4o","input":[{"role":"user","content":[{"type":"text","text":"Analyze this image. If it's a photo of a dish, identify what it is and generate a possible recipe for it. If it's a photo of a written recipe, extract the information. \nExtract or generate the following information in JSON format:\n- name: The name of the dish\/recipe\n- category: One of 'Drinks', 'Breakfast', 'Dinner', 'Appetizers'\n- ingredients: An array of objects, each with:\n - name: Name of the ingredient\n - quantity: Numeric quantity (float)\n - unit: Unit of measurement (e.g., 'g', 'kg', 'ml', 'l', 'piece', 'pack')\n- guests: The default number of guests\/servings the recipe is for (integer)\n\nImportant: \n1. The quantities should be for 1 person if possible, or for the number of guests specified. If you're generating a recipe for a dish, default to 1 or 2 guests.\n2. Return ONLY the JSON object.\n3. If you can't determine something, provide a best guess or null."},{"type":"image_url","imag... [TRUNCATED] +2026-01-26 20:36:43 fetchStatus(1151) status: 200, body: {"status":"failed","error":"the server responded with status 400"} +2026-01-26 20:36:43 AI Scan Error: failed +2026-01-26 20:37:58 Processing image: type=image/jpeg, size=85908 bytes +2026-01-26 20:37:58 Data URL prefix: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD... +2026-01-26 20:37:58 AI Request to https://flatlogic.com/projects/35604/ai-request: {"model":"gpt-4o","input":[{"role":"user","content":[{"type":"text","text":"Analyze this image. If it's a photo of a dish, identify what it is and generate a possible recipe for it. If it's a photo of a written recipe, extract the information. \nExtract or generate the following information in JSON format:\n- name: The name of the dish\/recipe\n- category: One of 'Drinks', 'Breakfast', 'Dinner', 'Appetizers'\n- ingredients: An array of objects, each with:\n - name: Name of the ingredient\n - quantity: Numeric quantity (float)\n - unit: Unit of measurement (e.g., 'g', 'kg', 'ml', 'l', 'piece', 'pack')\n- guests: The default number of guests\/servings the recipe is for (integer)\n\nImportant: \n1. The quantities should be for 1 person if possible, or for the number of guests specified. If you're generating a recipe for a dish, default to 1 or 2 guests.\n2. Return ONLY the JSON object.\n3. If you can't determine something, provide a best guess or null."},{"type":"image_url","imag... [TRUNCATED] +2026-01-26 20:37:58 fetchStatus(1152) status: 200, body: {"status":"failed","error":"the server responded with status 400"} +2026-01-26 20:37:58 AI Scan Error: failed diff --git a/api/scan_recipe.php b/api/scan_recipe.php index 8ec6b67..8716cca 100644 --- a/api/scan_recipe.php +++ b/api/scan_recipe.php @@ -13,9 +13,13 @@ if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) { } $imagePath = $_FILES['image']['tmp_name']; +$imageSize = filesize($imagePath); $imageData = base64_encode(file_get_contents($imagePath)); $imageType = $_FILES['image']['type']; +file_put_contents(__DIR__ . '/scan_debug.log', date('Y-m-d H:i:s') . " Processing image: type=$imageType, size=$imageSize bytes" . PHP_EOL, FILE_APPEND); +file_put_contents(__DIR__ . '/scan_debug.log', date('Y-m-d H:i:s') . " Data URL prefix: " . substr("data:$imageType;base64,$imageData", 0, 50) . "..." . PHP_EOL, FILE_APPEND); + $prompt = "Analyze this image. If it's a photo of a dish, identify what it is and generate a possible recipe for it. If it's a photo of a written recipe, extract the information. Extract or generate the following information in JSON format: - name: The name of the dish/recipe @@ -33,6 +37,7 @@ Important: try { $response = LocalAIApi::createResponse([ + 'model' => 'gpt-4o', 'input' => [ [ 'role' => 'user', @@ -44,7 +49,8 @@ try { [ 'type' => 'image_url', 'image_url' => [ - 'url' => "data:$imageType;base64,$imageData" + 'url' => "data:$imageType;base64,$imageData", + 'detail' => 'auto' ] ] ] @@ -53,13 +59,18 @@ try { ]); if (!$response['success']) { + file_put_contents(__DIR__ . '/scan_debug.log', date('Y-m-d H:i:s') . ' AI Scan Error: ' . ($response['error'] ?? 'AI request failed') . PHP_EOL, FILE_APPEND); throw new Exception($response['error'] ?? 'AI request failed'); } + file_put_contents(__DIR__ . '/scan_debug.log', date('Y-m-d H:i:s') . ' AI Full Response: ' . json_encode($response) . PHP_EOL, FILE_APPEND); + $text = LocalAIApi::extractText($response); + file_put_contents(__DIR__ . '/scan_debug.log', date('Y-m-d H:i:s') . ' AI Scan Raw Text: ' . $text . PHP_EOL, FILE_APPEND); $recipeData = LocalAIApi::decodeJsonFromResponse($response); if (!$recipeData) { - throw new Exception('Failed to parse AI response as JSON. Raw response: ' . LocalAIApi::extractText($response)); + file_put_contents(__DIR__ . '/scan_debug.log', date('Y-m-d H:i:s') . ' AI Scan JSON Parse Error. Raw: ' . $text . PHP_EOL, FILE_APPEND); + throw new Exception('Failed to parse AI response as JSON. Please try again with a clearer image.'); } echo json_encode(['success' => true, 'data' => $recipeData]); diff --git a/api/test_ai.php b/api/test_ai.php new file mode 100644 index 0000000..b140dcd --- /dev/null +++ b/api/test_ai.php @@ -0,0 +1,15 @@ + [ + ['role' => 'user', 'content' => 'Hello, tell me a short joke.'] + ] + ]); + + echo json_encode($response); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/css/custom.css b/assets/css/custom.css index 0216418..b052b9b 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,573 +1,397 @@ -/* Christmas Decorations: Fixed Positioning */ /* General Body Styles */ body { - background-color: #142E35; /* Dark green background */ - color: #ffffff; /* White text */ + background-color: #FAF9F6; /* Light beige background */ + color: #2D2D2D; /* Dark gray text */ font-family: 'Poppins', sans-serif; } -/* Garland */ -body::before { - content: ''; - position: fixed; - top: 10px; - left: -5%; - width: 110%; - height: 20px; - background: - radial-gradient(circle, #de4950 4px, transparent 5px), - radial-gradient(circle, #142E35 4px, transparent 5px), - radial-gradient(circle, #7accb8 4px, transparent 5px), - radial-gradient(circle, #edd46e 4px, transparent 5px), - radial-gradient(circle, #6e98ed 4px, transparent 5px); - background-size: 100px 20px; - background-position: 0 0, 20px 0, 40px 0, 60px 0, 80px 0; - background-repeat: repeat-x; - z-index: 1031; - animation: garland-animation 1.5s infinite; -} - -@keyframes garland-animation { - 50% { - filter: brightness(0.7); - } -} - -#christmas-decorations-right { - position: fixed; - top: 0px; - right: 250px; - width: 220px; - z-index: 1033; -} - -#christmas-decorations-right img { - width: 100%; -} - -#christmas-decorations-left { - position: fixed; - top: 0px; - left: 250px; - width: 220px; - z-index: 1033; - transform: scaleX(-1); -} - -#christmas-decorations-left img { - width: 100%; -} - /* Headings */ h1, h2, h3, h4, h5, h6 { - color: #ffffff !important; /* Use !important to override other styles if necessary */ + color: #2D2D2D !important; + font-weight: 700; } /* Buttons and Inputs */ input, -button { - border-radius: 16px; +button, +.form-control, +.form-select { + border-radius: 12px; } .btn { - border-radius: 50px !important; /* Fully rounded corners */ -} - -/* Navbar */ -.navbar { - background-color: rgba(20, 46, 53, 0.8) !important; /* Semi-transparent dark green */ - backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.navbar-brand { - color: #ffffff !important; + border-radius: 50px !important; font-weight: 600; + padding: 10px 24px; + transition: all 0.3s ease; } /* Main Content */ .display-4 { font-weight: 700; - color: #ffffff; + color: #2D2D2D; } .lead { - color: #ffffff; + color: #666666; } /* Cards */ .card { - background-color: rgba(255, 255, 255, 0.05); + background-color: #ffffff; border: none; - border-radius: 15px; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); + transition: transform 0.3s ease, box-shadow 0.3s ease; } .card-body { - color: #ffffff; + color: #2D2D2D; } /* Forms */ .form-label { - color: #ffffff; + color: #2D2D2D; + font-weight: 600; + margin-bottom: 0.5rem; } -.form-control { - background-color: rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #ffffff; +.form-control, +.form-select { + background-color: #ffffff; + border: 2px solid #EEEEEE; + color: #2D2D2D; + padding: 12px 16px; } -.form-control:focus { - background-color: rgba(0, 0, 0, 0.3); - border-color: #0a1a1f; /* Dark green accent */ - box-shadow: 0 0 0 0.25rem rgba(10, 26, 31, 0.25); - color: #ffffff; +.form-control:focus, +.form-select:focus { + background-color: #ffffff; + border-color: #FF7F50; /* Orange accent */ + box-shadow: 0 0 0 0.25rem rgba(255, 127, 80, 0.15); + color: #2D2D2D; } .form-control::placeholder { - color: rgba(255, 255, 255, 0.5); + color: #AAAAAA; } /* Buttons */ -.btn-primary, .btn-danger { - background-color: #de4950 !important; /* Coral red */ - border-color: #de4950 !important; - font-weight: 600; - padding: 12px 30px; - transition: all 0.3s ease; -} - -.btn-primary:hover, .btn-danger:hover { - background-color: #a02929 !important; - border-color: #a02929 !important; - transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(222, 73, 80, 0.2); -} - -.btn-outline-secondary { - border-color: rgba(255, 255, 255, 0.8); - color: #ffffff; - font-weight: 600; - padding: 12px 30px; - transition: all 0.3s ease; -} - -.btn-outline-secondary:hover { - background-color: rgba(255, 255, 255, 0.1); - color: #ffffff; - border-color: #ffffff; -} - -.btn-secondary { - background-color: rgba(255, 255, 255, 0.15); - border: none; - color: #ffffff; - padding: 12px 30px; -} - -/* Shopping List & Recipe Cards */ -#shopping-list-container .text-muted, -#recipe-cards-container .text-muted { +.btn-primary { + background-color: #FF7F50 !important; /* Orange */ + border-color: #FF7F50 !important; color: #ffffff !important; } -.recipe-card { - background-color: rgba(255, 255, 255, 0.1); - border-radius: 10px; - padding: 15px; - margin-bottom: 15px; +.btn-primary:hover { + background-color: #E66E45 !important; + border-color: #E66E45 !important; + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(255, 127, 80, 0.3); } -/* 3-Column Layout Adjustments */ -.row.g-4 > [class*='col-'] .card { - height: 100%; /* Make cards in columns equal height */ +.btn-danger { + background-color: #FF4B2B !important; + border-color: #FF4B2B !important; + color: #ffffff !important; +} + +.btn-outline-secondary { + border: 2px solid #EEEEEE; + color: #666666; +} + +.btn-outline-secondary:hover { + background-color: #F8F8F8; + color: #2D2D2D; + border-color: #DDDDDD; +} + +.btn-secondary { + background-color: #EEEEEE; + border: none; + color: #666666; +} + +.btn-secondary:hover { + background-color: #DDDDDD; + color: #2D2D2D; +} + +/* Shopping List & Recipe Cards */ +.recipe-card { + background-color: #ffffff; + border-radius: 20px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid #F0F0F0; } #recipe-cards-container, #shopping-list-container { - max-height: 60vh; /* Adjust as needed */ + max-height: 70vh; overflow-y: auto; padding: 10px; } -/* Custom scrollbar for webkit browsers */ +/* Custom scrollbar */ ::-webkit-scrollbar { - width: 8px; + width: 6px; } ::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; + background: transparent; } ::-webkit-scrollbar-thumb { - background: #0a1a1f; /* Even darker green */ - border-radius: 4px; + background: #DDDDDD; + border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { - background: #10242B; /* Slightly lighter than thumb */ + background: #CCCCCC; } /* Footer */ -footer.bg-light { - background-color: transparent !important; - border-top: 1px solid rgba(255, 255, 255, 0.1); - color: #ffffff; -} - -/* Snow Effect */ -#snow-container { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1032; - overflow: hidden; -} - -.snowflake { - position: absolute; - top: -20px; - background: #fff; - border-radius: 50%; - opacity: 0.8; - pointer-events: none; - animation: fall linear infinite; -} - -@keyframes fall { - to { - transform: translateY(105vh); - opacity: 0; - } +footer { + border-top: 1px solid #EEEEEE; + color: #999999; + font-size: 0.9rem; } /* Shopping List Checkbox */ +.list-group-item { + background-color: transparent !important; + border-bottom: 1px solid #F0F0F0 !important; + padding: 15px 0 !important; + color: #2D2D2D !important; +} + .list-group-item.checked .form-check-label { text-decoration: line-through; - opacity: 0.6; + opacity: 0.5; } -.unit-btn { - padding: 0.375rem 0.5rem; - font-size: 0.875rem; -} - -/* Adjust padding for the remove button in the ingredient row */ -.ingredient-row .remove-ingredient { - padding: 0.375rem 0.75rem; -} - -/* Custom Shopping List Styles */ -.col-md-4:has(#shopping-list-container) .card { - background-color: transparent; - box-shadow: none !important; - border: 1px solid rgba(255, 255, 255, 0.1); -} - -#shopping-list-container .list-group-item { - background-color: transparent; - color: #ffffff; - border-color: rgba(255, 255, 255, 0.1) !important; -} - -/* Custom Checkbox Styles */ -.form-.form-check-input { - background-color: transparent; - border: 1px solid #ffffff; +.form-check-input { + width: 1.25em; + height: 1.25em; + border: 2px solid #DDDDDD; + cursor: pointer; } .form-check-input:checked { - background-color: #de4950; - border-color: #de4950; + background-color: #FF7F50; + border-color: #FF7F50; } -.form-check-input:focus { - border-color: #0a1a1f; - box-shadow: 0 0 0 0.25rem rgba(10, 26, 31, 0.25); +/* Unit buttons */ +.unit-selector .btn { + padding: 6px 12px !important; + font-size: 0.85rem; } -@media print { - body * { - visibility: hidden; - } - #shopping-list-container, #shopping-list-container * { - visibility: visible; - } - #shopping-list-container { - position: absolute; - left: 0; - top: 0; - width: 100%; - } - .card { - border: none !important; - box-shadow: none !important; - } - .list-group-item { - color: #000 !important; - background-color: #fff !important; - } - .badge { - color: #000 !important; - background-color: #fff !important; - border: 1px solid #ccc; - } - .form-check-input { - border: 1px solid #000 !important; - } - h2, h3 { - color: #000 !important; - } +.unit-selector .btn-secondary { + background-color: #FF7F50 !important; + color: white !important; } +/* Badges */ .bg-custom-green { - background-color: #142E35 !important; + background-color: #FFF0EB !important; + color: #FF7F50 !important; + font-weight: 700; + border: 1px solid #FFE4DB; } -/* Modal Styles */ -#recipe-form-modal .modal-content, #add-product-modal .modal-content, #confirmRemoveModal .modal-content, #view-recipe-modal .modal-content { - background-color: #142E35; - color: white; -} - -#recipe-form-modal .modal-header, -#add-product-modal .modal-header, -#confirmRemoveModal .modal-header, -#view-recipe-modal .modal-header { - border-bottom: 1px solid rgba(255, 255, 255, 0.2); -} - -#recipe-form-modal .modal-footer, -#add-product-modal .modal-footer, -#confirmRemoveModal .modal-footer, -#view-recipe-modal .modal-footer { - border-top: 1px solid rgba(255, 255, 255, 0.2); -} - -#recipe-form-modal .btn-close, -#add-product-modal .btn-close, -#confirmRemoveModal .btn-close, -#view-recipe-modal .btn-close { - filter: invert(1); -} - -/* Category Filter Button Styles */ -#category-filters .btn { - border-radius: 20px; /* Rounded corners */ - padding: 6px 12px; - font-size: 0.8rem; - font-weight: 600; - line-height: 1.5; - border: 1px solid rgba(255, 255, 255, 0.2); - background-color: rgba(255, 255, 255, 0.1); - color: white; - transition: background-color 0.2s ease; -} - -#category-filters .btn:hover { - background-color: rgba(255, 255, 255, 0.2); -} - -#category-filters .btn.active { - background-color: #de4950; /* Coral red for active */ - border-color: #de4950; - color: white; -} - -/* Recipe Card Category Label */ -.recipe-category-label { - background-color: #142E35; - color: white; - padding: 5px 10px; - border-radius: 5px; - font-size: 0.8em; - font-weight: 600; - margin-left: 10px; /* Add some space between title and label */ - white-space: nowrap; /* Prevent the label itself from wrapping */ - align-self: flex-start; /* Align to the top of the flex container */ -} - -/* Shopping List Quantity Controls */ -.btn.btn-quantity-modifier { - width: 32px; - height: 32px; - border-radius: 50%; - padding: 0; - font-size: 1.2rem; - font-weight: bold; - line-height: 1; +/* Quantity Modifiers */ +.btn-quantity-modifier { + width: 28px; + height: 28px; + border-radius: 8px !important; display: inline-flex; align-items: center; justify-content: center; - border: 1px solid rgba(255, 255, 255, 0.2); - outline: none; - box-shadow: none; + background-color: #F8F8F8; + border: 1px solid #EEEEEE; + color: #666666; + padding: 0 !important; +} + +.btn-quantity-modifier:hover { + background-color: #FF7F50; color: white; - background-color: rgba(255, 255, 255, 0.1); - transition: background-color 0.2s ease; + border-color: #FF7F50; } -.btn.btn-quantity-modifier:hover { - background-color: rgba(255, 255, 255, 0.2); +/* Modal Styles */ +.modal-content { + background-color: #ffffff; + border: none; + border-radius: 24px; + overflow: hidden; } -.quantity-controls .quantity { - margin: 0 10px; +.modal-header { + border-bottom: 1px solid #F0F0F0; + padding: 24px; } -/* Make delete recipe button same height as edit button */ -.card .btn.delete-recipe { - padding: .25rem .5rem; - margin-left: 8px; +.modal-footer { + border-top: 1px solid #F0F0F0; + padding: 20px 24px; +} + +/* Category Label */ +.recipe-category-label { + background-color: #FFF0EB; + color: #FF7F50; + padding: 4px 12px; + border-radius: 8px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Category Filters */ +#category-filters .btn { + border: 2px solid #EEEEEE; + background-color: #ffffff; + color: #666666; + font-size: 0.85rem; + padding: 8px 16px !important; +} + +#category-filters .btn.active { + background-color: #FF7F50 !important; + border-color: #FF7F50 !important; + color: #ffffff !important; +} + +/* Search Container */ +#recipe-search { + padding-left: 3rem; + height: 54px; + border-color: #EEEEEE; +} + +.search-container .bi-search { + left: 1.25rem; + color: #AAAAAA; + font-size: 1.1rem; } /* Recipe Card Image */ .card .card-img-top { - height: 200px; + height: 180px; object-fit: cover; + border-top-left-radius: 20px; + border-top-right-radius: 20px; } - - -/* Recipe Card Animation */ -.recipe-card-enter { - animation: fade-in 0.5s ease-in-out; -} - -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@media (max-width: 768px) { - #christmas-decorations-right, - #christmas-decorations-left { - display: none; - } - - body { - font-size: 14px; - } - - .display-4 { - font-size: 2.5rem; - } - - .btn { - padding: 10px 20px; - font-size: 0.9rem; - } - - h1, .h1 { font-size: 2rem; } - h2, .h2 { font-size: 1.5rem; } - h3, .h3 { font-size: 1.5rem; } - - .card .btn.edit-recipe { - padding-top: .25rem; - padding-bottom: .25rem; - } - - #print-shopping-list-btn { - font-size: 0; /* Hide the text */ - padding: .25rem .5rem; /* Match other small buttons */ - } - - #print-shopping-list-btn .bi-printer { - font-size: 1rem; /* Restore icon size */ - } - - /* Make Add Recipe and Add Product buttons same height as Print button on mobile */ - #add-product-btn, - .col-md-6:first-child .btn-primary { - padding-top: .375rem; - padding-bottom: .375rem; - } - - /* Allow unit selector buttons to wrap in modals */ - .unit-selector { - flex-wrap: wrap; - gap: 0.5rem; - } - - /* Stack form elements in modals on mobile */ - #add-product-modal .row .col, - #recipe-form-modal .row .col { - flex: 1 0 100%; /* Make columns full width */ - } - - /* Make unit selector more compact on mobile */ - .unit-selector { - justify-content: flex-start; - gap: 0.25rem; /* Reduce gap between buttons */ - } - - .unit-selector .unit-btn { - padding: 0.25rem 0.5rem; /* Reduce padding */ - font-size: 0.8rem; /* Slightly smaller font */ - } -} - -/* Search Input with Icon */ -.search-container { - position: relative; -} - -@media (min-width: 769px) { - .btn-primary, - #print-shopping-list-btn { - padding-top: 8px; - padding-bottom: 8px; - } - - .btn.edit-recipe { - padding-left: 16px; - padding-right: 16px; - } -} - -.search-container .bi-search { - position: absolute; - top: 50%; - left: 1rem; - transform: translateY(-50%); - z-index: 2; - color: rgba(255, 255, 255, 0.5); -} - -#recipe-search { - padding-left: 2.5rem; - border-radius: 50px; -} - -/* Unit selector button styling */ -.unit-selector .btn.btn-outline-secondary { - border-color: transparent; - opacity: 0.7; -} - -.card-title { - white-space: normal; - word-wrap: break-word; -} - +/* View Recipe Modal */ #view-recipe-ingredients { - background-color: rgba(0, 0, 0, 0.2); /* Darker shade */ - padding: 15px; - border-radius: 10px; - margin-top: 10px; + background-color: #F9F9F9; + border-radius: 16px; + padding: 20px; } #view-recipe-ingredients .list-group-item { - background-color: transparent; - border: none; - color: #fff; + border-bottom: 1px solid #EEEEEE !important; + padding: 10px 0 !important; } + +#view-recipe-ingredients .list-group-item:last-child { + border-bottom: none !important; +} + +/* Auth Screen Styles */ +.auth-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + z-index: 1050; + background-color: #ffffff; +} + +.auth-image-container { + background-image: url('../images/auth-bg.jpg'); + background-size: cover; + background-position: center; + position: relative; +} + +.auth-image-overlay { + background: radial-gradient(circle at center, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.6) 100%); + width: 100%; + height: 100%; +} + +.auth-branding-content { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 30px; + padding: 60px 40px; + max-width: 600px; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2); +} + +.auth-branding-content p.lead { + color: rgba(255, 255, 255, 0.9) !important; +} + +.auth-form-container { + box-shadow: -10px 0 30px rgba(0, 0, 0, 0.05); + background-color: #ffffff; +} + +.auth-form-container h2 { + color: #2D2D2D; + font-weight: 700; + letter-spacing: -1px; +} + +.auth-screen .form-control-lg { + padding: 14px 20px; + font-size: 1rem; + border-radius: 14px; + border: 2px solid #F5F5F5; + background-color: #FAFAFA; +} + +.auth-screen .form-control-lg:focus { + background-color: #ffffff; + border-color: #FF7F50; +} + +#auth-nav.guest-nav { + display: none !important; +} + +body:has(#guest-view:not(.d-none)) .navbar { + display: none !important; +} + +/* Footer hidden on auth screen */ +body:has(#guest-view:not(.d-none)) footer { + display: none !important; +} + +@media (max-width: 991.98px) { + .auth-form-container { + padding: 40px 20px !important; + } +} + +/* Print Styles */ +@media print { + body { background-color: white; } + .card { box-shadow: none; border: 1px solid #EEE; } +} + +@media (max-width: 768px) { + .display-4 { font-size: 2rem; } +} \ No newline at end of file diff --git a/assets/images/auth-bg.jpg b/assets/images/auth-bg.jpg new file mode 100644 index 0000000..c6a6fae Binary files /dev/null and b/assets/images/auth-bg.jpg differ diff --git a/assets/js/main.js b/assets/js/main.js index 6a1dfee..02cd044 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -4,9 +4,107 @@ const app = { recipes: [], confirmedRecipeProducts: [], checkedItems: [], - additionalProducts: [] + additionalProducts: [], + user: null }, api: { + async checkAuth() { + try { + const response = await fetch('api/check_auth.php'); + const data = await response.json(); + if (data.success && data.logged_in) { + app.state.user = data.user; + if (data.user.shopping_list) { + app.state.checkedItems = data.user.shopping_list.checkedItems || []; + app.state.additionalProducts = data.user.shopping_list.additionalProducts || []; + app.state.confirmedRecipeProducts = data.user.shopping_list.confirmedRecipeProducts || []; + } + } else { + app.state.user = null; + } + } catch (error) { + console.error('Auth check failed:', error); + } + }, + async saveShoppingList() { + if (!app.state.user) return; + try { + await fetch('api/save_shopping_list.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + shopping_list: { + checkedItems: app.state.checkedItems, + additionalProducts: app.state.additionalProducts, + confirmedRecipeProducts: app.state.confirmedRecipeProducts + } + }) + }); + } catch (error) { + console.error('Failed to save shopping list:', error); + } + }, + async login(email, password) { + try { + const response = await fetch('api/login.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + const data = await response.json(); + if (data.success) { + await app.api.checkAuth(); + await app.api.getRecipes(); + app.ui.updateAuthNav(); + app.ui.renderRecipeCards(app.state.recipes); + app.ui.updateShoppingList(); + return { success: true }; + } + return data; + } catch (error) { + return { success: false, error: error.message }; + } + }, + async register(email, password) { + try { + const response = await fetch('api/register.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + const data = await response.json(); + if (data.success) { + await app.api.checkAuth(); + await app.api.getRecipes(); + app.ui.updateAuthNav(); + app.ui.renderRecipeCards(app.state.recipes); + app.ui.updateShoppingList(); + return { success: true }; + } + return data; + } catch (error) { + return { success: false, error: error.message }; + } + }, + async logout() { + try { + const response = await fetch('api/logout.php'); + const data = await response.json(); + if (data.success) { + app.state.user = null; + app.state.recipes = []; + app.state.checkedItems = []; + app.state.additionalProducts = []; + app.state.confirmedRecipeProducts = []; + await app.api.getRecipes(); + app.ui.updateAuthNav(); + app.ui.renderRecipeCards(app.state.recipes); + app.ui.updateShoppingList(); + } + } catch (error) { + console.error('Logout failed:', error); + } + }, async getRecipes() { try { const response = await fetch('api/get_recipes.php'); @@ -99,20 +197,7 @@ const app = { const text = document.createElement('p'); text.className = 'card-text text-muted'; - text.textContent = `Serves ${recipe.guests} | ${recipe.ingredients.length} ingredients`; - - const guestPortionControls = document.createElement('div'); - guestPortionControls.className = 'row g-2 mb-3'; - guestPortionControls.innerHTML = ` -
- - -
-
- - -
- `; + text.textContent = `${recipe.ingredients.length} ingredients`; const buttonGroup = document.createElement('div'); buttonGroup.className = 'mt-auto pt-2'; @@ -124,7 +209,6 @@ const app = { cardBody.appendChild(titleWrapper); cardBody.appendChild(text); - cardBody.appendChild(guestPortionControls); cardBody.appendChild(buttonGroup); card.appendChild(cardBody); cardCol.appendChild(card); @@ -398,38 +482,98 @@ const app = { } return null; }, - createSnowflakes() { - const snowContainer = document.getElementById('snow-container'); - if (!snowContainer) return; - snowContainer.innerHTML = ''; - const numberOfSnowflakes = 50; - for (let i = 0; i < numberOfSnowflakes; i++) { - const snowflake = document.createElement('div'); - snowflake.className = 'snowflake'; - const size = Math.random() * 4 + 2; - snowflake.style.width = `${size}px`; - snowflake.style.height = `${size}px`; - snowflake.style.left = Math.random() * 100 + 'vw'; - const animationDuration = Math.random() * 5 + 5; - snowflake.style.animationDuration = `${animationDuration}s`; - const animationDelay = Math.random() * 5; - snowflake.style.animationDelay = `${animationDelay}s`; - snowflake.style.opacity = Math.random() * 0.7 + 0.3; - snowContainer.appendChild(snowflake); - } - }, loadCheckedItems() { - const checkedItems = localStorage.getItem('checkedItems'); - if (checkedItems) { - app.state.checkedItems = JSON.parse(checkedItems); + const stored = localStorage.getItem('checkedItems'); + if (stored) { + app.state.checkedItems = JSON.parse(stored); } }, saveCheckedItems() { localStorage.setItem('checkedItems', JSON.stringify(app.state.checkedItems)); + app.api.saveShoppingList(); + }, + updateAuthNav() { + const nav = document.getElementById('auth-nav'); + const guestView = document.getElementById('guest-view'); + const appView = document.getElementById('app-view'); + if (!nav) return; + + if (app.state.user) { + nav.innerHTML = ` + + + `; + if (guestView) guestView.classList.add('d-none'); + if (appView) appView.classList.remove('d-none'); + } else { + nav.innerHTML = ''; // Landing page handles login/register + if (guestView) guestView.classList.remove('d-none'); + if (appView) appView.classList.add('d-none'); + } } }, events: { attachEventListeners() { + // Auth events (Landing Page) + const loginFormLanding = document.getElementById('login-form-landing'); + if (loginFormLanding) { + loginFormLanding.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = loginFormLanding.querySelector('[name="email"]').value; + const password = loginFormLanding.querySelector('[name="password"]').value; + const result = await app.api.login(email, password); + if (result.success) { + loginFormLanding.reset(); + } else { + alert(result.error); + } + }); + } + + const registerFormLanding = document.getElementById('register-form-landing'); + if (registerFormLanding) { + registerFormLanding.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = registerFormLanding.querySelector('[name="email"]').value; + const password = registerFormLanding.querySelector('[name="password"]').value; + const result = await app.api.register(email, password); + if (result.success) { + registerFormLanding.reset(); + } else { + alert(result.error); + } + }); + } + + // Toggle login/register on landing + const showRegister = document.getElementById('show-register'); + const showLogin = document.getElementById('show-login'); + const loginContainer = document.getElementById('login-container'); + const registerContainer = document.getElementById('register-container'); + + if (showRegister && showLogin) { + showRegister.addEventListener('click', (e) => { + e.preventDefault(); + loginContainer.classList.add('d-none'); + registerContainer.classList.remove('d-none'); + }); + showLogin.addEventListener('click', (e) => { + e.preventDefault(); + registerContainer.classList.add('d-none'); + loginContainer.classList.remove('d-none'); + }); + } + + document.getElementById('auth-nav').addEventListener('click', (e) => { + if (e.target.id === 'logout-btn') { + app.api.logout(); + } + }); + app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow()); app.dom.aiScanBtn.addEventListener('click', async function() { @@ -608,6 +752,7 @@ const app = { productToModify.quantity++; app.ui.updateShoppingList(); + app.api.saveShoppingList(); } else if (e.target.matches('.decrement-item')) { const key = e.target.dataset.key; @@ -647,6 +792,7 @@ const app = { if (app.state.confirmedRecipeProducts.includes(confirmationKey)) { productToModify.quantity--; app.ui.updateShoppingList(); + app.api.saveShoppingList(); } else { document.getElementById('modal-recipe-name').textContent = recipeNameForModal; document.getElementById('modal-ingredient-name').textContent = productToModify.name; @@ -658,6 +804,7 @@ const app = { app.state.confirmedRecipeProducts.push(confirmationKey); productToModify.quantity--; app.ui.updateShoppingList(); + app.api.saveShoppingList(); confirmModal.hide(); }; } @@ -665,6 +812,7 @@ const app = { // It's not a recipe ingredient about to be removed, but has been added via '+' productToModify.quantity--; app.ui.updateShoppingList(); + app.api.saveShoppingList(); } // If not a recipe ingredient and quantity is 0, do nothing. } @@ -756,6 +904,7 @@ const app = { } app.ui.updateShoppingList(); + app.api.saveShoppingList(); // Reset form app.dom.productNameInput.value = ''; @@ -776,16 +925,6 @@ const app = { app.dom.addProductModal.hide(); }); - app.dom.musicToggle.addEventListener('click', function() { - if (app.dom.christmasMusic.paused) { - app.dom.christmasMusic.play(); - app.dom.musicToggle.innerHTML = ''; - } else { - app.dom.christmasMusic.pause(); - app.dom.musicToggle.innerHTML = ''; - } - }); - document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () { if (!app.dom.recipeIdInput.value) { app.ui.clearForm(); @@ -821,20 +960,19 @@ const app = { productQuantityInput: document.getElementById('productQuantity'), productCategoryWrapper: document.getElementById('product-category-wrapper'), productCategory: document.getElementById('productCategory'), - christmasMusic: document.getElementById('christmas-music'), - musicToggle: document.getElementById('music-toggle'), - }; - app.ui.createSnowflakes(); app.ui.loadCheckedItems(); app.events.attachEventListeners(); app.dom.cancelEditBtn.style.display = 'none'; app.ui.addIngredientRow(); - app.api.getRecipes().then(() => { - app.ui.renderRecipeCards(app.state.recipes); - app.ui.updateShoppingList(); + app.api.checkAuth().then(() => { + app.ui.updateAuthNav(); + app.api.getRecipes().then(() => { + app.ui.renderRecipeCards(app.state.recipes); + app.ui.updateShoppingList(); + }); }); } }; diff --git a/db/migrations/005_create_users_table.sql b/db/migrations/005_create_users_table.sql new file mode 100644 index 0000000..91d1818 --- /dev/null +++ b/db/migrations/005_create_users_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/migrations/006_add_user_id_to_recipes.sql b/db/migrations/006_add_user_id_to_recipes.sql new file mode 100644 index 0000000..8b766a3 --- /dev/null +++ b/db/migrations/006_add_user_id_to_recipes.sql @@ -0,0 +1,2 @@ +ALTER TABLE recipes ADD COLUMN user_id INT DEFAULT NULL; +ALTER TABLE recipes ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; \ No newline at end of file diff --git a/db/migrations/007_add_shopping_list_to_users.sql b/db/migrations/007_add_shopping_list_to_users.sql new file mode 100644 index 0000000..f98ecde --- /dev/null +++ b/db/migrations/007_add_shopping_list_to_users.sql @@ -0,0 +1,2 @@ +-- Add shopping_list column to users table to store checked items and additional products +ALTER TABLE users ADD COLUMN shopping_list LONGTEXT DEFAULT NULL; diff --git a/index.php b/index.php index c2a3de9..641c393 100644 --- a/index.php +++ b/index.php @@ -5,20 +5,20 @@ - Christmas Recipe Calculator - - + Smart Recipe & Shopping List + + - - + + - - + + @@ -27,74 +27,157 @@ - + -
- Christmas Decorations -
-
- Christmas Decorations -
- -
- -
-
-

Hey, it's Christmas time!

-

Let's get your holiday recipes sorted.

-
+ -
- -
-
-

All Recipes

- +
+
+
+ +
+
+
+ +

SmartRecipe

+

Manage your recipes and generate smart shopping lists. Identify dishes with AI and calculate ingredients effortlessly.

+
+
-
- - -
-
- - - - - - -
-
-
-

Your saved recipes will appear here.

+ +
+
+ + +
+
+

Welcome back

+

Enter your credentials to access your recipes.

+
+
+
+ + +
+
+ + +
+
+ +
+
+

+ Don't have an account? Create account +

+
+ + +
+
+

Create Account

+

Start your smart cooking journey today.

+
+
+
+ + +
+
+ + +
+
+ +
+
+

+ Already have an account? Login here +

+
+
+
+
- -
-
-

Shopping List

-
- - +
+
+
+

My Recipe Book

+

Plan your meals and get your shopping lists sorted.

+
+ +
+ +
+
+

All Recipes

+ +
+
+ + +
+
+ + + + + + +
+
+
+

Your saved recipes will appear here.

+
-
-
-
-

Your calculated list will appear here.

+ + +
+
+

Shopping List

+
+ + +
+
+
+
+
+

Your calculated list will appear here.

+
-
-
+
+