Filter products using select instead of toggles

This commit is contained in:
2025-12-08 13:23:33 +01:00
parent cdb8e44228
commit 7d9261dd6e
13 changed files with 142 additions and 39 deletions
+1 -1
View File
@@ -41,7 +41,7 @@
"laravel-vite-plugin": "^2.0.0",
"lucide-vue-next": "^0.468.0",
"pinia": "^3.0.3",
"reka-ui": "^2.6.0",
"reka-ui": "^2.6.1",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
+3 -11
View File
@@ -1,23 +1,15 @@
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TabsRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TabsRootProps & { class?: HTMLAttributes["class"] }>()
const props = defineProps<TabsRootProps>()
const emits = defineEmits<TabsRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<TabsRoot
data-slot="tabs"
v-bind="forwarded"
:class="cn('flex flex-col gap-2', props.class)"
>
<TabsRoot v-bind="forwarded">
<slot />
</TabsRoot>
</template>
@@ -12,8 +12,7 @@ const delegatedProps = reactiveOmit(props, "class")
<template>
<TabsContent
data-slot="tabs-content"
:class="cn('flex-1 outline-none', props.class)"
:class="cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', props.class)"
v-bind="delegatedProps"
>
<slot />
+1 -2
View File
@@ -12,10 +12,9 @@ const delegatedProps = reactiveOmit(props, "class")
<template>
<TabsList
data-slot="tabs-list"
v-bind="delegatedProps"
:class="cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
'inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
props.class,
)"
>
@@ -14,13 +14,14 @@ const forwardedProps = useForwardProps(delegatedProps)
<template>
<TabsTrigger
data-slot="tabs-trigger"
v-bind="forwardedProps"
:class="cn(
`data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
props.class,
)"
>
<slot />
<span class="truncate">
<slot />
</span>
</TabsTrigger>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { TagsInputRootEmits, TagsInputRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TagsInputRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<TagsInputRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TagsInputRoot v-bind="forwarded" :class="cn('flex flex-wrap gap-2 items-center rounded-md border border-input bg-background px-3 py-2 text-sm', props.class)">
<slot />
</TagsInputRoot>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { TagsInputInputProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputInput, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TagsInputInputProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputInput v-bind="forwardedProps" :class="cn('text-sm min-h-6 focus:outline-none flex-1 bg-transparent px-1', props.class)" />
</template>
@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { TagsInputItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TagsInputItemProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputItem v-bind="forwardedProps" :class="cn('flex h-6 items-center rounded bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background', props.class)">
<slot />
</TagsInputItem>
</template>
@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { TagsInputItemDeleteProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { TagsInputItemDelete, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TagsInputItemDeleteProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputItemDelete v-bind="forwardedProps" :class="cn('flex rounded bg-transparent mr-1', props.class)">
<slot>
<X class="w-4 h-4" />
</slot>
</TagsInputItemDelete>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { TagsInputItemTextProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputItemText, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TagsInputItemTextProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputItemText v-bind="forwardedProps" :class="cn('py-1 px-2 text-sm rounded bg-transparent', props.class)" />
</template>
@@ -0,0 +1,5 @@
export { default as TagsInput } from "./TagsInput.vue"
export { default as TagsInputInput } from "./TagsInputInput.vue"
export { default as TagsInputItem } from "./TagsInputItem.vue"
export { default as TagsInputItemDelete } from "./TagsInputItemDelete.vue"
export { default as TagsInputItemText } from "./TagsInputItemText.vue"
+1 -1
View File
@@ -284,7 +284,7 @@ const call = (number: string, event: Event) => {
</Button>
</ButtonGroup>
<TooltipArrow :height="8" :width="16"
class="fill-popover drop-shadow-(--shadow-arrow) stroke-[0.5px] stroke-border -mt-[1px]" />
class="fill-popover drop-shadow-(--shadow-arrow) stroke-[0.5px] stroke-border -mt-px" />
</TooltipContent>
</Tooltip>
+31 -19
View File
@@ -6,12 +6,12 @@ import { Product } from '@/types';
import Fuse from 'fuse.js';
import { Input } from '@/components/ui/crm-input'
import { Button } from '@/components/ui/crm-button'
import { Delete, Search, Plus } from "lucide-vue-next"
import { Delete, Search, Plus, CheckIcon, ChevronDown } from "lucide-vue-next"
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
import { toCurrency, toRoundedCurrency } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { Toggle } from '@/components/ui/toggle'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, SelectSeparator } from '@/components/ui/select'
interface Props {
productsData: Product[];
@@ -21,6 +21,7 @@ const searchQuery = ref('')
const searchField = ref()
const categoryVisibility = ref<Record<string, boolean>>({});
const selectedCategory = ref<string>("all")
onMounted(() => {
initializeCategoryVisibility();
@@ -40,11 +41,6 @@ const fuse = computed(() => {
})
const filteredProducts = computed(() => {
const visibleCategories = Object.entries(categoryVisibility.value)
.filter(([_, isVisible]) => isVisible)
.map(([categoryName]) => categoryName);
if (visibleCategories.length == 0) return []
let products = props.productsData;
@@ -54,9 +50,12 @@ const filteredProducts = computed(() => {
}
// Filter nach Kategorien
products = products.filter(product =>
product.category?.name && visibleCategories.includes(product.category.name)
);
if (selectedCategory.value !== "all") {
products = products.filter(product =>
product.category?.name &&
product.category.name == selectedCategory.value
);
}
return products;
});
@@ -71,6 +70,7 @@ const categories = computed(() => {
return Array.from(categorySet).sort();
});
const initializeCategoryVisibility = () => {
const visibility: Record<string, boolean> = {};
props.productsData.forEach(product => {
@@ -99,7 +99,26 @@ const toggleAllCategories = () => {
<AppLayout title="Produkte">
<AppHeader>
<template #left></template>
<template #left>
<!-- Category select -->
<Select v-model="selectedCategory">
<SelectTrigger class="w-[180px] bg-background">
<SelectValue placeholder="Kategorie" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">
Alle
</SelectItem>
<SelectSeparator />
<SelectItem v-for="category in categories" :value="category">
{{ category }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>
<template #middle>
<!-- Search field -->
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
@@ -135,19 +154,12 @@ const toggleAllCategories = () => {
</template>
</AppHeader>
<div class="flex items-center justify-center mb-8">
<Toggle :modelValue="allCategoriesVisible" variant="outline" size="sm" aria-label="Alle Kategorien"
@click="toggleAllCategories">Alle</Toggle>
<Toggle v-for="category in categories" v-model="categoryVisibility[category]" variant="outline" size="sm"
:aria-label="'Kategorie ' + category">{{ category }}</Toggle>
</div>
<div class="flex flex-wrap gap-8 lg:gap-12">
<div v-for="product in filteredProducts"
class="w-[calc(50%-4*var(--spacing))] md:w-[calc(33%-5.333*var(--spacing))] lg:w-[calc(25%-9*var(--spacing))] xl:w-[calc(20%-9.6*var(--spacing))] flex flex-col gap-4">
<div class="w-full relative aspect-4/3 overflow-hidden rounded-lg">
<img v-if="product.image" :src="'storage/uploads/products/' + product.image"
class="size-full object-cover" loading="lazy" />
class="size-full object-cover dark:brightness-75" loading="lazy" />
<PlaceholderPattern v-else />
</div>