dev: hotfix refactor
+ merged pub/custom-ui and pub/base into my-ui - droped pub/custom-ui
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { DateRange } from 'radix-vue'
|
||||
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||
import { cn } from '~/lib/utils'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
const props = defineProps<{
|
||||
prep: HeaderPrep
|
||||
refSearchNav?: RefSearchNav
|
||||
}>()
|
||||
|
||||
// function emitSearchNavClick() {
|
||||
// props.refSearchNav?.onClick()
|
||||
// }
|
||||
//
|
||||
// function onInput(event: Event) {
|
||||
// props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
|
||||
// }
|
||||
//
|
||||
// function btnClick() {
|
||||
// props.prep?.addNav?.onClick?.()
|
||||
// }
|
||||
|
||||
const searchQuery = ref('')
|
||||
const dateRange = ref<{ from: Date | null; to: Date | null }>({
|
||||
from: new Date(),
|
||||
to: new Date(),
|
||||
})
|
||||
|
||||
const df = new DateFormatter('en-US', {
|
||||
dateStyle: 'medium',
|
||||
})
|
||||
|
||||
const value = ref({
|
||||
start: new CalendarDate(2022, 1, 20),
|
||||
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||
}) as Ref<DateRange>
|
||||
|
||||
function onFilterClick() {
|
||||
console.log('Search:', searchQuery.value)
|
||||
console.log('Date Range:', dateRange.value)
|
||||
props.refSearchNav?.onClick()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative w-64">
|
||||
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
:class="cn('w-[280px] justify-start text-left font-normal', !value && 'text-muted-foreground')"
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
<template v-if="value.start">
|
||||
<template v-if="value.end">
|
||||
{{ df.format(value.start.toDate(getLocalTimeZone())) }} -
|
||||
{{ df.format(value.end.toDate(getLocalTimeZone())) }}
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else> Pick a date </template>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-0">
|
||||
<RangeCalendar
|
||||
v-model="value"
|
||||
initial-focus
|
||||
:number-of-months="2"
|
||||
@update:start-value="(startDate) => (value.start = startDate)"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Button variant="outline" class="border-orange-500 text-orange-600 hover:bg-orange-50" @click="onFilterClick">
|
||||
<FilterIcon class="mr-2 size-4" />
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{
|
||||
prep: HeaderPrep
|
||||
modelValue?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'search': [value: string]
|
||||
}>()
|
||||
|
||||
// Internal search state
|
||||
const searchInput = ref(props.modelValue || '')
|
||||
const debouncedSearch = refDebounced(searchInput, props.prep.refSearchNav?.debounceMs || 500)
|
||||
|
||||
// Computed search model for v-model
|
||||
const searchModel = computed({
|
||||
get: () => searchInput.value,
|
||||
set: (value: string) => {
|
||||
searchInput.value = value
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
// Watch for external changes to modelValue
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue !== searchInput.value) {
|
||||
searchInput.value = newValue || ''
|
||||
}
|
||||
})
|
||||
|
||||
// Watch debounced search and emit search event
|
||||
watch(debouncedSearch, (newValue) => {
|
||||
const minLength = props.prep.refSearchNav?.minLength || 3
|
||||
// Only search if meets minimum length or empty (to clear search)
|
||||
if (newValue.length === 0 || newValue.length >= minLength) {
|
||||
emit('search', newValue)
|
||||
props.prep.refSearchNav?.onInput(newValue)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle clear search
|
||||
function clearSearch() {
|
||||
searchModel.value = ''
|
||||
props.prep.refSearchNav?.onClear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="ml-3 text-lg font-bold text-gray-900">
|
||||
<Icon :name="props.prep.icon!" class="mr-2 size-4 md:size-6 align-middle" />
|
||||
{{ props.prep.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<!-- Search Section -->
|
||||
<div v-if="props.prep.refSearchNav" class="ml-3 text-lg text-gray-900 relative">
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="search-table"
|
||||
v-model="searchModel"
|
||||
name="search-table"
|
||||
type="text"
|
||||
class="w-full rounded-md border bg-white px-4 py-2 text-gray-900 sm:text-sm"
|
||||
:class="[
|
||||
props.prep.refSearchNav.inputClass,
|
||||
{
|
||||
'border-amber-300 bg-amber-50': searchInput.length > 0 && searchInput.length < (props.prep.refSearchNav.minLength || 3),
|
||||
'border-green-300 bg-green-50': searchInput.length >= (props.prep.refSearchNav.minLength || 3),
|
||||
},
|
||||
]"
|
||||
:placeholder="props.prep.refSearchNav.placeholder || 'Cari (min. 3 karakter)...'"
|
||||
/>
|
||||
|
||||
<!-- Clear button -->
|
||||
<button
|
||||
v-if="searchInput.length > 0"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
type="button"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<Icon name="i-lucide-x" class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<!-- Validation feedback -->
|
||||
<div
|
||||
v-if="props.prep.refSearchNav.showValidationFeedback !== false && searchInput.length > 0 && searchInput.length < (props.prep.refSearchNav.minLength || 3)"
|
||||
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
|
||||
>
|
||||
Minimal {{ props.prep.refSearchNav.minLength || 3 }} karakter untuk mencari
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Button -->
|
||||
<div v-if="props.prep.addNav" class="m-2 flex items-center">
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
|
||||
:class="props.prep.addNav.classVal"
|
||||
:variant="(props.prep.addNav.variant as any) || 'default'"
|
||||
@click="props.prep.addNav?.onClick"
|
||||
>
|
||||
<Icon :name="props.prep.addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ props.prep.addNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Button -->
|
||||
<div v-if="props.prep.filterNav" class="m-2 flex items-center">
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||
:class="props.prep.filterNav.classVal"
|
||||
:variant="(props.prep.filterNav.variant as any) || 'default'"
|
||||
@click="props.prep.filterNav?.onClick"
|
||||
>
|
||||
<Icon :name="props.prep.filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ props.prep.filterNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Print Button -->
|
||||
<div v-if="props.prep.printNav" class="m-2 flex items-center">
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||
:class="props.prep.printNav.classVal"
|
||||
:variant="(props.prep.printNav.variant as any) || 'default'"
|
||||
@click="props.prep.printNav?.onClick"
|
||||
>
|
||||
<Icon :name="props.prep.printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ props.prep.printNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
const props = defineProps<{
|
||||
prep: HeaderPrep
|
||||
refSearchNav?: RefSearchNav
|
||||
}>()
|
||||
|
||||
function emitSearchNavClick() {
|
||||
props.refSearchNav?.onClick()
|
||||
}
|
||||
|
||||
function onInput(event: Event) {
|
||||
props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
function btnClick() {
|
||||
props.prep?.addNav?.onClick?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="md:text-base xl:text-lg font-semibold text-gray-900">
|
||||
<Icon :name="props.prep.icon!" class="mr-2 size-4 md:size-6 align-middle" />
|
||||
{{ props.prep.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div v-if="props.refSearchNav" class="text-lg text-gray-900">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
class="sm:text-sm"
|
||||
@click="emitSearchNavClick"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="prep.addNav" class="flex items-center ms-2">
|
||||
<Button class="rounded-md border border-gray-300 text-white" @click="btnClick">
|
||||
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ prep.addNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
const props = defineProps<{
|
||||
prep: HeaderPrep
|
||||
refSearchNav: RefSearchNav
|
||||
}>()
|
||||
|
||||
function emitSearchNavClick() {
|
||||
props.refSearchNav.onClick()
|
||||
}
|
||||
|
||||
function onInput(event: Event) {
|
||||
props.refSearchNav.onInput((event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
function btnClick() {
|
||||
props.prep?.addNav?.onClick?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="flex items-center">
|
||||
<div class="ml-3 text-lg text-gray-900">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
class="w-full rounded-md border bg-white px-4 py-2 text-gray-900 sm:text-sm"
|
||||
@click="emitSearchNavClick"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="prep.addNav" class="m-2 flex items-center">
|
||||
<Button size="md" class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm" @click="btnClick">
|
||||
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ prep.addNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
Reference in New Issue
Block a user