mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 14:31:55 +01:00
migrate pages to shadcn (#628)
* feat: migrate tools page and label generator to shadcn * chore: lint issues * feat: also do profile page * feat: shadcn 404 page * feat: login page shadcn * fix: daisyui ironically breaks the z height for the login page * feat: componentise the language selector and add it to the login page * feat: use nuxtlink * feat: card and table made more shadcn * feat: shadcn statscard * chore: lint * feat: shadcn labelchip and locationcard * feat: shadcn locations page * refactor: remove unused new item page * chore: lint * feat: shadcn item card * fix: wrapping of location and lint * feat: ctrl enter in text area in form submits form * feat: begin shadcn locations page and remove pageqrcode comp in favour of integrating it into labelmaker * chore: lint + remove unused code * fix: remove uneeded margin * feat: shadcn labels page and fix some issues with location * feat: shadcn scanner * chore: lint * feat: begin shadcning item pages * feat: shadcn maintenance page * feat: begin shadcn search page * fix: quick switch blurry text and crashing page when switching + incorrect z height for create menu * feat: finish shadcn search page * chore: lint * feat: shadcn edit item page * fix: quickmenumodal bug * feat: shadcn item details page * feat: remove all non-color related daisyui classes * fix: type error * fix: quick menu modal again :(
This commit is contained in:
139
frontend/components/Item/Selector.vue
Normal file
139
frontend/components/Item/Selector.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Label :for="id" class="px-1">
|
||||
{{ label }}
|
||||
</Label>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button :id="id" variant="outline" role="combobox" :aria-expanded="open" class="w-full justify-between">
|
||||
<span>
|
||||
<slot name="display" v-bind="{ item: value }">
|
||||
{{ displayValue(value) || placeholder }}
|
||||
</slot>
|
||||
</span>
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
|
||||
<Command :ignore-filter="true">
|
||||
<CommandInput v-model="search" :placeholder="searchPlaceholder" :display-value="_ => ''" />
|
||||
<CommandEmpty>
|
||||
{{ noResultsText }}
|
||||
</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem v-for="item in filtered" :key="itemKey(item)" :value="itemKey(item)" @select="select(item)">
|
||||
<Check :class="cn('mr-2 h-4 w-4', isSelected(item) ? 'opacity-100' : 'opacity-0')" />
|
||||
<slot name="display" v-bind="{ item }">
|
||||
{{ displayValue(item) }}
|
||||
</slot>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { Check, ChevronsUpDown } from "lucide-vue-next";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useId } from "#imports";
|
||||
|
||||
type ItemsObject = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
modelValue: string | ItemsObject | null | undefined;
|
||||
items: ItemsObject[] | string[];
|
||||
itemText?: string;
|
||||
itemValue?: string;
|
||||
search?: string;
|
||||
searchPlaceholder?: string;
|
||||
noResultsText?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:search"]);
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: "",
|
||||
modelValue: "",
|
||||
items: () => [],
|
||||
itemText: "text",
|
||||
itemValue: "value",
|
||||
search: "",
|
||||
searchPlaceholder: "Type to search...",
|
||||
noResultsText: "No Results Found",
|
||||
placeholder: "Select...",
|
||||
});
|
||||
|
||||
const id = useId();
|
||||
const open = ref(false);
|
||||
const search = ref(props.search);
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
|
||||
watch(
|
||||
() => props.search,
|
||||
val => {
|
||||
search.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => search.value,
|
||||
val => {
|
||||
emit("update:search", val);
|
||||
}
|
||||
);
|
||||
|
||||
function isStrings(arr: string[] | ItemsObject[]): arr is string[] {
|
||||
return typeof arr[0] === "string";
|
||||
}
|
||||
|
||||
function displayValue(item: string | ItemsObject | null | undefined): string {
|
||||
if (!item) return "";
|
||||
if (typeof item === "string") return item;
|
||||
return (item[props.itemText] as string) || "";
|
||||
}
|
||||
|
||||
function itemKey(item: string | ItemsObject): string {
|
||||
if (typeof item === "string") return item;
|
||||
return (item[props.itemValue] as string) || displayValue(item);
|
||||
}
|
||||
|
||||
function isSelected(item: string | ItemsObject): boolean {
|
||||
if (!value.value) return false;
|
||||
if (typeof item === "string") return value.value === item;
|
||||
if (typeof value.value === "string") return itemKey(item) === value.value;
|
||||
return itemKey(item) === itemKey(value.value);
|
||||
}
|
||||
|
||||
function select(item: string | ItemsObject) {
|
||||
if (isSelected(item)) {
|
||||
value.value = null;
|
||||
} else {
|
||||
value.value = item;
|
||||
}
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (!search.value) return props.items;
|
||||
if (isStrings(props.items)) {
|
||||
return props.items.filter(item => item.toLowerCase().includes(search.value.toLowerCase()));
|
||||
} else {
|
||||
// Fuzzy search on itemText
|
||||
return fuzzysort.go(search.value, props.items, { key: props.itemText, all: true }).map(i => i.obj);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user