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,
+ }
+})