kiwi/frontend/src/App.vue
pyr0ball 0da1d97a60 feat: recipe + settings frontend — Recipes and Settings tabs
- RecipesView: level selector (1-4), constraints/allergies tag inputs,
  hard day mode toggle, max missing input, expiry-first pantry extraction,
  recipe cards with collapsible swaps/directions, grocery links, rate
  limit banner
- SettingsView: cooking equipment tag input with quick-add chips, save
  with confirmation feedback
- stores/recipes.ts: Pinia store for recipe state + suggest() action
- stores/settings.ts: Pinia store for cooking_equipment persistence
- api.ts: RecipeRequest/Result/Suggestion types + recipesAPI + settingsAPI
- App.vue: two new tabs (Recipes, Settings), lazy inventory load on tab switch
2026-03-31 19:20:13 -07:00

228 lines
4.5 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="app">
<header class="app-header">
<div class="container">
<h1>🥝 Kiwi</h1>
<p class="tagline">Smart Pantry Tracking & Recipe Suggestions</p>
</div>
</header>
<main class="app-main">
<div class="container">
<!-- Tabs -->
<div class="tabs">
<button
:class="['tab', { active: currentTab === 'inventory' }]"
@click="switchTab('inventory')"
>
🏪 Inventory
</button>
<button
:class="['tab', { active: currentTab === 'receipts' }]"
@click="switchTab('receipts')"
>
🧾 Receipts
</button>
<button
:class="['tab', { active: currentTab === 'recipes' }]"
@click="switchTab('recipes')"
>
🍳 Recipes
</button>
<button
:class="['tab', { active: currentTab === 'settings' }]"
@click="switchTab('settings')"
>
Settings
</button>
</div>
<!-- Tab Content -->
<div v-show="currentTab === 'inventory'" class="tab-content">
<InventoryList />
</div>
<div v-show="currentTab === 'receipts'" class="tab-content">
<ReceiptsView />
</div>
<div v-show="currentTab === 'recipes'" class="tab-content">
<RecipesView />
</div>
<div v-show="currentTab === 'settings'" class="tab-content">
<SettingsView />
</div>
</div>
</main>
<footer class="app-footer">
<div class="container">
<p>&copy; 2026 CircuitForge LLC</p>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import InventoryList from './components/InventoryList.vue'
import ReceiptsView from './components/ReceiptsView.vue'
import RecipesView from './components/RecipesView.vue'
import SettingsView from './components/SettingsView.vue'
import { useInventoryStore } from './stores/inventory'
type Tab = 'inventory' | 'receipts' | 'recipes' | 'settings'
const currentTab = ref<Tab>('inventory')
const inventoryStore = useInventoryStore()
async function switchTab(tab: Tab) {
currentTab.value = tab
if (tab === 'recipes' && inventoryStore.items.length === 0) {
await inventoryStore.fetchItems()
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--color-bg-primary);
color: var(--color-text-primary);
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
}
.app-header {
background: var(--gradient-primary);
color: white;
padding: var(--spacing-xl) 0;
box-shadow: var(--shadow-md);
}
.app-header h1 {
font-size: 32px;
margin-bottom: 5px;
}
.app-header .tagline {
font-size: 16px;
opacity: 0.9;
}
.app-main {
flex: 1;
padding: 20px 0;
}
.app-footer {
background: var(--color-bg-elevated);
color: var(--color-text-secondary);
padding: var(--spacing-lg) 0;
text-align: center;
margin-top: var(--spacing-xl);
border-top: 1px solid var(--color-border);
}
.app-footer p {
font-size: var(--font-size-sm);
opacity: 0.8;
}
/* Tabs */
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 15px 30px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.tab:hover {
background: rgba(255, 255, 255, 0.3);
}
.tab.active {
background: var(--color-bg-card);
color: var(--color-primary);
font-weight: 600;
}
.tab-content {
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Mobile Responsive Breakpoints */
@media (max-width: 480px) {
.container {
padding: 0 12px;
}
.app-header h1 {
font-size: 24px;
}
.app-header .tagline {
font-size: 14px;
}
.tabs {
gap: 8px;
}
.tab {
padding: 12px 20px;
font-size: 14px;
flex: 1;
}
}
@media (min-width: 481px) and (max-width: 768px) {
.container {
padding: 0 16px;
}
.app-header h1 {
font-size: 28px;
}
.tab {
padding: 14px 25px;
}
}
</style>