UI/logic updates, tests added, backend updated
This commit is contained in:
@@ -190,6 +190,17 @@ html, body {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.save-success-btn {
|
||||
opacity: 1 !important;
|
||||
color: white !important;
|
||||
cursor: default;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.transition-swing {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
background-color: #3b4e1e !important;
|
||||
color: #f4e4bc !important;
|
||||
@@ -208,89 +219,82 @@ html, body {
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
margin: 0.5in 0.75in !important;
|
||||
margin: 0 !important;
|
||||
size: auto;
|
||||
}
|
||||
|
||||
.v-application,
|
||||
.v-application__wrap,
|
||||
main.v-main,
|
||||
.v-container {
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
.recipe-content {
|
||||
margin-top: -20px !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.chat-container,
|
||||
.v-app-bar,
|
||||
.no-print,
|
||||
.separator,
|
||||
.v-divider,
|
||||
button,
|
||||
.v-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body, .recipe-bg, .landing-page, .recipe-card {
|
||||
body {
|
||||
background: white !important;
|
||||
background-image: none !important;
|
||||
box-shadow: none !important;
|
||||
color: black !important;
|
||||
padding: 0.75in !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow: visible !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.chat-container, .v-app-bar, .no-print, .separator, .v-divider, .recipe-description, button, .v-btn {
|
||||
display: none !important;
|
||||
height: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.v-row,
|
||||
.v-col,
|
||||
.v-container,
|
||||
.v-card,
|
||||
.recipe-card,
|
||||
div[class*="v-col"] {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
.v-application, .v-application__wrap, main.v-main, .v-container, .recipe-card {
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.v-application {
|
||||
display: block !important; /* Prevents flex-grow from creating a 2nd page */
|
||||
}
|
||||
|
||||
/* 3. Tighten the Footer (Just in case) */
|
||||
footer, .brand-subtitle {
|
||||
page-break-after: avoid !important;
|
||||
}
|
||||
|
||||
.v-card,
|
||||
.recipe-card,
|
||||
.v-application,
|
||||
.v-application__wrap,
|
||||
[class*="elevation-"] {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
background-color: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* 2. Specific fix for Vuetify's "thin" borders */
|
||||
.v-border-thin,
|
||||
.border,
|
||||
.border-sm {
|
||||
border-width: 0 !important;
|
||||
}
|
||||
|
||||
.v-border-thin,
|
||||
.border,
|
||||
.border-sm {
|
||||
border-width: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
header.text-center {
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
header.text-center img,
|
||||
.v-img,
|
||||
[class*="v-img"] {
|
||||
max-height: 60px !important;
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
margin-bottom: 5px !important;
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
.recipe-content {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
header.text-center img, .v-img, [class*="v-img"] {
|
||||
max-height: 65px !important;
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
margin-bottom: 45px !important;
|
||||
margin-bottom: 40px !important;
|
||||
font-size: 1.6rem !important;
|
||||
text-align: center;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.recipe-content .v-row {
|
||||
@@ -298,53 +302,41 @@ html, body {
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
width: 100% !important;
|
||||
gap: 0.4in !important;
|
||||
gap: 0.5in !important;
|
||||
align-items: flex-start !important;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.recipe-content .v-row > div:first-child {
|
||||
flex: 0 0 35% !important;
|
||||
width: 35% !important;
|
||||
max-width: 35% !important;
|
||||
padding: 0 !important;
|
||||
flex: 0 0 33% !important;
|
||||
max-width: 33% !important;
|
||||
}
|
||||
|
||||
.recipe-content .v-row > div:last-child {
|
||||
flex: 0 0 60% !important;
|
||||
width: 60% !important;
|
||||
max-width: 60% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
flex: 0 0 62% !important;
|
||||
max-width: 62% !important;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 12px !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-bottom: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
* {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
font-weight: bold !important;
|
||||
margin-bottom: 15px !important;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ingredient-item, .step-text {
|
||||
font-size: 0.95rem !important;
|
||||
line-height: 1.3 !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
border: none !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
.instruction-step {
|
||||
margin-bottom: 8px !important;
|
||||
gap: 8px !important;
|
||||
margin-bottom: 10px !important;
|
||||
display: flex !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
|
||||
.recipe-content, .v-row, .v-col, * {
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.step-number, .ingredient-item::before {
|
||||
@@ -352,12 +344,6 @@ html, body {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.recipe-content, .v-row, .v-col {
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
|
||||
@@ -94,10 +94,11 @@
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.v-textarea .v-field__input {
|
||||
font-weight: 500 !important;
|
||||
color: #2c2925 !important;
|
||||
.v-textarea .v-field__input, .v-textarea textarea {
|
||||
font-family: 'Libre Baskerville', serif !important;
|
||||
font-size: 1.1rem !important;
|
||||
line-height: 1.6 !important;
|
||||
color: #2c2925 !important
|
||||
}
|
||||
|
||||
.v-field-label {
|
||||
@@ -105,6 +106,13 @@
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
.v-textarea .v-label,
|
||||
.v-textarea .v-field-label {
|
||||
font-family: 'Libre Baskerville', serif !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
.v-field__outline {
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: #d1c7b7 !important;
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
<v-divider class="mb-10 separator"></v-divider>
|
||||
|
||||
<h2 class="recipe-title text-center mb-4">{{ recipe.title }}</h2>
|
||||
<p v-if="recipe.description" class="recipe-description text-center mb-16 text-italic">
|
||||
{{ recipe.description }}
|
||||
</p>
|
||||
|
||||
<v-img
|
||||
v-if="recipe.imageUrl"
|
||||
:src="recipe.imageUrl"
|
||||
class="recipe-image rounded-lg mb-8 mx-auto"
|
||||
elevation="2"
|
||||
max-height="400"
|
||||
cover
|
||||
></v-img>
|
||||
|
||||
<v-row class="mt-10" no-gutters>
|
||||
<v-row class="mt-10" density="compact">
|
||||
<v-col cols="12" md="5" class="pe-md-10">
|
||||
<div class="section-header justify-center mb-6">
|
||||
<v-icon icon="mdi-spoon-sugar" class="mr-2" size="small"></v-icon>
|
||||
@@ -45,26 +51,25 @@
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="!hasSaved"
|
||||
class="save-recipe-btn px-12"
|
||||
class="px-12 transition-swing"
|
||||
size="large"
|
||||
elevation="0"
|
||||
:loading="isSaving"
|
||||
:disabled="hasSaved"
|
||||
:color="hasSaved ? '#556b2f' : '#5d4a36'"
|
||||
:class="hasSaved ? 'save-success-btn' : 'save-recipe-btn'"
|
||||
@click="$emit('save')"
|
||||
>
|
||||
<v-icon icon="mdi-content-save-check-outline" class="mr-2"></v-icon>
|
||||
Save to Collection
|
||||
<template v-if="!hasSaved">
|
||||
<v-icon icon="mdi-content-save-check-outline" class="mr-2"></v-icon>
|
||||
Save to Collection
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-icon icon="mdi-check-decagram" class="mr-2"></v-icon>
|
||||
Saved in Archives
|
||||
</template>
|
||||
</v-btn>
|
||||
|
||||
<v-chip
|
||||
v-else
|
||||
color="#556b2f"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-check-decagram"
|
||||
class="pa-6"
|
||||
>
|
||||
Saved to Archives
|
||||
</v-chip>
|
||||
</v-row>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -75,7 +80,7 @@ import { ref } from 'vue'
|
||||
import '@/assets/css/app-theme.css'
|
||||
|
||||
defineProps({
|
||||
//recipe: { type: Object, default: null },
|
||||
recipe: { type: Object, default: null },
|
||||
isSaving: { type: Boolean, default: false },
|
||||
hasSaved: { type: Boolean, default: false }
|
||||
})
|
||||
@@ -87,7 +92,7 @@ const printRecipe = () => {
|
||||
}
|
||||
|
||||
// mock output
|
||||
const recipe = ref({
|
||||
/*const recipe = ref({
|
||||
title: "Bakery-Style Lemon Blueberry Muffins",
|
||||
ingredients: [
|
||||
"2 cups all-purpose flour",
|
||||
@@ -109,5 +114,5 @@ const recipe = ref({
|
||||
"Toss the blueberries in a teaspoon of flour, then gently fold them into the batter.",
|
||||
"Divide the batter evenly into the muffin cups and bake for 18-20 minutes until golden."
|
||||
]
|
||||
})
|
||||
}) */
|
||||
</script>
|
||||
@@ -18,6 +18,7 @@
|
||||
<v-col cols="12" md="11">
|
||||
<div class="chat-container">
|
||||
<div class="section-header mb-4 d-flex align-center">
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon icon="mdi-chef-hat" class="mr-2" size="small"></v-icon>
|
||||
<span>Ask the Chef</span>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -82,9 +83,39 @@ const userQuery = ref('')
|
||||
const chatLoading = ref(false)
|
||||
const chatMessages = ref([])
|
||||
const chatDisplay = ref(null)
|
||||
const router = ref(false)
|
||||
const saving = ref(false)
|
||||
const hasSaved = ref(false)
|
||||
|
||||
const isAuthenticated = async () => {
|
||||
try {
|
||||
await $fetch('/api/auth/manage/info', { credentials: 'include' })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const saveToCollection = async () => {
|
||||
if (!recipe.value || hasSaved.value) return
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
await $fetch(`${config.public.apiBase}api/recipe/save`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: recipe.value
|
||||
})
|
||||
|
||||
hasSaved.value = true
|
||||
} catch (error) {
|
||||
console.error("Save failed:", error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const askChef = async () => {
|
||||
if (!userQuery.value.trim()) return
|
||||
|
||||
|
||||
@@ -30,8 +30,15 @@
|
||||
class="rounded-sm mb-4 d-flex align-center justify-center"
|
||||
style="border: 1px solid #e8e2d6;"
|
||||
>
|
||||
<v-img
|
||||
v-if="recipe.imageUrl"
|
||||
:src="recipe.imageUrl"
|
||||
cover
|
||||
class="recipe-thumbnail"
|
||||
></v-img>
|
||||
<v-icon
|
||||
:icon="getRecipeIcon(recipe)"
|
||||
v-else
|
||||
icon="mdi-camera-outline"
|
||||
size="80"
|
||||
color="#d1c7b7"
|
||||
></v-icon>
|
||||
@@ -77,6 +84,7 @@
|
||||
<v-dialog v-model="showDetails" max-width="800" persistent>
|
||||
<v-card v-if="selectedRecipe" class="recipe-card pa-8">
|
||||
<v-btn
|
||||
v-if="!isEditing"
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
position="absolute"
|
||||
@@ -100,7 +108,7 @@
|
||||
<v-row justify="center" class="px-md-10">
|
||||
<v-col cols="12" md="5" class="d-flex flex-column align-center">
|
||||
<div style="width: 100%; max-width: 300px;">
|
||||
<h3 class="section-header mb-4">
|
||||
<h3 class="section-header justify-center mb-4">
|
||||
<v-icon icon="mdi-basket-outline" class="mr-2" size="small"></v-icon>
|
||||
Ingredients
|
||||
</h3>
|
||||
@@ -127,7 +135,7 @@
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="7">
|
||||
<h3 class="section-header mb-4">
|
||||
<h3 class="section-header justify-center mb-4">
|
||||
<v-icon icon="mdi-chef-hat" class="mr-2" size="small"></v-icon>
|
||||
Instructions
|
||||
</h3>
|
||||
@@ -178,6 +186,45 @@
|
||||
</p>
|
||||
</footer>
|
||||
</v-card>
|
||||
<v-row justify="center" class="mb-4">
|
||||
<v-col cols="12" class="d-flex flex-column align-center">
|
||||
<v-hover v-slot="{ isHovering, props }">
|
||||
<v-card
|
||||
v-bind="props"
|
||||
width="200"
|
||||
height="200"
|
||||
class="rounded-lg d-flex align-center justify-center cursor-pointer position-relative"
|
||||
@click="$refs.fileInput.click()"
|
||||
:elevation="isHovering ? 4 : 1"
|
||||
style="border: 2px dashed #d1c7b7; background: #fcfaf5;"
|
||||
>
|
||||
<v-img
|
||||
v-if="selectedRecipe.imageUrl"
|
||||
:src="selectedRecipe.imageUrl"
|
||||
cover
|
||||
class="rounded-lg"
|
||||
></v-img>
|
||||
|
||||
<div
|
||||
v-if="isEditing && (!selectedRecipe.imageUrl || isHovering)"
|
||||
class="d-flex flex-column align-center justify-center position-absolute"
|
||||
style="background: rgba(255,255,255,0.7); inset: 0;"
|
||||
>
|
||||
<v-icon icon="mdi-camera-plus" color="#556b2f" size="large"></v-icon>
|
||||
<span class="brand-subtitle" style="font-size: 0.7rem;">Update Photo</span>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -214,6 +261,17 @@ const fetchRecipes = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
selectedRecipe.value.imageUrl = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
};
|
||||
|
||||
const openRecipe = (recipe) => {
|
||||
selectedRecipe.value = { ...recipe }
|
||||
@@ -252,25 +310,31 @@ const closeDetails = () => {
|
||||
|
||||
const saveChanges = async () => {
|
||||
try {
|
||||
const payload = { ...selectedRecipe.value };
|
||||
if (typeof payload.ingredients === 'string') {
|
||||
payload.ingredients = payload.ingredients.split('\n').filter(i => i.trim());
|
||||
}
|
||||
if (typeof payload.instructions === 'string') {
|
||||
payload.instructions = payload.instructions.split('\n').filter(i => i.trim());
|
||||
}
|
||||
const payload = {
|
||||
...selectedRecipe.value,
|
||||
ingredients: typeof selectedRecipe.value.ingredients === 'string'
|
||||
? selectedRecipe.value.ingredients.split('\n').filter(i => i.trim())
|
||||
: selectedRecipe.value.ingredients,
|
||||
instructions: typeof selectedRecipe.value.instructions === 'string'
|
||||
? selectedRecipe.value.instructions.split('\n').filter(i => i.trim())
|
||||
: selectedRecipe.value.instructions
|
||||
};
|
||||
|
||||
await $fetch(`${config.public.apiBase}api/recipe/update/${selectedRecipe.value.id}`, {
|
||||
await $fetch(`${config.public.apiBase}api/recipe/update/${payload.id}`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
body: payload
|
||||
body: payload,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
await fetchRecipes();
|
||||
const index = recipes.value.findIndex(r => r.id === payload.id);
|
||||
if (index !== -1) {
|
||||
recipes.value[index] = { ...payload };
|
||||
}
|
||||
|
||||
closeDetails();
|
||||
} catch (e) {
|
||||
console.error("Failed to update recipe:", e);
|
||||
alert("Could not save changes.");
|
||||
console.error("The kitchen ledger could not be updated:", e);
|
||||
alert("Could not save changes. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-0 landing-wrapper">
|
||||
<v-row no-gutters justify="center" align="start" class="pt-6">
|
||||
<v-row density="compact" justify="center" align="start" class="pt-6">
|
||||
<v-col cols="12" class="text-center px-4">
|
||||
|
||||
<v-card class="recipe-card pa-8 mx-auto mt-2" max-width="900">
|
||||
@@ -29,7 +29,7 @@
|
||||
</p>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="isLoggedIn" to="/uploader" class="mt-12 column-btn">
|
||||
Got to Uploader
|
||||
Go to Uploader
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="text-center d-flex flex-column align-center">
|
||||
|
||||
Reference in New Issue
Block a user