Filter products using select instead of toggles
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"reka-ui": "^2.6.0",
|
"reka-ui": "^2.6.1",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
|
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { TabsRoot, useForwardPropsEmits } from "reka-ui"
|
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 emits = defineEmits<TabsRootEmits>()
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsRoot
|
<TabsRoot v-bind="forwarded">
|
||||||
data-slot="tabs"
|
|
||||||
v-bind="forwarded"
|
|
||||||
:class="cn('flex flex-col gap-2', props.class)"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</TabsRoot>
|
</TabsRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ const delegatedProps = reactiveOmit(props, "class")
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
data-slot="tabs-content"
|
: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)"
|
||||||
:class="cn('flex-1 outline-none', props.class)"
|
|
||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ const delegatedProps = reactiveOmit(props, "class")
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsList
|
<TabsList
|
||||||
data-slot="tabs-list"
|
|
||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
:class="cn(
|
: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,
|
props.class,
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ const forwardedProps = useForwardProps(delegatedProps)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
data-slot="tabs-trigger"
|
|
||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="cn(
|
: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,
|
props.class,
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<slot />
|
<span class="truncate">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</template>
|
</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"
|
||||||
@@ -284,7 +284,7 @@ const call = (number: string, event: Event) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<TooltipArrow :height="8" :width="16"
|
<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>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { Product } from '@/types';
|
|||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { Input } from '@/components/ui/crm-input'
|
import { Input } from '@/components/ui/crm-input'
|
||||||
import { Button } from '@/components/ui/crm-button'
|
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 PlaceholderPattern from '@/components/PlaceholderPattern.vue';
|
||||||
import { toCurrency, toRoundedCurrency } from '@/lib/utils';
|
import { toCurrency, toRoundedCurrency } from '@/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
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 {
|
interface Props {
|
||||||
productsData: Product[];
|
productsData: Product[];
|
||||||
@@ -21,6 +21,7 @@ const searchQuery = ref('')
|
|||||||
const searchField = ref()
|
const searchField = ref()
|
||||||
|
|
||||||
const categoryVisibility = ref<Record<string, boolean>>({});
|
const categoryVisibility = ref<Record<string, boolean>>({});
|
||||||
|
const selectedCategory = ref<string>("all")
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initializeCategoryVisibility();
|
initializeCategoryVisibility();
|
||||||
@@ -40,11 +41,6 @@ const fuse = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const filteredProducts = 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;
|
let products = props.productsData;
|
||||||
|
|
||||||
@@ -54,9 +50,12 @@ const filteredProducts = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter nach Kategorien
|
// Filter nach Kategorien
|
||||||
products = products.filter(product =>
|
if (selectedCategory.value !== "all") {
|
||||||
product.category?.name && visibleCategories.includes(product.category.name)
|
products = products.filter(product =>
|
||||||
);
|
product.category?.name &&
|
||||||
|
product.category.name == selectedCategory.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return products;
|
return products;
|
||||||
});
|
});
|
||||||
@@ -71,6 +70,7 @@ const categories = computed(() => {
|
|||||||
return Array.from(categorySet).sort();
|
return Array.from(categorySet).sort();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const initializeCategoryVisibility = () => {
|
const initializeCategoryVisibility = () => {
|
||||||
const visibility: Record<string, boolean> = {};
|
const visibility: Record<string, boolean> = {};
|
||||||
props.productsData.forEach(product => {
|
props.productsData.forEach(product => {
|
||||||
@@ -99,7 +99,26 @@ const toggleAllCategories = () => {
|
|||||||
<AppLayout title="Produkte">
|
<AppLayout title="Produkte">
|
||||||
|
|
||||||
<AppHeader>
|
<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>
|
<template #middle>
|
||||||
<!-- Search field -->
|
<!-- Search field -->
|
||||||
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
||||||
@@ -135,19 +154,12 @@ const toggleAllCategories = () => {
|
|||||||
</template>
|
</template>
|
||||||
</AppHeader>
|
</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 class="flex flex-wrap gap-8 lg:gap-12">
|
||||||
<div v-for="product in filteredProducts"
|
<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">
|
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">
|
<div class="w-full relative aspect-4/3 overflow-hidden rounded-lg">
|
||||||
<img v-if="product.image" :src="'storage/uploads/products/' + product.image"
|
<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 />
|
<PlaceholderPattern v-else />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user