Added tag search for events and characters
This commit is contained in:
@@ -19,7 +19,7 @@ defineProps<{
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe-naturelle',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { isCharacter, type Character } from '@/models/Characters'
|
||||
import {
|
||||
isCharacter,
|
||||
type Character,
|
||||
characterCategories,
|
||||
type CharacterCategory
|
||||
} from '@/models/Characters'
|
||||
import type { LeimDateOrder } from '@/models/Date'
|
||||
import { isCalendarEvent, type CalendarEvent } from '@/models/Events'
|
||||
import {
|
||||
isCalendarEvent,
|
||||
type CalendarEvent,
|
||||
calendarEventCategories,
|
||||
type CalendarEventCategory
|
||||
} from '@/models/Events'
|
||||
import { capitalize } from '@/utils/Strings'
|
||||
import { useCharacters } from '@/stores/CharacterStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import { useMagicKeys, useStorage, useTimeoutFn, whenever } from '@vueuse/core'
|
||||
import { VisuallyHidden } from 'radix-vue'
|
||||
import { useMagicKeys, useStorage, whenever } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { searchUnifier, type SearchMode } from '../Search'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -29,17 +39,33 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrev
|
||||
} from '@/components/ui/pagination'
|
||||
import {
|
||||
TagsInput,
|
||||
TagsInputInput,
|
||||
TagsInputItem,
|
||||
TagsInputItemDelete,
|
||||
TagsInputItemText
|
||||
} from '@/components/ui/tags-input'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import {
|
||||
ComboboxAnchor,
|
||||
ComboboxInput,
|
||||
ComboboxPortal,
|
||||
ComboboxRoot,
|
||||
VisuallyHidden
|
||||
} from 'radix-vue'
|
||||
|
||||
import SearchList from './lists/SearchList.vue'
|
||||
|
||||
const { characters } = useCharacters()
|
||||
const { baseEvents } = useCalendarEvents()
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const modalOpen = ref<boolean>(false)
|
||||
|
||||
const searchQuery = ref('')
|
||||
const searchEnough = computed(() => searchQuery.value.length >= 2)
|
||||
const searchQuery = ref<string>('')
|
||||
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
|
||||
|
||||
const selectedEntity = useStorage('se', 'events' as SearchMode)
|
||||
|
||||
@@ -57,17 +83,18 @@ function setOrderDesc() {
|
||||
|
||||
// Limit
|
||||
const currentPage = ref<number>(1)
|
||||
const itemsPerPage = 20
|
||||
const startOfList = computed(() => (currentPage.value - 1) * itemsPerPage)
|
||||
const endOfList = computed(() => startOfList.value + itemsPerPage)
|
||||
const itemsPerPage: number = 20
|
||||
const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage)
|
||||
const endOfList = computed<number>(() => startOfList.value + itemsPerPage)
|
||||
|
||||
function resetPage() {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const searchResults = computed(() => {
|
||||
const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
let results: (Character | CalendarEvent)[] = []
|
||||
let dataToFilter: Character[] | CalendarEvent[] | (Character | CalendarEvent)[]
|
||||
const shouldFilterCategories = selectedCategories.value.length > 0
|
||||
|
||||
// Assign data to loop over and filter
|
||||
// They are assigned this way for readability
|
||||
@@ -79,6 +106,9 @@ const searchResults = computed(() => {
|
||||
dataToFilter = [...baseEvents, ...characters]
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Refactor the categories logic, basically extract the return out of the ifs, like above
|
||||
*/
|
||||
results = dataToFilter.filter((item) => {
|
||||
// Filter calendar events
|
||||
if (isCalendarEvent(item)) {
|
||||
@@ -95,7 +125,27 @@ const searchResults = computed(() => {
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
return hitTitle || hitDesc
|
||||
if (!shouldFilterCategories) {
|
||||
return hitTitle || hitDesc
|
||||
}
|
||||
|
||||
// Handle categories logic
|
||||
let hitCategories: boolean = false
|
||||
let allCategories: CalendarEventCategory[] = []
|
||||
|
||||
if (item.category) {
|
||||
allCategories.push(item.category)
|
||||
}
|
||||
|
||||
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
|
||||
allCategories.push(...item.secondaryCategories)
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat)
|
||||
})
|
||||
|
||||
return (hitTitle || hitDesc) && hitCategories
|
||||
}
|
||||
|
||||
// Filter characters
|
||||
@@ -109,7 +159,27 @@ const searchResults = computed(() => {
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
return hitTitle
|
||||
if (!shouldFilterCategories) {
|
||||
return hitTitle
|
||||
}
|
||||
|
||||
// Handle categories logic
|
||||
let hitCategories: boolean = false
|
||||
let allCategories: CalendarEventCategory[] = []
|
||||
|
||||
if (item.category) {
|
||||
allCategories.push(item.category)
|
||||
}
|
||||
|
||||
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
|
||||
allCategories.push(...item.secondaryCategories)
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat)
|
||||
})
|
||||
|
||||
return hitTitle && hitCategories
|
||||
}
|
||||
})
|
||||
|
||||
@@ -118,12 +188,10 @@ const searchResults = computed(() => {
|
||||
|
||||
function resetSearch() {
|
||||
searchQuery.value = ''
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
const resetSearchLazy = useTimeoutFn(() => {
|
||||
resetSearch()
|
||||
}, 100)
|
||||
|
||||
function openDialog() {
|
||||
modalOpen.value = true
|
||||
}
|
||||
@@ -132,16 +200,49 @@ function closeDialog() {
|
||||
modalOpen.value = false
|
||||
}
|
||||
|
||||
function handleEntitySwitch() {
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
// Key combos to deploy modal
|
||||
const keys = useMagicKeys()
|
||||
|
||||
whenever(keys.control_period, () => {
|
||||
openDialog()
|
||||
})
|
||||
|
||||
// Categories
|
||||
const currentCategories = computed(() => {
|
||||
if (selectedEntity.value === 'characters') {
|
||||
return [...characterCategories]
|
||||
} else {
|
||||
return [...calendarEventCategories]
|
||||
}
|
||||
})
|
||||
|
||||
const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([])
|
||||
const categoryFilterOpened = ref<boolean>(false)
|
||||
const searchCategory = ref<string>('')
|
||||
|
||||
const filteredFrameworks = computed(() =>
|
||||
currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
|
||||
)
|
||||
|
||||
function handleCategorySelect(e: any) {
|
||||
if (typeof e.detail.value === 'string') {
|
||||
searchCategory.value = ''
|
||||
selectedCategories.value.push(e.detail.value)
|
||||
}
|
||||
|
||||
if (filteredFrameworks.value.length === 0) {
|
||||
categoryFilterOpened.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="modalOpen" @update:open="resetSearchLazy.start">
|
||||
<Dialog v-model:open="modalOpen" @update:open="resetSearch()">
|
||||
<DialogTrigger>
|
||||
<Button search-slash>
|
||||
<PhMagnifyingGlass size="20" weight="light" />
|
||||
@@ -165,7 +266,7 @@ whenever(keys.control_period, () => {
|
||||
</VisuallyHidden>
|
||||
|
||||
<!-- Dialog header -->
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-3" id="searchForm">
|
||||
<div class="relative w-full h-fit">
|
||||
<Input
|
||||
id="search"
|
||||
@@ -186,7 +287,7 @@ whenever(keys.control_period, () => {
|
||||
type="single"
|
||||
class="justify-start"
|
||||
v-model="selectedEntity"
|
||||
@update:model-value="resetPage()"
|
||||
@update:model-value="handleEntitySwitch()"
|
||||
>
|
||||
<ToggleGroupItem value="events" aria-label="Uniquement les évènements">
|
||||
Évènements
|
||||
@@ -198,6 +299,52 @@ whenever(keys.control_period, () => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<TagsInput class="px-0 gap-0 w-52" :model-value="selectedCategories">
|
||||
<div class="flex gap-2 flex-wrap items-center px-3">
|
||||
<TagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
|
||||
<TagsInputItemText class="capitalize" />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
</div>
|
||||
|
||||
<ComboboxRoot
|
||||
v-model="selectedCategories"
|
||||
v-model:open="categoryFilterOpened"
|
||||
v-model:searchTerm="searchCategory"
|
||||
class="w-full"
|
||||
>
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxInput placeholder="Catégories" as-child>
|
||||
<TagsInputInput
|
||||
class="w-full px-3"
|
||||
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</ComboboxInput>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxPortal :to="'#searchForm'">
|
||||
<CommandList
|
||||
position="popper"
|
||||
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
|
||||
:dismissable="true"
|
||||
>
|
||||
<CommandEmpty />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="framework in filteredFrameworks"
|
||||
:key="framework"
|
||||
:value="framework"
|
||||
@select.prevent="handleCategorySelect"
|
||||
>
|
||||
{{ capitalize(framework) }}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</ComboboxPortal>
|
||||
</ComboboxRoot>
|
||||
</TagsInput>
|
||||
|
||||
<TooltipProvider :delayDuration="250">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
|
||||
@@ -72,7 +72,7 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
|
||||
'text-white bg-lime-600 hover:bg-lime-700': r.category === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': r.category === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': r.category === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': r.category === 'catastrophe-naturelle',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': r.category === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': r.category === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': r.category === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': r.category === 'joueurs',
|
||||
@@ -83,16 +83,21 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
|
||||
}"
|
||||
@click="handleJumpToDate(r.date)"
|
||||
>
|
||||
<div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="font-bold">
|
||||
{{ r.title }}
|
||||
</h2>
|
||||
<div v-if="r.wiki">
|
||||
<a :href="r.wiki" target="_blank" title="Voir sur le Wiki">Page wiki</a>
|
||||
<Button variant="link" size="xs" as-child class="text-inherit">
|
||||
<a :href="r.wiki" target="_blank">
|
||||
Wiki
|
||||
<PhArrowSquareOut size="16" weight="fill" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-1 space-y-1">
|
||||
<div class="mb-1 flex gap-4 items-center">
|
||||
<p class="opacity-75">{{ getFormattedDateTitle(r.date, true) }}</p>
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" />
|
||||
|
||||
35
src/components/ui/command/Command.vue
Normal file
35
src/components/ui/command/Command.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue'
|
||||
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
open: true,
|
||||
modelValue: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits<ComboboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxRoot>
|
||||
</template>
|
||||
23
src/components/ui/command/CommandDialog.vue
Normal file
23
src/components/ui/command/CommandDialog.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useForwardPropsEmits } from 'radix-vue'
|
||||
import type { DialogRootEmits, DialogRootProps } from 'radix-vue'
|
||||
import Command from './Command.vue'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-bind="forwarded">
|
||||
<DialogContent class="overflow-hidden p-0 shadow-lg">
|
||||
<Command
|
||||
class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||
>
|
||||
<slot />
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
20
src/components/ui/command/CommandEmpty.vue
Normal file
20
src/components/ui/command/CommandEmpty.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxEmptyProps } from 'radix-vue'
|
||||
import { ComboboxEmpty } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
|
||||
<slot />
|
||||
</ComboboxEmpty>
|
||||
</template>
|
||||
36
src/components/ui/command/CommandGroup.vue
Normal file
36
src/components/ui/command/CommandGroup.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxGroupProps } from 'radix-vue'
|
||||
import { ComboboxGroup, ComboboxLabel } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxGroupProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
heading?: string
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
</ComboboxLabel>
|
||||
<slot />
|
||||
</ComboboxGroup>
|
||||
</template>
|
||||
40
src/components/ui/command/CommandInput.vue
Normal file
40
src/components/ui/command/CommandInput.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = defineProps<
|
||||
ComboboxInputProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
|
||||
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ComboboxInput
|
||||
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||
auto-focus
|
||||
:class="
|
||||
cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
31
src/components/ui/command/CommandItem.vue
Normal file
31
src/components/ui/command/CommandItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue'
|
||||
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<ComboboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxItem>
|
||||
</template>
|
||||
33
src/components/ui/command/CommandList.vue
Normal file
33
src/components/ui/command/CommandList.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue'
|
||||
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
dismissable: false
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<ComboboxContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxContent
|
||||
v-bind="forwarded"
|
||||
:class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)"
|
||||
>
|
||||
<div role="presentation">
|
||||
<slot />
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
</template>
|
||||
20
src/components/ui/command/CommandSeparator.vue
Normal file
20
src/components/ui/command/CommandSeparator.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import type { ComboboxSeparatorProps } from 'radix-vue'
|
||||
import { ComboboxSeparator } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxSeparator v-bind="delegatedProps" :class="cn('-mx-1 h-px bg-border', props.class)">
|
||||
<slot />
|
||||
</ComboboxSeparator>
|
||||
</template>
|
||||
14
src/components/ui/command/CommandShortcut.vue
Normal file
14
src/components/ui/command/CommandShortcut.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
9
src/components/ui/command/index.ts
Normal file
9
src/components/ui/command/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as Command } from './Command.vue'
|
||||
export { default as CommandDialog } from './CommandDialog.vue'
|
||||
export { default as CommandEmpty } from './CommandEmpty.vue'
|
||||
export { default as CommandGroup } from './CommandGroup.vue'
|
||||
export { default as CommandInput } from './CommandInput.vue'
|
||||
export { default as CommandItem } from './CommandItem.vue'
|
||||
export { default as CommandList } from './CommandList.vue'
|
||||
export { default as CommandSeparator } from './CommandSeparator.vue'
|
||||
export { default as CommandShortcut } from './CommandShortcut.vue'
|
||||
35
src/components/ui/tags-input/TagsInput.vue
Normal file
35
src/components/ui/tags-input/TagsInput.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import {
|
||||
TagsInputRoot,
|
||||
type TagsInputRootEmits,
|
||||
type TagsInputRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<TagsInputRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
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>
|
||||
22
src/components/ui/tags-input/TagsInputInput.vue
Normal file
22
src/components/ui/tags-input/TagsInputInput.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TagsInputInput, type TagsInputInputProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputInputProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
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>
|
||||
30
src/components/ui/tags-input/TagsInputItem.vue
Normal file
30
src/components/ui/tags-input/TagsInputItem.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TagsInputItem, type TagsInputItemProps, useForwardProps } from 'radix-vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
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>
|
||||
27
src/components/ui/tags-input/TagsInputItemDelete.vue
Normal file
27
src/components/ui/tags-input/TagsInputItemDelete.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TagsInputItemDelete, type TagsInputItemDeleteProps, useForwardProps } from 'radix-vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemDeleteProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
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>
|
||||
22
src/components/ui/tags-input/TagsInputItemText.vue
Normal file
22
src/components/ui/tags-input/TagsInputItemText.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
import { TagsInputItemText, type TagsInputItemTextProps, useForwardProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemTextProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputItemText
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('py-1 px-2 text-sm rounded bg-transparent', props.class)"
|
||||
/>
|
||||
</template>
|
||||
5
src/components/ui/tags-input/index.ts
Normal file
5
src/components/ui/tags-input/index.ts
Normal file
@@ -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'
|
||||
@@ -9,19 +9,41 @@ export const charactersList: Character[] = [
|
||||
name: 'Sulvan Trois-Barbes',
|
||||
birth: { day: 20, month: 3, year: 3169 },
|
||||
wiki: 'https://alexcreates.fr/leim/index.php/Sulvan_Trois-Barbes',
|
||||
category: 'joueur'
|
||||
category: 'joueur',
|
||||
secondaryCategories: ['criminel', 'étincelle']
|
||||
},
|
||||
{
|
||||
name: 'Vascylly',
|
||||
birth: { day: 3, month: 5, year: 3181 },
|
||||
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly',
|
||||
category: 'joueur'
|
||||
category: 'joueur',
|
||||
secondaryCategories: ['mage']
|
||||
},
|
||||
{
|
||||
name: 'Tara Belyus',
|
||||
birth: { day: 14, month: 9, year: 3186 },
|
||||
category: 'joueur',
|
||||
secondaryCategories: ['mage']
|
||||
},
|
||||
{ name: 'Tara Belyus', birth: { day: 14, month: 9, year: 3186 }, category: 'joueur' },
|
||||
// "Les Cloches de Cantane"
|
||||
{ name: 'Malik', birth: { day: 22, month: 8, year: 3181 }, category: 'joueur' },
|
||||
{ name: 'Elie', birth: { day: 5, month: 6, year: 3192 }, category: 'joueur' },
|
||||
{ name: 'Clayron', birth: { day: 3, month: 5, year: 3178 }, category: 'joueur' },
|
||||
{
|
||||
name: 'Malik',
|
||||
birth: { day: 22, month: 8, year: 3181 },
|
||||
category: 'joueur',
|
||||
secondaryCategories: ['scientifique']
|
||||
},
|
||||
{
|
||||
name: 'Elie',
|
||||
birth: { day: 5, month: 6, year: 3192 },
|
||||
category: 'joueur',
|
||||
secondaryCategories: ['ecclésiastique']
|
||||
},
|
||||
{
|
||||
name: 'Clayron',
|
||||
birth: { day: 3, month: 5, year: 3178 },
|
||||
category: 'joueur',
|
||||
secondaryCategories: ['sentinelle']
|
||||
},
|
||||
|
||||
/**
|
||||
* NPC
|
||||
|
||||
@@ -9,7 +9,7 @@ export const regularEvents: CalendarEvent[] = [
|
||||
"Le léviathan Tyhos rend l'âme après un combat de plusieurs années contre Lystos, le dieu du Soleil.",
|
||||
category: 'bénédiction',
|
||||
secondaryCategories: ['mort'],
|
||||
wiki: 'https://alexcreates.fr/leim/index.php/Alliance_Kald%C3%A9lienne#Trait%C3%A9_de_Kadel',
|
||||
wiki: 'https://alexcreates.fr/leim/index.php/Tyhos',
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,18 +11,20 @@ export interface Character {
|
||||
secondaryCategories?: CharacterCategory[]
|
||||
wiki?: string
|
||||
}
|
||||
export const characterCategories = [
|
||||
'joueur',
|
||||
'comte',
|
||||
'scientifique',
|
||||
'mage',
|
||||
'professeur',
|
||||
'criminel',
|
||||
'étincelle',
|
||||
'buse blanche',
|
||||
'ecclésiastique',
|
||||
'sentinelle'
|
||||
] as const
|
||||
|
||||
export type CharacterCategory =
|
||||
| 'joueur'
|
||||
| 'comte'
|
||||
| 'scientifique'
|
||||
| 'mage'
|
||||
| 'professeur'
|
||||
| 'criminel'
|
||||
| 'étincelle'
|
||||
| 'buse blanche'
|
||||
| 'ecclésiastique'
|
||||
| 'sentinelle'
|
||||
export type CharacterCategory = (typeof characterCategories)[number]
|
||||
|
||||
export function isCharacter(object: any): object is Character {
|
||||
return 'birth' in object
|
||||
|
||||
@@ -10,18 +10,21 @@ export interface CalendarEvent {
|
||||
wiki?: string
|
||||
}
|
||||
|
||||
export type CalendarEventCategory =
|
||||
| 'naissance'
|
||||
| 'mort'
|
||||
| 'catastrophe'
|
||||
| 'législation'
|
||||
| 'catastrophe-naturelle'
|
||||
| 'inauguration'
|
||||
| 'religion'
|
||||
| 'invention'
|
||||
| 'science'
|
||||
| 'bénédiction' // Great event on a global scale
|
||||
| 'joueurs'
|
||||
export const calendarEventCategories = [
|
||||
'naissance',
|
||||
'mort',
|
||||
'catastrophe',
|
||||
'législation',
|
||||
'catastrophe naturelle',
|
||||
'inauguration',
|
||||
'religion',
|
||||
'invention',
|
||||
'science',
|
||||
'bénédiction',
|
||||
'joueurs'
|
||||
]
|
||||
|
||||
export type CalendarEventCategory = (typeof calendarEventCategories)[number]
|
||||
|
||||
export function isCalendarEvent(object: any): object is CalendarEvent {
|
||||
return 'date' in object
|
||||
|
||||
12
src/utils/Strings.ts
Normal file
12
src/utils/Strings.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Capitalizes first letters of words in string.
|
||||
* @param {string} str String to be modified
|
||||
* @param {boolean=false} lower Whether all other letters should be lowercased
|
||||
* @return {string}
|
||||
* @usage
|
||||
* capitalize('fix this string'); // -> 'Fix This String'
|
||||
* capitalize('javaSCrIPT'); // -> 'JavaSCrIPT'
|
||||
* capitalize('javaSCrIPT', true); // -> 'Javascript'
|
||||
*/
|
||||
export const capitalize = (str: string, lower: boolean = false) =>
|
||||
(lower ? str.toLowerCase() : str).replace(/(?:^|\s|["'([{])+\S/g, (match) => match.toUpperCase())
|
||||
Reference in New Issue
Block a user