Link consolodation
This commit is contained in:
parent
762549f4c4
commit
1b740c0234
6 changed files with 105 additions and 13 deletions
|
|
@ -118,6 +118,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, onBeforeMount } from 'vue'
|
import { reactive, onBeforeMount } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { toMealEdit } from '@/router/links'
|
||||||
import { getMeal, saveMeal, getRecipe } from '@/api/sdk'
|
import { getMeal, saveMeal, getRecipe } from '@/api/sdk'
|
||||||
import { toMealInput } from '@/domain/decoders'
|
import { toMealInput } from '@/domain/decoders'
|
||||||
import { currentUser } from '@/api/auth'
|
import { currentUser } from '@/api/auth'
|
||||||
|
|
@ -258,7 +259,7 @@ async function onSaveMeal() {
|
||||||
const saved = await saveMeal(toMealInput(meal))
|
const saved = await saveMeal(toMealInput(meal))
|
||||||
if (saved && saved.id >= 0) {
|
if (saved && saved.id >= 0) {
|
||||||
Object.assign(meal, saved)
|
Object.assign(meal, saved)
|
||||||
router.push(`/meals/${saved.id}`)
|
router.push(toMealEdit(saved.id))
|
||||||
showAlert({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
|
showAlert({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { toRecipeAdd, toRecipeEdit } from '@/router/links'
|
||||||
import ActionItem from '@/components/ActionItem.vue'
|
import ActionItem from '@/components/ActionItem.vue'
|
||||||
import RecipeSearchBox from './RecipeSearchBox.vue'
|
import RecipeSearchBox from './RecipeSearchBox.vue'
|
||||||
import type { Recipe } from '@/domain/types'
|
import type { Recipe } from '@/domain/types'
|
||||||
|
|
@ -20,14 +21,12 @@ import type { Recipe } from '@/domain/types'
|
||||||
const addRecipe = new URL('@/assets/add-recipe.svg', import.meta.url).toString()
|
const addRecipe = new URL('@/assets/add-recipe.svg', import.meta.url).toString()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
|
||||||
const slug = typeof route.params.householdSlug === 'string' ? route.params.householdSlug : ''
|
|
||||||
|
|
||||||
function onSelectRecipe(r: Pick<Recipe, 'id'>) {
|
function onSelectRecipe(r: Pick<Recipe, 'id'>) {
|
||||||
router.push({ name: 'recipe-edit', params: { householdSlug: slug, id: r.id } })
|
router.push(toRecipeEdit(r.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAddRecipe() {
|
function onAddRecipe() {
|
||||||
router.push({ name: 'recipe-add', params: { householdSlug: slug } })
|
router.push(toRecipeAdd())
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onBeforeMount } from 'vue'
|
import { ref, computed, onBeforeMount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { toShoppingList } from '@/router/links'
|
||||||
import { useAlert } from '@/composables/useAlert'
|
import { useAlert } from '@/composables/useAlert'
|
||||||
import { useShopping } from '@/composables/useShopping'
|
import { useShopping } from '@/composables/useShopping'
|
||||||
import { getUpcomingMeals } from '@/api/sdk'
|
import { getUpcomingMeals } from '@/api/sdk'
|
||||||
|
|
@ -191,7 +192,7 @@ async function markPurchased() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected.value = []
|
selected.value = []
|
||||||
router.push(`/shopping/${shopping.id}`)
|
router.push(toShoppingList(shopping.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelect(item: Group) {
|
function toggleSelect(item: Group) {
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,11 @@
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="source.recipe && source.ingredient">
|
<span v-else-if="source.recipe && source.ingredient">
|
||||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
||||||
<router-link :to="`/recipes/${source.recipe.id}/`">{{
|
<router-link :to="toRecipeEdit(source.recipe.id)">{{
|
||||||
source.recipe.name
|
source.recipe.name
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
for
|
for
|
||||||
<router-link :to="`/meals/${source.meal?.id}/`">{{
|
<router-link :to="source.meal ? toMealEdit(source.meal.id) : toRecipes()">{{
|
||||||
source.meal?.suggestedDate
|
source.meal?.suggestedDate
|
||||||
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="source.meal && source.ingredient">
|
<span v-else-if="source.meal && source.ingredient">
|
||||||
{{ source.ingredient.line }} for
|
{{ source.ingredient.line }} for
|
||||||
<router-link :to="`/meals/${source.meal?.id}/`">{{
|
<router-link :to="source.meal ? toMealEdit(source.meal.id) : toMealPlan()">{{
|
||||||
source.meal?.suggestedDate
|
source.meal?.suggestedDate
|
||||||
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
|
|
@ -84,11 +84,11 @@
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="source.recipe && source.ingredient">
|
<span v-else-if="source.recipe && source.ingredient">
|
||||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
||||||
<router-link :to="`/recipes/${source.recipe.id}/`">{{
|
<router-link :to="toRecipeEdit(source.recipe.id)">{{
|
||||||
source.recipe.name
|
source.recipe.name
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
for
|
for
|
||||||
<router-link :to="`/meals/${source.meal?.id ?? ''}/`">{{
|
<router-link :to="source.meal ? toMealEdit(source.meal.id) : toRecipes()">{{
|
||||||
source.meal?.suggestedDate
|
source.meal?.suggestedDate
|
||||||
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="source.meal && source.ingredient">
|
<span v-else-if="source.meal && source.ingredient">
|
||||||
{{ source.ingredient.line }} for
|
{{ source.ingredient.line }} for
|
||||||
<router-link :to="`/meals/${source.meal.id}/`">{{
|
<router-link :to="toMealEdit(source.meal.id)">{{
|
||||||
source.meal.suggestedDate
|
source.meal.suggestedDate
|
||||||
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
? source.meal.suggestedDate.toLocaleDateString('en-AU', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
|
|
@ -118,11 +118,13 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { toMealEdit, toRecipeEdit, toMealPlan, toRecipes } from '@/router/links'
|
||||||
import { ago } from '@/dateformats'
|
import { ago } from '@/dateformats'
|
||||||
import { calculateTotals } from '@/units'
|
import { calculateTotals } from '@/units'
|
||||||
import type { Group } from '@/composables/useShopping'
|
import type { Group } from '@/composables/useShopping'
|
||||||
|
|
||||||
const props = defineProps<{ shoppingListItemGroup: Group }>()
|
const props = defineProps<{ shoppingListItemGroup: Group }>()
|
||||||
|
// Slug resolved via link helpers using current household context
|
||||||
|
|
||||||
const fallbackImg = new URL('@/assets/missing-product.svg', import.meta.url).toString()
|
const fallbackImg = new URL('@/assets/missing-product.svg', import.meta.url).toString()
|
||||||
const imageSrc = computed(() => {
|
const imageSrc = computed(() => {
|
||||||
|
|
|
||||||
66
src/router/links.ts
Normal file
66
src/router/links.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
import { getHouseholdSlug } from '@/api/client'
|
||||||
|
|
||||||
|
// Centralized builders for slug-scoped routes to keep navigation consistent
|
||||||
|
|
||||||
|
// When an options object is provided, require an explicit slug.
|
||||||
|
// Callers may also omit the argument entirely to infer from context.
|
||||||
|
type WithSlug = { slug: string }
|
||||||
|
|
||||||
|
function resolveSlug(input?: WithSlug): string {
|
||||||
|
if (input && typeof input.slug === 'string') return input.slug
|
||||||
|
return getHouseholdSlug() ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages without ids
|
||||||
|
export function toMealPlan(slugOrOpts?: WithSlug): RouteLocationRaw {
|
||||||
|
const slug = resolveSlug(slugOrOpts)
|
||||||
|
return { name: 'mealplan', params: { householdSlug: slug } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRecipes(slugOrOpts?: WithSlug): RouteLocationRaw {
|
||||||
|
const slug = resolveSlug(slugOrOpts)
|
||||||
|
return { name: 'recipes', params: { householdSlug: slug } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRecipeAdd(slugOrOpts?: WithSlug): RouteLocationRaw {
|
||||||
|
const slug = resolveSlug(slugOrOpts)
|
||||||
|
return { name: 'recipe-add', params: { householdSlug: slug } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toMealAdd(slugOrOpts?: WithSlug): RouteLocationRaw {
|
||||||
|
const slug = resolveSlug(slugOrOpts)
|
||||||
|
return { name: 'meal-add', params: { householdSlug: slug } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toShoppingCurrent(slugOrOpts?: WithSlug): RouteLocationRaw {
|
||||||
|
const slug = resolveSlug(slugOrOpts)
|
||||||
|
return { name: 'shopping-current', params: { householdSlug: slug } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toHouseholdSettings(slugOrOpts?: WithSlug): RouteLocationRaw {
|
||||||
|
const slug = resolveSlug(slugOrOpts)
|
||||||
|
return { name: 'household-settings', params: { householdSlug: slug } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages with ids
|
||||||
|
type WithId = { id: number | string }
|
||||||
|
type IdOrOpts = number | string | (WithId & WithSlug)
|
||||||
|
|
||||||
|
export function toRecipeEdit(idOrOpts: IdOrOpts): RouteLocationRaw {
|
||||||
|
const id = typeof idOrOpts === 'object' ? idOrOpts.id : idOrOpts
|
||||||
|
const slug = resolveSlug(typeof idOrOpts === 'object' ? idOrOpts : undefined)
|
||||||
|
return { name: 'recipe-edit', params: { householdSlug: slug, id } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toMealEdit(idOrOpts: IdOrOpts): RouteLocationRaw {
|
||||||
|
const id = typeof idOrOpts === 'object' ? idOrOpts.id : idOrOpts
|
||||||
|
const slug = resolveSlug(typeof idOrOpts === 'object' ? idOrOpts : undefined)
|
||||||
|
return { name: 'meal-edit', params: { householdSlug: slug, id } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toShoppingList(idOrOpts: IdOrOpts): RouteLocationRaw {
|
||||||
|
const id = typeof idOrOpts === 'object' ? idOrOpts.id : idOrOpts
|
||||||
|
const slug = resolveSlug(typeof idOrOpts === 'object' ? idOrOpts : undefined)
|
||||||
|
return { name: 'shopping-list', params: { householdSlug: slug, id } }
|
||||||
|
}
|
||||||
23
tests/router.slug.navigation.test.ts
Normal file
23
tests/router.slug.navigation.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
describe('slug-scoped navigation', () => {
|
||||||
|
it('feature routes are nested under :householdSlug and links use named routes', async () => {
|
||||||
|
const { createAppRouter } = await import('@/router/index')
|
||||||
|
const router = createAppRouter(() => ({}))
|
||||||
|
|
||||||
|
const paths = router.getRoutes().map((r) => r.path)
|
||||||
|
expect(paths).toContain('/:householdSlug/recipes')
|
||||||
|
expect(paths).toContain('/:householdSlug/recipes/:id')
|
||||||
|
expect(paths).toContain('/:householdSlug/mealplan')
|
||||||
|
expect(paths).toContain('/:householdSlug/meals/add')
|
||||||
|
expect(paths).toContain('/:householdSlug/meals/:id')
|
||||||
|
expect(paths).toContain('/:householdSlug/shopping')
|
||||||
|
expect(paths).toContain('/:householdSlug/shopping/current')
|
||||||
|
expect(paths).toContain('/:householdSlug/shopping/:id')
|
||||||
|
|
||||||
|
// No flat feature routes
|
||||||
|
expect(paths).not.toContain('/recipes')
|
||||||
|
expect(paths).not.toContain('/meals/:id')
|
||||||
|
expect(paths).not.toContain('/shopping')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue