mirror of
https://github.com/taogaetz/chefbible.git
synced 2025-12-06 11:47:24 -05:00
search bar fixed refactor all to tailwind add recipe form
This commit is contained in:
parent
161b83371e
commit
2a577cf6ab
66
src/app.css
66
src/app.css
@ -1,6 +1,72 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin "daisyui";
|
||||
|
||||
/* Tailwind v4 theme tokens (two-color theme + highlight) */
|
||||
@theme {
|
||||
--color-primary: hsl(var(--text-hsl));
|
||||
--color-secondary: hsl(var(--highlight-hsl));
|
||||
--color-accent: hsl(var(--text-hsl));
|
||||
--color-success: hsl(var(--text-hsl));
|
||||
--color-neutral: hsl(var(--text-hsl));
|
||||
--color-base-100: hsl(var(--bg-hsl));
|
||||
--color-base-200: hsl(var(--bg-2-hsl));
|
||||
--color-base-300: hsl(var(--bg-3-hsl));
|
||||
--color-base-content: hsl(var(--text-hsl));
|
||||
}
|
||||
|
||||
[data-theme='italian'] {
|
||||
/* DaisyUI theme bridge */
|
||||
--p: hsl(var(--text-hsl));
|
||||
--pc: hsl(var(--inverse-hsl));
|
||||
--s: hsl(var(--highlight-hsl));
|
||||
--sc: hsl(var(--inverse-on-highlight-hsl));
|
||||
--a: hsl(var(--text-hsl));
|
||||
--ac: hsl(var(--inverse-hsl));
|
||||
--n: hsl(var(--text-hsl));
|
||||
--nc: hsl(var(--inverse-hsl));
|
||||
--b1: hsl(var(--bg-hsl));
|
||||
--b2: hsl(var(--bg-2-hsl));
|
||||
--b3: hsl(var(--bg-3-hsl));
|
||||
--bc: hsl(var(--text-hsl));
|
||||
|
||||
/* Two key colors (RGB → HSL) */
|
||||
/* Background: rgb(255, 247, 222) → hsl(45.6 100% 93.53%) */
|
||||
--bg-hsl: 45.6 100% 93.53%;
|
||||
--bg-2-hsl: 45.6 100% 90%;
|
||||
--bg-3-hsl: 45.6 100% 85%;
|
||||
/* Text: rgb(123, 34, 30) → hsl(2.5 60.78% 30%) */
|
||||
--text-hsl: 2.5 60.78% 30%;
|
||||
--inverse-hsl: 0 0% 100%;
|
||||
/* Highlight: hsla(54.9, 89.47%, 33.53%, 1) */
|
||||
--highlight-hsl: 54.9 89.47% 33.53%;
|
||||
--inverse-on-highlight-hsl: 0 0% 0%;
|
||||
}
|
||||
|
||||
/* Ensure our theme overrides daisyUI's default light tokens */
|
||||
[data-theme='italian'] {
|
||||
/* Base surfaces */
|
||||
--color-base-100: hsl(var(--bg-hsl));
|
||||
--color-base-200: hsl(var(--bg-2-hsl));
|
||||
--color-base-300: hsl(var(--bg-3-hsl));
|
||||
--color-base-content: hsl(var(--text-hsl));
|
||||
|
||||
/* Brand tokens */
|
||||
--color-primary: hsl(var(--text-hsl));
|
||||
--color-primary-content: hsl(var(--inverse-hsl));
|
||||
--color-secondary: hsl(var(--highlight-hsl));
|
||||
--color-secondary-content: hsl(var(--inverse-on-highlight-hsl));
|
||||
--color-accent: hsl(var(--text-hsl));
|
||||
--color-accent-content: hsl(var(--inverse-hsl));
|
||||
--color-neutral: hsl(var(--text-hsl));
|
||||
--color-neutral-content: hsl(var(--inverse-hsl));
|
||||
}
|
||||
|
||||
/* Force page background/text to the theme values */
|
||||
html[data-theme='italian'] {
|
||||
/* Color the browser/OS area behind the page (e.g., device clock) */
|
||||
background-color: hsl(var(--bg-hsl));
|
||||
}
|
||||
|
||||
/* Global styles for Chef Bible */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
||||
21
src/app.html
21
src/app.html
@ -1,11 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<html lang="en" data-theme="italian">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
src/lib/assets/logo.webp
Normal file
BIN
src/lib/assets/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@ -9,13 +9,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="recipe-card" on:click={handleClick}>
|
||||
<div class="image-container">
|
||||
<div
|
||||
class="card flex h-full cursor-pointer flex-col border border-base-200 bg-white shadow-lg transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
|
||||
on:click={handleClick}
|
||||
>
|
||||
<div class="relative h-48 w-full overflow-hidden">
|
||||
{#if recipe.photoUrl}
|
||||
<img
|
||||
src={recipe.photoUrl}
|
||||
alt={recipe.name}
|
||||
loading="lazy"
|
||||
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
on:error={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
@ -23,8 +27,12 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="placeholder-image hidden">
|
||||
<div class="placeholder-icon">
|
||||
<div
|
||||
class="relative flex hidden h-full w-full flex-col items-center justify-center gap-4 overflow-hidden bg-gradient-to-br from-base-200 to-base-300 text-base-content/60"
|
||||
>
|
||||
<div
|
||||
class="relative z-10 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-primary to-secondary shadow-lg"
|
||||
>
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
@ -32,17 +40,26 @@
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="text-white drop-shadow-sm"
|
||||
>
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="placeholder-text">
|
||||
<span class="recipe-name">{recipe.name}</span>
|
||||
<span class="recipe-station">{recipe.station}</span>
|
||||
<div class="relative z-10 flex flex-col items-center gap-2">
|
||||
<span class="max-w-[90%] text-center text-base leading-tight font-bold text-base-content">
|
||||
{recipe.name}
|
||||
</span>
|
||||
<span
|
||||
class="rounded-full bg-base-100/80 px-3 py-1 text-xs font-semibold text-base-content/70 backdrop-blur-sm"
|
||||
>
|
||||
{recipe.station}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-overlay">
|
||||
<button class="view-button">
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-black/40 to-black/10 opacity-0 transition-opacity duration-300 hover:opacity-100"
|
||||
>
|
||||
<button class="btn gap-2 backdrop-blur-sm btn-sm btn-primary">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
@ -59,14 +76,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h3 class="title">{recipe.name}</h3>
|
||||
<div class="card-body flex flex-1 flex-col p-5">
|
||||
<h3 class="mb-2 card-title line-clamp-2 text-lg font-bold text-base-content">
|
||||
{recipe.name}
|
||||
</h3>
|
||||
{#if recipe.description}
|
||||
<p class="description">{recipe.description}</p>
|
||||
<p class="mb-4 line-clamp-3 flex-1 text-sm leading-relaxed text-base-content/70">
|
||||
{recipe.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="meta">
|
||||
<span class="time">
|
||||
<div class="mt-auto flex items-center justify-between border-t border-base-200 pt-3">
|
||||
<span class="flex items-center gap-1 text-xs text-base-content/60">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
@ -80,283 +101,9 @@
|
||||
</svg>
|
||||
{recipe.time || 'Medium'}
|
||||
</span>
|
||||
<span class="station">{recipe.station || 'Pans'}</span>
|
||||
<span class="badge badge-sm badge-success">
|
||||
{recipe.station || 'Pans'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.recipe-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recipe-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.recipe-card:hover .image-container img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.placeholder-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
color: #64748b;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholder-image::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent 30%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
transparent 70%
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-image.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.placeholder-icon svg {
|
||||
color: white;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.recipe-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.recipe-station {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.placeholder-image span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
max-width: 80%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.1) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.recipe-card:hover .image-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.view-button {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1f2937;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.view-button:hover {
|
||||
background: white;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 16px 0;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.time,
|
||||
.station {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.station {
|
||||
color: #059669;
|
||||
background: #d1fae5;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 640px) {
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.placeholder-icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.recipe-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.recipe-station {
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
import { invalidate } from '$app/navigation';
|
||||
|
||||
export let recipes: any[] = [];
|
||||
export let placeholder: string = 'Search recipes...';
|
||||
@ -30,7 +28,7 @@
|
||||
}
|
||||
|
||||
function handleRecipeClick(recipe: any) {
|
||||
goto(`/recipe/${recipe.id}`);
|
||||
window.location.href = `/recipe/${recipe.id}`;
|
||||
searchTerm = '';
|
||||
showResults = false;
|
||||
}
|
||||
@ -50,7 +48,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener when component mounts
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
@ -62,10 +59,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="search-container" bind:this={searchContainer}>
|
||||
<div class="search-input-wrapper">
|
||||
<div class="relative w-full max-w-lg" bind:this={searchContainer}>
|
||||
<div
|
||||
class="relative flex items-center rounded-xl border-2 border-transparent bg-white shadow-sm transition-all duration-200 focus-within:border-primary focus-within:shadow-lg"
|
||||
>
|
||||
<svg
|
||||
class="search-icon"
|
||||
class="pointer-events-none absolute left-4 text-gray-400"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
@ -82,11 +81,12 @@
|
||||
on:input={handleSearch}
|
||||
on:focus={handleInputFocus}
|
||||
{placeholder}
|
||||
class="search-input"
|
||||
class="w-full border-none bg-transparent px-4 py-3.5 pl-12 text-base text-gray-900 outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
{#if searchTerm}
|
||||
<button
|
||||
class="clear-button"
|
||||
class="absolute right-3 cursor-pointer rounded border-none bg-none p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
||||
aria-label="Clear search"
|
||||
on:click={() => {
|
||||
searchTerm = '';
|
||||
showResults = false;
|
||||
@ -107,208 +107,58 @@
|
||||
</div>
|
||||
|
||||
{#if showResults && filteredRecipes.length > 0}
|
||||
<div class="search-results">
|
||||
{#each filteredRecipes as recipe}
|
||||
<button class="result-item" on:click={() => handleRecipeClick(recipe)}>
|
||||
<div class="result-content">
|
||||
<h4 class="result-title">{recipe.name}</h4>
|
||||
{#if recipe.description}
|
||||
<p class="result-description">{recipe.description}</p>
|
||||
{/if}
|
||||
<div class="result-meta">
|
||||
<span class="result-station">{recipe.station}</span>
|
||||
<span class="result-time">{recipe.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="result-arrow"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
{/each}
|
||||
<div class="absolute top-full left-1/2 z-50 mt-1 w-screen -translate-x-1/2">
|
||||
<div class="mx-auto max-w-7xl px-5">
|
||||
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
|
||||
{#each filteredRecipes as recipe}
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center gap-3 border-b border-gray-100 bg-none p-4 transition-colors last:border-b-0 hover:bg-gray-50"
|
||||
on:click={() => handleRecipeClick(recipe)}
|
||||
>
|
||||
<div class="flex-1 text-left">
|
||||
<h4 class="mb-1 text-base font-semibold text-gray-900">{recipe.name}</h4>
|
||||
{#if recipe.description}
|
||||
<p class="mb-2 line-clamp-2 text-sm text-gray-600">{recipe.description}</p>
|
||||
{/if}
|
||||
<div class="flex gap-3">
|
||||
<span class="rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700"
|
||||
>{recipe.station}</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-700"
|
||||
>{recipe.time}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="flex-shrink-0 text-gray-400"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if showResults && searchTerm.trim()}
|
||||
<div class="search-results">
|
||||
<div class="no-results">
|
||||
<p>No recipes found for "{searchTerm}"</p>
|
||||
<div class="absolute top-full left-1/2 z-50 mt-1 w-screen -translate-x-1/2">
|
||||
<div class="mx-auto max-w-7xl px-5">
|
||||
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
|
||||
<div class="p-6 text-center text-gray-600">
|
||||
<p>No recipes found for "{searchTerm}"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-input-wrapper:focus-within {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px 14px 48px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e5e7eb;
|
||||
margin-top: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.result-description {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-station,
|
||||
.result-time {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.result-station {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.result-arrow {
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 640px) {
|
||||
.search-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
/* No component CSS needed - all styles use Tailwind utilities */
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import logo from '$lib/assets/logo.webp';
|
||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
@ -11,73 +12,26 @@
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="layout">
|
||||
<header class="global-header">
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<a href="/">Chef Bible</a>
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<header
|
||||
class="sticky top-0 z-50 border-b border-base-300 bg-base-100/95 shadow-md brightness-95 backdrop-blur"
|
||||
>
|
||||
<div class="mx-auto flex max-w-[1400px] items-center justify-between gap-6 px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<img src={logo} alt="Chef Bible" class="w-20" />
|
||||
</a>
|
||||
</div>
|
||||
<SearchBar recipes={data.recipes} />
|
||||
<a href="/recipe/new" class="btn font-extrabold btn-lg btn-primary">+</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<main class="flex-1">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.global-header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
padding: 16px 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #1f2937;
|
||||
text-decoration: none;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
/* Utilities provided by Tailwind/daisyUI; no component-scoped CSS needed */
|
||||
</style>
|
||||
|
||||
@ -13,60 +13,12 @@
|
||||
<meta name="description" content="Birbante" />
|
||||
</svelte:head>
|
||||
|
||||
<main class="container">
|
||||
<section class="recipes-section">
|
||||
<div class="recipes-grid">
|
||||
<main class="m-0 min-h-screen bg-gradient-to-br from-base-200 to-base-300 p-0">
|
||||
<section class="mx-auto max-w-7xl p-10 lg:p-6">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{#each data.recipes as recipe}
|
||||
<RecipeCard {recipe} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.recipes-section {
|
||||
padding: 40px 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.recipes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Mobile-first responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.recipes-section {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.recipes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
.recipes-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.recipes-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -16,9 +16,12 @@
|
||||
<meta name="description" content={recipe.description || `Recipe for ${recipe.name}`} />
|
||||
</svelte:head>
|
||||
|
||||
<main class="recipe-page">
|
||||
<header class="recipe-header">
|
||||
<button class="back-button" on:click={goBack}>
|
||||
<main class="m-0 min-h-screen bg-gradient-to-br from-base-200 to-base-300 p-0">
|
||||
<header class="border-b border-base-300 bg-base-100/90 p-5 backdrop-blur-md">
|
||||
<button
|
||||
class="btn gap-2 text-base-content/70 btn-ghost hover:bg-base-200 hover:text-base-content"
|
||||
on:click={goBack}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
@ -33,309 +36,117 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="recipe-content">
|
||||
<div class="recipe-hero">
|
||||
<div class="recipe-image">
|
||||
{#if recipe.photoUrl}
|
||||
<img src={recipe.photoUrl} alt={recipe.name} />
|
||||
{:else}
|
||||
<div class="placeholder-image">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{recipe.name}</span>
|
||||
<div class="mx-auto max-w-7xl p-2">
|
||||
<div class="card mb-10 bg-white shadow-lg">
|
||||
<div class="card-body p-10 lg:p-6">
|
||||
<div class="grid grid-cols-1 gap-10 lg:grid-cols-3 lg:gap-6">
|
||||
<div class="h-80 w-full overflow-hidden rounded-xl lg:h-auto">
|
||||
{#if recipe.photoUrl}
|
||||
<img src={recipe.photoUrl} alt={recipe.name} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-full w-full flex-col items-center justify-center gap-4 bg-gradient-to-br from-base-200 to-base-300 text-base-content/60"
|
||||
>
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="text-base-content/40"
|
||||
>
|
||||
<path
|
||||
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-lg font-medium">{recipe.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="recipe-info">
|
||||
<h1 class="recipe-title">{recipe.name}</h1>
|
||||
{#if recipe.description}
|
||||
<p class="recipe-description">{recipe.description}</p>
|
||||
{/if}
|
||||
<div class="flex flex-col justify-center lg:col-span-2">
|
||||
<h1 class="mb-4 text-4xl leading-tight font-bold text-base-content lg:text-5xl">
|
||||
{recipe.name}
|
||||
</h1>
|
||||
{#if recipe.description}
|
||||
<p class="mb-6 text-lg leading-relaxed text-base-content/70">
|
||||
{recipe.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="recipe-meta">
|
||||
<div class="meta-item">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12,6 12,12 16,14" />
|
||||
</svg>
|
||||
<span>{recipe.time}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 3h18v18H3z" />
|
||||
<path d="M9 9h6v6H9z" />
|
||||
</svg>
|
||||
<span>{recipe.station}</span>
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<div class="badge gap-2 badge-outline badge-lg">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12,6 12,12 16,14" />
|
||||
</svg>
|
||||
<span class="font-semibold">{recipe.time}</span>
|
||||
</div>
|
||||
<div class="badge gap-2 badge-outline badge-lg">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 3h18v18H3z" />
|
||||
<path d="M9 9h6v6H9z" />
|
||||
</svg>
|
||||
<span class="font-semibold">{recipe.station}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recipe-details">
|
||||
{#if recipe.ingredients && recipe.ingredients.length > 0}
|
||||
<section class="ingredients-section">
|
||||
<h2>Ingredients</h2>
|
||||
<ul class="ingredients-list">
|
||||
{#if recipe.ingredients && recipe.ingredients.length > 0}
|
||||
<div class="card mb-6 bg-white shadow-lg">
|
||||
<div class="card-body p-8">
|
||||
<h2 class="mb-6 card-title text-2xl font-bold text-base-content">Ingredients</h2>
|
||||
<ul class="space-y-3">
|
||||
{#each recipe.ingredients as recipeIngredient}
|
||||
<li class="ingredient-item">
|
||||
<span class="ingredient-name">{recipeIngredient.ingredient.name}</span>
|
||||
<li class="flex items-center gap-3 border-b border-base-200 py-3 last:border-b-0">
|
||||
<span class="flex-1 font-semibold text-base-content">
|
||||
{recipeIngredient.ingredient.name}
|
||||
</span>
|
||||
{#if recipeIngredient.quantity || recipeIngredient.unit}
|
||||
<span class="ingredient-amount">
|
||||
<span class="font-medium text-base-content/70">
|
||||
{recipeIngredient.quantity}
|
||||
{recipeIngredient.unit}
|
||||
</span>
|
||||
{/if}
|
||||
{#if recipeIngredient.prep}
|
||||
<span class="ingredient-prep">({recipeIngredient.prep})</span>
|
||||
<span class="text-sm text-base-content/50 italic">
|
||||
({recipeIngredient.prep})
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if recipe.instructions}
|
||||
<section class="instructions-section">
|
||||
<h2>Instructions</h2>
|
||||
<div class="instructions-content">
|
||||
{#if recipe.instructions}
|
||||
<div class="card bg-white shadow-lg">
|
||||
<div class="card-body p-8">
|
||||
<h2 class="mb-6 card-title text-2xl font-bold text-base-content">Instructions</h2>
|
||||
<div class="prose-lg prose max-w-none leading-relaxed text-base-content">
|
||||
{recipe.instructions}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.recipe-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.recipe-header {
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.recipe-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.recipe-hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 40px;
|
||||
margin-bottom: 40px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.recipe-image {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.recipe-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.placeholder-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
color: #6b7280;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.placeholder-image svg {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.recipe-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: #1f2937;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.recipe-description {
|
||||
font-size: 1.125rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.recipe-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.recipe-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.ingredients-section,
|
||||
.instructions-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ingredients-section h2,
|
||||
.instructions-section h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.ingredients-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ingredient-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.ingredient-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ingredient-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ingredient-amount {
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ingredient-prep {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.instructions-content {
|
||||
line-height: 1.7;
|
||||
color: #374151;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.recipe-hero {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.recipe-details {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.recipe-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.recipe-meta {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
60
src/routes/recipe/new/+page.server.ts
Normal file
60
src/routes/recipe/new/+page.server.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type { Actions } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import prisma from '$lib/server/prisma';
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const name = (formData.get('name') as string | null)?.trim() ?? '';
|
||||
const description = (formData.get('description') as string | null)?.trim() || null;
|
||||
const instructions = (formData.get('instructions') as string | null)?.trim() || null;
|
||||
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
|
||||
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
|
||||
const ingredientsRaw = (formData.get('ingredients') as string | null) || '';
|
||||
|
||||
if (!name) {
|
||||
return fail(400, { message: 'Name is required', name, description, instructions, time, station, ingredients: ingredientsRaw });
|
||||
}
|
||||
|
||||
const recipe = await prisma.recipe.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
time,
|
||||
station
|
||||
}
|
||||
});
|
||||
|
||||
const ingredients = Array.from(
|
||||
new Set(
|
||||
ingredientsRaw
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0)
|
||||
)
|
||||
);
|
||||
|
||||
if (ingredients.length > 0) {
|
||||
for (const ingredientName of ingredients) {
|
||||
const ingredient = await prisma.ingredient.upsert({
|
||||
where: { name: ingredientName },
|
||||
update: {},
|
||||
create: { name: ingredientName }
|
||||
});
|
||||
|
||||
await prisma.recipeIngredient.create({
|
||||
data: {
|
||||
recipeId: recipe.id,
|
||||
ingredientId: ingredient.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect(303, `/recipe/${recipe.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
103
src/routes/recipe/new/+page.svelte
Normal file
103
src/routes/recipe/new/+page.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
let pending = $state(false);
|
||||
let lastError = $state<string | null>(null);
|
||||
|
||||
async function onsubmit(event: SubmitEvent) {
|
||||
const form = event.target as HTMLFormElement;
|
||||
if (!form) return;
|
||||
pending = true;
|
||||
lastError = null;
|
||||
try {
|
||||
// native submit; progressive enhancement
|
||||
} finally {
|
||||
// allow server navigation to take over
|
||||
setTimeout(() => (pending = false), 400);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto my-6 max-w-3xl px-5">
|
||||
<h1 class="mb-5 text-2xl font-extrabold">New Recipe</h1>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/create"
|
||||
{onsubmit}
|
||||
class="card grid gap-4 border border-base-200 bg-base-100 p-4 shadow-xl"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Name</span></span>
|
||||
<input
|
||||
name="name"
|
||||
required
|
||||
autocomplete="off"
|
||||
inputmode="text"
|
||||
class="input-bordered input input-lg"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Time</span></span>
|
||||
<select name="time" class="select-bordered select select-lg">
|
||||
<option>Quick</option>
|
||||
<option selected>Medium</option>
|
||||
<option>Long</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Station</span></span>
|
||||
<select name="station" class="select-bordered select select-lg">
|
||||
<option>Garde Manger</option>
|
||||
<option selected>Pans</option>
|
||||
<option>Grill</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Ingredients (one per line)</span></span
|
||||
>
|
||||
<textarea
|
||||
name="ingredients"
|
||||
rows="6"
|
||||
placeholder="Eggs
|
||||
Butter
|
||||
Salt"
|
||||
class="textarea-bordered textarea textarea-lg"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Description</span></span>
|
||||
<textarea name="description" rows="3" class="textarea-bordered textarea textarea-lg"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Instructions</span></span>
|
||||
<textarea name="instructions" rows="6" class="textarea-bordered textarea textarea-lg"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<div class="mt-1 flex justify-end gap-3">
|
||||
<a class="btn" href="/">Cancel</a>
|
||||
<button
|
||||
class="btn font-extrabold btn-lg btn-primary"
|
||||
type="submit"
|
||||
name="/create"
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
{#if lastError}
|
||||
<p class="text-error">{lastError}</p>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Utilities provided by Tailwind/daisyUI; no component-scoped CSS needed */
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user