26.01.26
This commit is contained in:
parent
05f4c9c979
commit
d7b90335da
@ -48,7 +48,107 @@ class LocalAIApi
|
|||||||
$payload['model'] = $cfg['default_model'];
|
$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<string,mixed> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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);
|
$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) {
|
if ($body === false) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -211,6 +316,11 @@ class LocalAIApi
|
|||||||
return '';
|
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'])) {
|
if (!empty($payload['output']) && is_array($payload['output'])) {
|
||||||
$combined = '';
|
$combined = '';
|
||||||
foreach ($payload['output'] as $item) {
|
foreach ($payload['output'] as $item) {
|
||||||
|
|||||||
@ -46,7 +46,7 @@ return [
|
|||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
'project_uuid' => $projectUuid,
|
'project_uuid' => $projectUuid,
|
||||||
'project_header' => 'project-uuid',
|
'project_header' => 'project-uuid',
|
||||||
'default_model' => 'gpt-5',
|
'default_model' => 'gpt-4o-mini',
|
||||||
'timeout' => 30,
|
'timeout' => 30,
|
||||||
'verify_tls' => true,
|
'verify_tls' => true,
|
||||||
];
|
];
|
||||||
|
|||||||
23
api/auth_helper.php
Normal file
23
api/auth_helper.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
function is_logged_in() {
|
||||||
|
return isset($_SESSION['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_logged_in_user_id() {
|
||||||
|
return $_SESSION['user_id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function login_user($user_id) {
|
||||||
|
$_SESSION['user_id'] = $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout_user() {
|
||||||
|
unset($_SESSION['user_id']);
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
25
api/check_auth.php
Normal file
25
api/check_auth.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
|
if (is_logged_in()) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->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]);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
@ -10,10 +10,21 @@ if (!$data || !isset($data['id'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$recipeId = $data['id'];
|
$recipeId = $data['id'];
|
||||||
|
$userId = get_logged_in_user_id();
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check ownership
|
||||||
|
$stmt = $pdo->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 = $pdo->prepare("DELETE FROM recipes WHERE id = ?");
|
||||||
$stmt->execute([$recipeId]);
|
$stmt->execute([$recipeId]);
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$recipes_stmt = $pdo->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();
|
$recipes = $recipes_stmt->fetchAll();
|
||||||
|
|
||||||
$ingredients_stmt = $pdo->query('SELECT * FROM ingredients');
|
$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();
|
$all_ingredients = $ingredients_stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
$ingredients_by_recipe = [];
|
$ingredients_by_recipe = [];
|
||||||
foreach ($all_ingredients as $ingredient) {
|
foreach ($all_ingredients as $ingredient) {
|
||||||
|
|||||||
28
api/login.php
Normal file
28
api/login.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$email = $data['email'] ?? '';
|
||||||
|
$password = $data['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($email) || empty($password)) {
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
6
api/logout.php
Normal file
6
api/logout.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
|
logout_user();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
40
api/register.php
Normal file
40
api/register.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$email = $data['email'] ?? '';
|
||||||
|
$password = $data['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($email) || empty($password)) {
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
function get_ingredient_category($name) {
|
function get_ingredient_category($name) {
|
||||||
$name = strtolower($name);
|
$name = strtolower($name);
|
||||||
@ -17,6 +17,8 @@ function get_ingredient_category($name) {
|
|||||||
$data = $_POST;
|
$data = $_POST;
|
||||||
$files = $_FILES;
|
$files = $_FILES;
|
||||||
|
|
||||||
|
$userId = get_logged_in_user_id();
|
||||||
|
|
||||||
if (!isset($data['name']) || !isset($data['guests']) || !isset($data['ingredients'])) {
|
if (!isset($data['name']) || !isset($data['guests']) || !isset($data['ingredients'])) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid input.']);
|
echo json_encode(['success' => false, 'error' => 'Invalid input.']);
|
||||||
exit;
|
exit;
|
||||||
@ -55,16 +57,22 @@ try {
|
|||||||
$recipeId = $data['id'];
|
$recipeId = $data['id'];
|
||||||
$category = !empty($data['category']) ? $data['category'] : 'No category';
|
$category = !empty($data['category']) ? $data['category'] : 'No category';
|
||||||
|
|
||||||
// Fetch existing image URL if a new one isn't uploaded
|
// Check if recipe belongs to user
|
||||||
if ($imageUrl === null) {
|
$stmt = $pdo->prepare("SELECT user_id, image_url FROM recipes WHERE id = ?");
|
||||||
$stmt = $pdo->prepare("SELECT image_url FROM recipes WHERE id = ?");
|
|
||||||
$stmt->execute([$recipeId]);
|
$stmt->execute([$recipeId]);
|
||||||
$existing = $stmt->fetch();
|
$existing = $stmt->fetch();
|
||||||
$imageUrl = $existing ? $existing['image_url'] : null;
|
|
||||||
|
if (!$existing || ($existing['user_id'] !== null && $existing['user_id'] != $userId)) {
|
||||||
|
throw new Exception('Unauthorized to update this recipe.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ?, category = ?, image_url = ? WHERE id = ?");
|
// Fetch existing image URL if a new one isn't uploaded
|
||||||
$stmt->execute([$data['name'], $data['guests'], $category, $imageUrl, $recipeId]);
|
if ($imageUrl === null) {
|
||||||
|
$imageUrl = $existing['image_url'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
// Easiest way to handle ingredients is to delete old ones and insert new ones
|
||||||
$stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?");
|
$stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?");
|
||||||
@ -73,8 +81,8 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
// Insert new recipe
|
// Insert new recipe
|
||||||
$category = !empty($data['category']) ? $data['category'] : 'No category';
|
$category = !empty($data['category']) ? $data['category'] : 'No category';
|
||||||
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category, image_url) VALUES (?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category, image_url, user_id) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$data['name'], $data['guests'], $category, $imageUrl]);
|
$stmt->execute([$data['name'], $data['guests'], $category, $imageUrl, $userId]);
|
||||||
$recipeId = $pdo->lastInsertId();
|
$recipeId = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
api/save_shopping_list.php
Normal file
21
api/save_shopping_list.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/auth_helper.php';
|
||||||
|
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
20
api/scan_debug.log
Normal file
20
api/scan_debug.log
Normal file
@ -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
|
||||||
@ -13,9 +13,13 @@ if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$imagePath = $_FILES['image']['tmp_name'];
|
$imagePath = $_FILES['image']['tmp_name'];
|
||||||
|
$imageSize = filesize($imagePath);
|
||||||
$imageData = base64_encode(file_get_contents($imagePath));
|
$imageData = base64_encode(file_get_contents($imagePath));
|
||||||
$imageType = $_FILES['image']['type'];
|
$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.
|
$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:
|
Extract or generate the following information in JSON format:
|
||||||
- name: The name of the dish/recipe
|
- name: The name of the dish/recipe
|
||||||
@ -33,6 +37,7 @@ Important:
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$response = LocalAIApi::createResponse([
|
$response = LocalAIApi::createResponse([
|
||||||
|
'model' => 'gpt-4o',
|
||||||
'input' => [
|
'input' => [
|
||||||
[
|
[
|
||||||
'role' => 'user',
|
'role' => 'user',
|
||||||
@ -44,7 +49,8 @@ try {
|
|||||||
[
|
[
|
||||||
'type' => 'image_url',
|
'type' => 'image_url',
|
||||||
'image_url' => [
|
'image_url' => [
|
||||||
'url' => "data:$imageType;base64,$imageData"
|
'url' => "data:$imageType;base64,$imageData",
|
||||||
|
'detail' => 'auto'
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -53,13 +59,18 @@ try {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$response['success']) {
|
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');
|
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);
|
$recipeData = LocalAIApi::decodeJsonFromResponse($response);
|
||||||
|
|
||||||
if (!$recipeData) {
|
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]);
|
echo json_encode(['success' => true, 'data' => $recipeData]);
|
||||||
|
|||||||
15
api/test_ai.php
Normal file
15
api/test_ai.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = LocalAIApi::createResponse([
|
||||||
|
'input' => [
|
||||||
|
['role' => 'user', 'content' => 'Hello, tell me a short joke.']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@ -1,573 +1,397 @@
|
|||||||
/* Christmas Decorations: Fixed Positioning */
|
|
||||||
/* General Body Styles */
|
/* General Body Styles */
|
||||||
body {
|
body {
|
||||||
background-color: #142E35; /* Dark green background */
|
background-color: #FAF9F6; /* Light beige background */
|
||||||
color: #ffffff; /* White text */
|
color: #2D2D2D; /* Dark gray text */
|
||||||
font-family: 'Poppins', sans-serif;
|
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 */
|
/* Headings */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
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 */
|
/* Buttons and Inputs */
|
||||||
input,
|
input,
|
||||||
button {
|
button,
|
||||||
border-radius: 16px;
|
.form-control,
|
||||||
|
.form-select {
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border-radius: 50px !important; /* Fully rounded corners */
|
border-radius: 50px !important;
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
padding: 10px 24px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Content */
|
/* Main Content */
|
||||||
.display-4 {
|
.display-4 {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: #2D2D2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
color: #ffffff;
|
color: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cards */
|
/* Cards */
|
||||||
.card {
|
.card {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: #ffffff;
|
||||||
border: none;
|
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 {
|
.card-body {
|
||||||
color: #ffffff;
|
color: #2D2D2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
.form-label {
|
.form-label {
|
||||||
color: #ffffff;
|
color: #2D2D2D;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control,
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
.form-select {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
background-color: #ffffff;
|
||||||
color: #ffffff;
|
border: 2px solid #EEEEEE;
|
||||||
|
color: #2D2D2D;
|
||||||
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus,
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
.form-select:focus {
|
||||||
border-color: #0a1a1f; /* Dark green accent */
|
background-color: #ffffff;
|
||||||
box-shadow: 0 0 0 0.25rem rgba(10, 26, 31, 0.25);
|
border-color: #FF7F50; /* Orange accent */
|
||||||
color: #ffffff;
|
box-shadow: 0 0 0 0.25rem rgba(255, 127, 80, 0.15);
|
||||||
|
color: #2D2D2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control::placeholder {
|
.form-control::placeholder {
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: #AAAAAA;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn-primary, .btn-danger {
|
.btn-primary {
|
||||||
background-color: #de4950 !important; /* Coral red */
|
background-color: #FF7F50 !important; /* Orange */
|
||||||
border-color: #de4950 !important;
|
border-color: #FF7F50 !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 {
|
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card {
|
.btn-primary:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: #E66E45 !important;
|
||||||
border-radius: 10px;
|
border-color: #E66E45 !important;
|
||||||
padding: 15px;
|
transform: translateY(-2px);
|
||||||
margin-bottom: 15px;
|
box-shadow: 0 8px 20px rgba(255, 127, 80, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3-Column Layout Adjustments */
|
.btn-danger {
|
||||||
.row.g-4 > [class*='col-'] .card {
|
background-color: #FF4B2B !important;
|
||||||
height: 100%; /* Make cards in columns equal height */
|
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 {
|
#recipe-cards-container, #shopping-list-container {
|
||||||
max-height: 60vh; /* Adjust as needed */
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar for webkit browsers */
|
/* Custom scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: transparent;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #0a1a1f; /* Even darker green */
|
background: #DDDDDD;
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #10242B; /* Slightly lighter than thumb */
|
background: #CCCCCC;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
footer.bg-light {
|
footer {
|
||||||
background-color: transparent !important;
|
border-top: 1px solid #EEEEEE;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
color: #999999;
|
||||||
color: #ffffff;
|
font-size: 0.9rem;
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shopping List Checkbox */
|
/* 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 {
|
.list-group-item.checked .form-check-label {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
opacity: 0.6;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-btn {
|
.form-check-input {
|
||||||
padding: 0.375rem 0.5rem;
|
width: 1.25em;
|
||||||
font-size: 0.875rem;
|
height: 1.25em;
|
||||||
}
|
border: 2px solid #DDDDDD;
|
||||||
|
cursor: pointer;
|
||||||
/* 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:checked {
|
.form-check-input:checked {
|
||||||
background-color: #de4950;
|
background-color: #FF7F50;
|
||||||
border-color: #de4950;
|
border-color: #FF7F50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-check-input:focus {
|
/* Unit buttons */
|
||||||
border-color: #0a1a1f;
|
.unit-selector .btn {
|
||||||
box-shadow: 0 0 0 0.25rem rgba(10, 26, 31, 0.25);
|
padding: 6px 12px !important;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
.unit-selector .btn-secondary {
|
||||||
body * {
|
background-color: #FF7F50 !important;
|
||||||
visibility: hidden;
|
color: white !important;
|
||||||
}
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
.bg-custom-green {
|
.bg-custom-green {
|
||||||
background-color: #142E35 !important;
|
background-color: #FFF0EB !important;
|
||||||
|
color: #FF7F50 !important;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid #FFE4DB;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Styles */
|
/* Quantity Modifiers */
|
||||||
#recipe-form-modal .modal-content, #add-product-modal .modal-content, #confirmRemoveModal .modal-content, #view-recipe-modal .modal-content {
|
.btn-quantity-modifier {
|
||||||
background-color: #142E35;
|
width: 28px;
|
||||||
color: white;
|
height: 28px;
|
||||||
}
|
border-radius: 8px !important;
|
||||||
|
|
||||||
#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;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
background-color: #F8F8F8;
|
||||||
outline: none;
|
border: 1px solid #EEEEEE;
|
||||||
box-shadow: none;
|
color: #666666;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-quantity-modifier:hover {
|
||||||
|
background-color: #FF7F50;
|
||||||
color: white;
|
color: white;
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
border-color: #FF7F50;
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.btn-quantity-modifier:hover {
|
/* Modal Styles */
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
.modal-content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quantity-controls .quantity {
|
.modal-header {
|
||||||
margin: 0 10px;
|
border-bottom: 1px solid #F0F0F0;
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make delete recipe button same height as edit button */
|
.modal-footer {
|
||||||
.card .btn.delete-recipe {
|
border-top: 1px solid #F0F0F0;
|
||||||
padding: .25rem .5rem;
|
padding: 20px 24px;
|
||||||
margin-left: 8px;
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
/* Recipe Card Image */
|
||||||
.card .card-img-top {
|
.card .card-img-top {
|
||||||
height: 200px;
|
height: 180px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
border-top-left-radius: 20px;
|
||||||
|
border-top-right-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* View Recipe Modal */
|
||||||
|
|
||||||
/* 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-ingredients {
|
#view-recipe-ingredients {
|
||||||
background-color: rgba(0, 0, 0, 0.2); /* Darker shade */
|
background-color: #F9F9F9;
|
||||||
padding: 15px;
|
border-radius: 16px;
|
||||||
border-radius: 10px;
|
padding: 20px;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#view-recipe-ingredients .list-group-item {
|
#view-recipe-ingredients .list-group-item {
|
||||||
background-color: transparent;
|
border-bottom: 1px solid #EEEEEE !important;
|
||||||
border: none;
|
padding: 10px 0 !important;
|
||||||
color: #fff;
|
}
|
||||||
|
|
||||||
|
#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; }
|
||||||
}
|
}
|
||||||
BIN
assets/images/auth-bg.jpg
Normal file
BIN
assets/images/auth-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
@ -4,9 +4,107 @@ const app = {
|
|||||||
recipes: [],
|
recipes: [],
|
||||||
confirmedRecipeProducts: [],
|
confirmedRecipeProducts: [],
|
||||||
checkedItems: [],
|
checkedItems: [],
|
||||||
additionalProducts: []
|
additionalProducts: [],
|
||||||
|
user: null
|
||||||
},
|
},
|
||||||
api: {
|
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() {
|
async getRecipes() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('api/get_recipes.php');
|
const response = await fetch('api/get_recipes.php');
|
||||||
@ -99,20 +197,7 @@ const app = {
|
|||||||
|
|
||||||
const text = document.createElement('p');
|
const text = document.createElement('p');
|
||||||
text.className = 'card-text text-muted';
|
text.className = 'card-text text-muted';
|
||||||
text.textContent = `Serves ${recipe.guests} | ${recipe.ingredients.length} ingredients`;
|
text.textContent = `${recipe.ingredients.length} ingredients`;
|
||||||
|
|
||||||
const guestPortionControls = document.createElement('div');
|
|
||||||
guestPortionControls.className = 'row g-2 mb-3';
|
|
||||||
guestPortionControls.innerHTML = `
|
|
||||||
<div class="col">
|
|
||||||
<label for="recipe-guests-${recipe.id}" class="form-label form-label-sm">Guests</label>
|
|
||||||
<input type="number" id="recipe-guests-${recipe.id}" class="form-control form-control-sm recipe-guests-input" value="${recipe.guests}" min="1" data-id="${recipe.id}">
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<label for="recipe-portions-${recipe.id}" class="form-label form-label-sm">Portions</label>
|
|
||||||
<input type="number" id="recipe-portions-${recipe.id}" class="form-control form-control-sm recipe-portions-input" value="1" min="1" data-id="${recipe.id}">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const buttonGroup = document.createElement('div');
|
const buttonGroup = document.createElement('div');
|
||||||
buttonGroup.className = 'mt-auto pt-2';
|
buttonGroup.className = 'mt-auto pt-2';
|
||||||
@ -124,7 +209,6 @@ const app = {
|
|||||||
|
|
||||||
cardBody.appendChild(titleWrapper);
|
cardBody.appendChild(titleWrapper);
|
||||||
cardBody.appendChild(text);
|
cardBody.appendChild(text);
|
||||||
cardBody.appendChild(guestPortionControls);
|
|
||||||
cardBody.appendChild(buttonGroup);
|
cardBody.appendChild(buttonGroup);
|
||||||
card.appendChild(cardBody);
|
card.appendChild(cardBody);
|
||||||
cardCol.appendChild(card);
|
cardCol.appendChild(card);
|
||||||
@ -398,38 +482,98 @@ const app = {
|
|||||||
}
|
}
|
||||||
return null;
|
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() {
|
loadCheckedItems() {
|
||||||
const checkedItems = localStorage.getItem('checkedItems');
|
const stored = localStorage.getItem('checkedItems');
|
||||||
if (checkedItems) {
|
if (stored) {
|
||||||
app.state.checkedItems = JSON.parse(checkedItems);
|
app.state.checkedItems = JSON.parse(stored);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveCheckedItems() {
|
saveCheckedItems() {
|
||||||
localStorage.setItem('checkedItems', JSON.stringify(app.state.checkedItems));
|
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 = `
|
||||||
|
<li class="nav-item me-3 d-none d-lg-block">
|
||||||
|
<span class="text-muted">Welcome, ${app.state.user.email}</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="btn btn-outline-primary btn-sm" id="logout-btn">Logout</button>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
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: {
|
events: {
|
||||||
attachEventListeners() {
|
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.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow());
|
||||||
|
|
||||||
app.dom.aiScanBtn.addEventListener('click', async function() {
|
app.dom.aiScanBtn.addEventListener('click', async function() {
|
||||||
@ -608,6 +752,7 @@ const app = {
|
|||||||
|
|
||||||
productToModify.quantity++;
|
productToModify.quantity++;
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
|
app.api.saveShoppingList();
|
||||||
|
|
||||||
} else if (e.target.matches('.decrement-item')) {
|
} else if (e.target.matches('.decrement-item')) {
|
||||||
const key = e.target.dataset.key;
|
const key = e.target.dataset.key;
|
||||||
@ -647,6 +792,7 @@ const app = {
|
|||||||
if (app.state.confirmedRecipeProducts.includes(confirmationKey)) {
|
if (app.state.confirmedRecipeProducts.includes(confirmationKey)) {
|
||||||
productToModify.quantity--;
|
productToModify.quantity--;
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
|
app.api.saveShoppingList();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('modal-recipe-name').textContent = recipeNameForModal;
|
document.getElementById('modal-recipe-name').textContent = recipeNameForModal;
|
||||||
document.getElementById('modal-ingredient-name').textContent = productToModify.name;
|
document.getElementById('modal-ingredient-name').textContent = productToModify.name;
|
||||||
@ -658,6 +804,7 @@ const app = {
|
|||||||
app.state.confirmedRecipeProducts.push(confirmationKey);
|
app.state.confirmedRecipeProducts.push(confirmationKey);
|
||||||
productToModify.quantity--;
|
productToModify.quantity--;
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
|
app.api.saveShoppingList();
|
||||||
confirmModal.hide();
|
confirmModal.hide();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -665,6 +812,7 @@ const app = {
|
|||||||
// It's not a recipe ingredient about to be removed, but has been added via '+'
|
// It's not a recipe ingredient about to be removed, but has been added via '+'
|
||||||
productToModify.quantity--;
|
productToModify.quantity--;
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
|
app.api.saveShoppingList();
|
||||||
}
|
}
|
||||||
// If not a recipe ingredient and quantity is 0, do nothing.
|
// If not a recipe ingredient and quantity is 0, do nothing.
|
||||||
}
|
}
|
||||||
@ -756,6 +904,7 @@ const app = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
|
app.api.saveShoppingList();
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
app.dom.productNameInput.value = '';
|
app.dom.productNameInput.value = '';
|
||||||
@ -776,16 +925,6 @@ const app = {
|
|||||||
app.dom.addProductModal.hide();
|
app.dom.addProductModal.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.dom.musicToggle.addEventListener('click', function() {
|
|
||||||
if (app.dom.christmasMusic.paused) {
|
|
||||||
app.dom.christmasMusic.play();
|
|
||||||
app.dom.musicToggle.innerHTML = '<i class="bi bi-pause-fill" style="font-size: 1.5rem;"></i>';
|
|
||||||
} else {
|
|
||||||
app.dom.christmasMusic.pause();
|
|
||||||
app.dom.musicToggle.innerHTML = '<i class="bi bi-play-fill" style="font-size: 1.5rem;"></i>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
|
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
|
||||||
if (!app.dom.recipeIdInput.value) {
|
if (!app.dom.recipeIdInput.value) {
|
||||||
app.ui.clearForm();
|
app.ui.clearForm();
|
||||||
@ -821,21 +960,20 @@ const app = {
|
|||||||
productQuantityInput: document.getElementById('productQuantity'),
|
productQuantityInput: document.getElementById('productQuantity'),
|
||||||
productCategoryWrapper: document.getElementById('product-category-wrapper'),
|
productCategoryWrapper: document.getElementById('product-category-wrapper'),
|
||||||
productCategory: document.getElementById('productCategory'),
|
productCategory: document.getElementById('productCategory'),
|
||||||
christmasMusic: document.getElementById('christmas-music'),
|
|
||||||
musicToggle: document.getElementById('music-toggle'),
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
app.ui.createSnowflakes();
|
|
||||||
app.ui.loadCheckedItems();
|
app.ui.loadCheckedItems();
|
||||||
app.events.attachEventListeners();
|
app.events.attachEventListeners();
|
||||||
app.dom.cancelEditBtn.style.display = 'none';
|
app.dom.cancelEditBtn.style.display = 'none';
|
||||||
app.ui.addIngredientRow();
|
app.ui.addIngredientRow();
|
||||||
|
|
||||||
|
app.api.checkAuth().then(() => {
|
||||||
|
app.ui.updateAuthNav();
|
||||||
app.api.getRecipes().then(() => {
|
app.api.getRecipes().then(() => {
|
||||||
app.ui.renderRecipeCards(app.state.recipes);
|
app.ui.renderRecipeCards(app.state.recipes);
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
6
db/migrations/005_create_users_table.sql
Normal file
6
db/migrations/005_create_users_table.sql
Normal file
@ -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
|
||||||
|
);
|
||||||
2
db/migrations/006_add_user_id_to_recipes.sql
Normal file
2
db/migrations/006_add_user_id_to_recipes.sql
Normal file
@ -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;
|
||||||
2
db/migrations/007_add_shopping_list_to_users.sql
Normal file
2
db/migrations/007_add_shopping_list_to_users.sql
Normal file
@ -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;
|
||||||
128
index.php
128
index.php
@ -5,20 +5,20 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<!-- SEO & Meta Tags -->
|
<!-- SEO & Meta Tags -->
|
||||||
<title>Christmas Recipe Calculator</title>
|
<title>Smart Recipe & Shopping List</title>
|
||||||
<meta name="description" content="Create and calculate holiday recipe shopping lists. Enter a recipe for one, specify your number of guests, and get a shopping list for your Christmas feast. Built with Flatlogic Generator.">
|
<meta name="description" content="Manage your recipes and generate smart shopping lists. Identify dishes with AI and calculate ingredients effortlessly.">
|
||||||
<meta name="keywords" content="recipe calculator, Christmas recipes, holiday cooking, shopping list generator, party food calculator, festive meals, cooking for a crowd, recipe scaler, Built with Flatlogic Generator.">
|
<meta name="keywords" content="recipe manager, shopping list generator, AI recipe scanner, cooking organizer, meal planner">
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="Christmas Recipe Calculator">
|
<meta property="og:title" content="Smart Recipe & Shopping List">
|
||||||
<meta property="og:description" content="Easily calculate shopping lists for your holiday recipes.">
|
<meta property="og:description" content="Manage your recipes and generate smart shopping lists.">
|
||||||
<meta property="og:image" content="">
|
<meta property="og:image" content="">
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="Christmas Recipe Calculator">
|
<meta name="twitter:title" content="Smart Recipe & Shopping List">
|
||||||
<meta name="twitter:description" content="Easily calculate shopping lists for your holiday recipes.">
|
<meta name="twitter:description" content="Manage your recipes and generate smart shopping lists.">
|
||||||
<meta name="twitter:image" content="">
|
<meta name="twitter:image" content="">
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
@ -27,25 +27,107 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>_v3">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="christmas-decorations-left">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light shadow-sm sticky-top mb-4">
|
||||||
<img src="assets/pasted-20251130-194432-3a0cbe61.png" alt="Christmas Decorations" />
|
<div class="container">
|
||||||
|
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||||
|
<i class="bi bi-book-half me-2" style="color: var(--accent-color);"></i>
|
||||||
|
<span class="fw-bold">SmartRecipe</span>
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto align-items-center" id="auth-nav">
|
||||||
|
<!-- Auth elements will be injected here -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div id="christmas-decorations-right">
|
</div>
|
||||||
<img src="assets/pasted-20251130-191602-0ffc27f4.png" alt="Christmas Decorations" />
|
</nav>
|
||||||
|
|
||||||
|
<div id="guest-view" class="d-none auth-screen">
|
||||||
|
<div class="container-fluid h-100 p-0">
|
||||||
|
<div class="row h-100 g-0">
|
||||||
|
<!-- Left Column: Image -->
|
||||||
|
<div class="col-lg-6 d-none d-lg-block auth-image-container">
|
||||||
|
<div class="auth-image-overlay d-flex align-items-center justify-content-center p-5 text-white">
|
||||||
|
<div class="auth-branding-content text-center">
|
||||||
|
<i class="bi bi-book-half display-1 mb-4" style="color: #FF7F50; filter: drop-shadow(0 0 10px rgba(255, 127, 80, 0.3));"></i>
|
||||||
|
<h1 class="display-3 fw-bold mb-4">SmartRecipe</h1>
|
||||||
|
<p class="lead fs-4">Manage your recipes and generate smart shopping lists. Identify dishes with AI and calculate ingredients effortlessly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Right Column: Forms -->
|
||||||
|
<div class="col-lg-6 d-flex align-items-center justify-content-center p-5 bg-white auth-form-container">
|
||||||
|
<div class="w-100" style="max-width: 450px;">
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<div id="login-container">
|
||||||
|
<div class="mb-5">
|
||||||
|
<h2 class="display-5 mb-2">Welcome back</h2>
|
||||||
|
<p class="text-muted">Enter your credentials to access your recipes.</p>
|
||||||
|
</div>
|
||||||
|
<form id="login-form-landing">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Email Address</label>
|
||||||
|
<input type="email" class="form-control form-control-lg" name="email" placeholder="name@example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control form-control-lg" name="password" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 mb-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Sign In</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="text-center text-muted">
|
||||||
|
Don't have an account? <a href="#" id="show-register" class="text-primary fw-bold text-decoration-none">Create account</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="snow-container"></div>
|
<!-- Register Form -->
|
||||||
|
<div id="register-container" class="d-none">
|
||||||
|
<div class="mb-5">
|
||||||
|
<h2 class="display-5 mb-2">Create Account</h2>
|
||||||
|
<p class="text-muted">Start your smart cooking journey today.</p>
|
||||||
|
</div>
|
||||||
|
<form id="register-form-landing">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Email Address</label>
|
||||||
|
<input type="email" class="form-control form-control-lg" name="email" placeholder="name@example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control form-control-lg" name="password" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 mb-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Register</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="text-center text-muted">
|
||||||
|
Already have an account? <a href="#" id="show-login" class="text-primary fw-bold text-decoration-none">Login here</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app-view" class="d-none">
|
||||||
<main class="container my-5">
|
<main class="container my-5">
|
||||||
<div class="text-center mb-5" style="padding-top: 20px;">
|
<div class="text-center mb-5" style="padding-top: 20px;">
|
||||||
<h1 class="display-4 mt-4">Hey, it's Christmas time!</h1>
|
<h1 class="display-4 mt-4">My Recipe Book</h1>
|
||||||
<p class="lead">Let's get your holiday recipes sorted.</p>
|
<p class="lead">Plan your meals and get your shopping lists sorted.</p>
|
||||||
<div id="christmas-countdown" class="lead"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
@ -95,6 +177,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal Recipe Form -->
|
<!-- Modal Recipe Form -->
|
||||||
<div class="modal fade" id="recipe-form-modal" tabindex="-1" aria-labelledby="recipe-form-modal-label" aria-hidden="true">
|
<div class="modal fade" id="recipe-form-modal" tabindex="-1" aria-labelledby="recipe-form-modal-label" aria-hidden="true">
|
||||||
@ -110,7 +193,7 @@
|
|||||||
<input type="hidden" id="recipeId">
|
<input type="hidden" id="recipeId">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="recipeName" class="form-label">Recipe Name</label>
|
<label for="recipeName" class="form-label">Recipe Name</label>
|
||||||
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Gingerbread Cookies">
|
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Avocado Toast">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="recipeCategory" class="form-label">Category</label>
|
<label for="recipeCategory" class="form-label">Category</label>
|
||||||
@ -251,19 +334,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="text-center py-4 mt-5">
|
<footer class="text-center py-4 mt-5">
|
||||||
<p class="mb-0">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
<p class="mb-0">© <?php echo date("Y"); ?> Smart Recipe & Shopping List.</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<audio id="christmas-music" loop>
|
|
||||||
<source src="https://www.fesliyanstudios.com/download-link.php?i=230" type="audio/mpeg">
|
|
||||||
</audio>
|
|
||||||
<button id="music-toggle" class="btn btn-light" style="position: fixed; bottom: 20px; right: 20px; border-radius: 50%; width: 50px; height: 50px; display: flex; align-items: center; justify-content: center;">
|
|
||||||
<i class="bi bi-play-fill" style="font-size: 1.5rem;"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="assets/js/main.js?v=1700253313"></script>
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|
||||||
<!-- Confirmation Modal -->
|
<!-- Confirmation Modal -->
|
||||||
<div class="modal fade" id="confirmRemoveModal" tabindex="-1" aria-labelledby="confirmRemoveModalLabel" aria-hidden="true">
|
<div class="modal fade" id="confirmRemoveModal" tabindex="-1" aria-labelledby="confirmRemoveModalLabel" aria-hidden="true">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user