From 8731cad85422de19a5607714791d7f273178ef0d Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 13 Apr 2026 11:34:54 -0700 Subject: [PATCH] feat: community feed Vue frontend -- Pinia store + feed panel + RecipesView tab --- .../src/components/CommunityFeedPanel.vue | 251 ++++++++++++++++++ frontend/src/components/CommunityPostCard.vue | 172 ++++++++++++ frontend/src/components/RecipesView.vue | 25 +- frontend/src/stores/community.ts | 98 +++++++ 4 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/CommunityFeedPanel.vue create mode 100644 frontend/src/components/CommunityPostCard.vue create mode 100644 frontend/src/stores/community.ts diff --git a/frontend/src/components/CommunityFeedPanel.vue b/frontend/src/components/CommunityFeedPanel.vue new file mode 100644 index 0000000..cdb4d7f --- /dev/null +++ b/frontend/src/components/CommunityFeedPanel.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/frontend/src/components/CommunityPostCard.vue b/frontend/src/components/CommunityPostCard.vue new file mode 100644 index 0000000..afa223c --- /dev/null +++ b/frontend/src/components/CommunityPostCard.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index e5d7700..7fba9fe 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -32,6 +32,14 @@ @open-recipe="openRecipeById" /> + + +
@@ -554,6 +562,7 @@ import { useInventoryStore } from '../stores/inventory' import RecipeDetailPanel from './RecipeDetailPanel.vue' import RecipeBrowserPanel from './RecipeBrowserPanel.vue' import SavedRecipesPanel from './SavedRecipesPanel.vue' +import CommunityFeedPanel from './CommunityFeedPanel.vue' import type { RecipeSuggestion, GroceryLink } from '../services/api' import { recipesAPI } from '../services/api' @@ -561,16 +570,17 @@ const recipesStore = useRecipesStore() const inventoryStore = useInventoryStore() // Tab state -type TabId = 'find' | 'browse' | 'saved' +type TabId = 'find' | 'browse' | 'saved' | 'community' const tabs: Array<{ id: TabId; label: string }> = [ - { id: 'find', label: 'Find' }, - { id: 'browse', label: 'Browse' }, - { id: 'saved', label: 'Saved' }, + { id: 'find', label: 'Find' }, + { id: 'browse', label: 'Browse' }, + { id: 'saved', label: 'Saved' }, + { id: 'community', label: 'Community' }, ] const activeTab = ref('find') function onTabKeydown(e: KeyboardEvent) { - const tabIds: TabId[] = ['find', 'browse', 'saved'] + const tabIds: TabId[] = ['find', 'browse', 'saved', 'community'] const current = tabIds.indexOf(activeTab.value) if (e.key === 'ArrowRight') { e.preventDefault() @@ -581,6 +591,11 @@ function onTabKeydown(e: KeyboardEvent) { } } +// Community tab: navigate to Find tab after a plan fork (full plan view deferred to Task 9) +function onPlanForked(_payload: { plan_id: number; week_start: string }) { + activeTab.value = 'find' +} + // Browser/saved tab recipe detail panel (fetches full recipe from API) const browserSelectedRecipe = ref(null) diff --git a/frontend/src/stores/community.ts b/frontend/src/stores/community.ts new file mode 100644 index 0000000..8abd6af --- /dev/null +++ b/frontend/src/stores/community.ts @@ -0,0 +1,98 @@ +/** + * Community Store + * + * Manages community post feed state and fork actions using Pinia. + * Follows the composition store pattern established in recipes.ts. + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' +import api from '../services/api' + +// ========== Types ========== + +export interface CommunityPostSlot { + day: string + meal_type: string + recipe_id: number +} + +export interface ElementProfiles { + seasoning_score: number | null + richness_score: number | null + brightness_score: number | null + depth_score: number | null + aroma_score: number | null + structure_score: number | null + texture_profile: string | null +} + +export interface CommunityPost { + slug: string + pseudonym: string + post_type: 'plan' | 'recipe_success' | 'recipe_blooper' + published: string + title: string + description: string | null + photo_url: string | null + slots: CommunityPostSlot[] + recipe_id: number | null + recipe_name: string | null + level: number | null + outcome_notes: string | null + element_profiles: ElementProfiles + dietary_tags: string[] + allergen_flags: string[] + flavor_molecules: string[] + fat_pct: number | null + protein_pct: number | null + moisture_pct: number | null +} + +export interface ForkResult { + plan_id: number + week_start: string + forked_from: string +} + +// ========== Store ========== + +export const useCommunityStore = defineStore('community', () => { + const posts = ref([]) + const loading = ref(false) + const error = ref(null) + const currentFilter = ref(null) + + async function fetchPosts(postType?: string) { + loading.value = true + error.value = null + currentFilter.value = postType ?? null + + try { + const params: Record = { page: 1, page_size: 40 } + if (postType) { + params.post_type = postType + } + const response = await api.get<{ posts: CommunityPost[] }>('/community/posts', { params }) + posts.value = response.data.posts + } catch (err: unknown) { + error.value = err instanceof Error ? err.message : 'Could not load community posts.' + } finally { + loading.value = false + } + } + + async function forkPost(slug: string): Promise { + const response = await api.post(`/community/posts/${slug}/fork`) + return response.data + } + + return { + posts, + loading, + error, + currentFilter, + fetchPosts, + forkPost, + } +})