search bar fixed refactor all to tailwind add recipe form

This commit is contained in:
taogaetz 2025-08-28 12:45:50 -04:00
parent 161b83371e
commit 2a577cf6ab
10 changed files with 448 additions and 902 deletions

View File

@ -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;

View File

@ -1,11 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<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">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</body>
</html>

BIN
src/lib/assets/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -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>

View File

@ -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,21 +107,31 @@
</div>
{#if showResults && filteredRecipes.length > 0}
<div class="search-results">
<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="result-item" on:click={() => handleRecipeClick(recipe)}>
<div class="result-content">
<h4 class="result-title">{recipe.name}</h4>
<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="result-description">{recipe.description}</p>
<p class="mb-2 line-clamp-2 text-sm text-gray-600">{recipe.description}</p>
{/if}
<div class="result-meta">
<span class="result-station">{recipe.station}</span>
<span class="result-time">{recipe.time}</span>
<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="result-arrow"
class="flex-shrink-0 text-gray-400"
width="16"
height="16"
viewBox="0 0 24 24"
@ -134,181 +144,21 @@
</button>
{/each}
</div>
</div>
</div>
{:else if showResults && searchTerm.trim()}
<div class="search-results">
<div class="no-results">
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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,13 +36,17 @@
</button>
</header>
<div class="recipe-content">
<div class="recipe-hero">
<div class="recipe-image">
<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} />
<img src={recipe.photoUrl} alt={recipe.name} class="h-full w-full object-cover" />
{:else}
<div class="placeholder-image">
<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"
@ -47,24 +54,29 @@
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>{recipe.name}</span>
<span class="text-lg font-medium">{recipe.name}</span>
</div>
{/if}
</div>
<div class="recipe-info">
<h1 class="recipe-title">{recipe.name}</h1>
<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="recipe-description">{recipe.description}</p>
<p class="mb-6 text-lg leading-relaxed text-base-content/70">
{recipe.description}
</p>
{/if}
<div class="recipe-meta">
<div class="meta-item">
<div class="flex flex-col gap-4 sm:flex-row">
<div class="badge gap-2 badge-outline badge-lg">
<svg
width="16"
height="16"
@ -76,9 +88,9 @@
<circle cx="12" cy="12" r="10" />
<polyline points="12,6 12,12 16,14" />
</svg>
<span>{recipe.time}</span>
<span class="font-semibold">{recipe.time}</span>
</div>
<div class="meta-item">
<div class="badge gap-2 badge-outline badge-lg">
<svg
width="16"
height="16"
@ -90,252 +102,51 @@
<path d="M3 3h18v18H3z" />
<path d="M9 9h6v6H9z" />
</svg>
<span>{recipe.station}</span>
<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">
<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>
</div>
</div>
{/if}
{#if recipe.instructions}
<section class="instructions-section">
<h2>Instructions</h2>
<div class="instructions-content">
<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>
</div>
</div>
{/if}
</div>
</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>

View 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}`);
}
};

View 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>