Added tag search for events and characters

This commit is contained in:
Alexis
2024-04-18 23:02:05 +02:00
parent 4626467c3d
commit 5b6fa82d1b
24 changed files with 649 additions and 56 deletions

View File

@@ -19,7 +19,7 @@ defineProps<{
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance', '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-stone-500 hover:bg-stone-700': event.category === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe', '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-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-purple-600 hover:bg-purple-700': event.category === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs', 'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',

View File

@@ -1,16 +1,26 @@
<script lang="ts" setup> <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 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 { useCharacters } from '@/stores/CharacterStore'
import { useCalendarEvents } from '@/stores/EventStore' import { useCalendarEvents } from '@/stores/EventStore'
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue' import { useMagicKeys, useStorage, whenever } from '@vueuse/core'
import { useMagicKeys, useStorage, useTimeoutFn, whenever } from '@vueuse/core'
import { VisuallyHidden } from 'radix-vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { searchUnifier, type SearchMode } from '../Search' import { searchUnifier, type SearchMode } from '../Search'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -29,17 +39,33 @@ import {
PaginationNext, PaginationNext,
PaginationPrev PaginationPrev
} from '@/components/ui/pagination' } from '@/components/ui/pagination'
import {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
} from '@/components/ui/tags-input'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' 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' import SearchList from './lists/SearchList.vue'
const { characters } = useCharacters() const { characters } = useCharacters()
const { baseEvents } = useCalendarEvents() const { baseEvents } = useCalendarEvents()
const modalOpen = ref(false) const modalOpen = ref<boolean>(false)
const searchQuery = ref('') const searchQuery = ref<string>('')
const searchEnough = computed(() => searchQuery.value.length >= 2) // const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
const selectedEntity = useStorage('se', 'events' as SearchMode) const selectedEntity = useStorage('se', 'events' as SearchMode)
@@ -57,17 +83,18 @@ function setOrderDesc() {
// Limit // Limit
const currentPage = ref<number>(1) const currentPage = ref<number>(1)
const itemsPerPage = 20 const itemsPerPage: number = 20
const startOfList = computed(() => (currentPage.value - 1) * itemsPerPage) const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage)
const endOfList = computed(() => startOfList.value + itemsPerPage) const endOfList = computed<number>(() => startOfList.value + itemsPerPage)
function resetPage() { function resetPage() {
currentPage.value = 1 currentPage.value = 1
} }
const searchResults = computed(() => { const searchResults = computed<(Character | CalendarEvent)[]>(() => {
let results: (Character | CalendarEvent)[] = [] let results: (Character | CalendarEvent)[] = []
let dataToFilter: Character[] | CalendarEvent[] | (Character | CalendarEvent)[] let dataToFilter: Character[] | CalendarEvent[] | (Character | CalendarEvent)[]
const shouldFilterCategories = selectedCategories.value.length > 0
// Assign data to loop over and filter // Assign data to loop over and filter
// They are assigned this way for readability // They are assigned this way for readability
@@ -79,6 +106,9 @@ const searchResults = computed(() => {
dataToFilter = [...baseEvents, ...characters] dataToFilter = [...baseEvents, ...characters]
} }
/**
* TODO: Refactor the categories logic, basically extract the return out of the ifs, like above
*/
results = dataToFilter.filter((item) => { results = dataToFilter.filter((item) => {
// Filter calendar events // Filter calendar events
if (isCalendarEvent(item)) { if (isCalendarEvent(item)) {
@@ -95,7 +125,27 @@ const searchResults = computed(() => {
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(queryString) .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 // Filter characters
@@ -109,7 +159,27 @@ const searchResults = computed(() => {
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(queryString) .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() { function resetSearch() {
searchQuery.value = '' searchQuery.value = ''
resetPage()
selectedCategories.value = []
} }
const resetSearchLazy = useTimeoutFn(() => {
resetSearch()
}, 100)
function openDialog() { function openDialog() {
modalOpen.value = true modalOpen.value = true
} }
@@ -132,16 +200,49 @@ function closeDialog() {
modalOpen.value = false modalOpen.value = false
} }
function handleEntitySwitch() {
resetPage()
selectedCategories.value = []
}
// Key combos to deploy modal // Key combos to deploy modal
const keys = useMagicKeys() const keys = useMagicKeys()
whenever(keys.control_period, () => { whenever(keys.control_period, () => {
openDialog() 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> </script>
<template> <template>
<Dialog v-model:open="modalOpen" @update:open="resetSearchLazy.start"> <Dialog v-model:open="modalOpen" @update:open="resetSearch()">
<DialogTrigger> <DialogTrigger>
<Button search-slash> <Button search-slash>
<PhMagnifyingGlass size="20" weight="light" /> <PhMagnifyingGlass size="20" weight="light" />
@@ -165,7 +266,7 @@ whenever(keys.control_period, () => {
</VisuallyHidden> </VisuallyHidden>
<!-- Dialog header --> <!-- Dialog header -->
<div class="grid gap-3"> <div class="grid gap-3" id="searchForm">
<div class="relative w-full h-fit"> <div class="relative w-full h-fit">
<Input <Input
id="search" id="search"
@@ -186,7 +287,7 @@ whenever(keys.control_period, () => {
type="single" type="single"
class="justify-start" class="justify-start"
v-model="selectedEntity" v-model="selectedEntity"
@update:model-value="resetPage()" @update:model-value="handleEntitySwitch()"
> >
<ToggleGroupItem value="events" aria-label="Uniquement les évènements"> <ToggleGroupItem value="events" aria-label="Uniquement les évènements">
Évènements Évènements
@@ -198,6 +299,52 @@ whenever(keys.control_period, () => {
</div> </div>
<div class="flex items-center gap-1"> <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"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>

View File

@@ -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-lime-600 hover:bg-lime-700': r.category === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': r.category === 'mort', '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-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-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-purple-600 hover:bg-purple-700': r.category === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': r.category === 'joueurs', '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)" @click="handleJumpToDate(r.date)"
> >
<div> <div class="flex gap-2 items-center">
<h2 class="font-bold"> <h2 class="font-bold">
{{ r.title }} {{ r.title }}
</h2> </h2>
<div v-if="r.wiki"> <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> </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="opacity-75">{{ getFormattedDateTitle(r.date, true) }}</p>
<p class="text-sm italic opacity-75 flex items-center gap-1"> <p class="text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> <PhHourglassMedium size="16" weight="fill" />

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View File

@@ -9,19 +9,41 @@ export const charactersList: Character[] = [
name: 'Sulvan Trois-Barbes', name: 'Sulvan Trois-Barbes',
birth: { day: 20, month: 3, year: 3169 }, birth: { day: 20, month: 3, year: 3169 },
wiki: 'https://alexcreates.fr/leim/index.php/Sulvan_Trois-Barbes', wiki: 'https://alexcreates.fr/leim/index.php/Sulvan_Trois-Barbes',
category: 'joueur' category: 'joueur',
secondaryCategories: ['criminel', 'étincelle']
}, },
{ {
name: 'Vascylly', name: 'Vascylly',
birth: { day: 3, month: 5, year: 3181 }, birth: { day: 3, month: 5, year: 3181 },
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly', 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" // "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: 'Malik',
{ name: 'Clayron', birth: { day: 3, month: 5, year: 3178 }, category: 'joueur' }, 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 * NPC

View File

@@ -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.", "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', category: 'bénédiction',
secondaryCategories: ['mort'], 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 hidden: true
}, },
{ {

View File

@@ -11,18 +11,20 @@ export interface Character {
secondaryCategories?: CharacterCategory[] secondaryCategories?: CharacterCategory[]
wiki?: string wiki?: string
} }
export const characterCategories = [
'joueur',
'comte',
'scientifique',
'mage',
'professeur',
'criminel',
'étincelle',
'buse blanche',
'ecclésiastique',
'sentinelle'
] as const
export type CharacterCategory = export type CharacterCategory = (typeof characterCategories)[number]
| 'joueur'
| 'comte'
| 'scientifique'
| 'mage'
| 'professeur'
| 'criminel'
| 'étincelle'
| 'buse blanche'
| 'ecclésiastique'
| 'sentinelle'
export function isCharacter(object: any): object is Character { export function isCharacter(object: any): object is Character {
return 'birth' in object return 'birth' in object

View File

@@ -10,18 +10,21 @@ export interface CalendarEvent {
wiki?: string wiki?: string
} }
export type CalendarEventCategory = export const calendarEventCategories = [
| 'naissance' 'naissance',
| 'mort' 'mort',
| 'catastrophe' 'catastrophe',
| 'législation' 'législation',
| 'catastrophe-naturelle' 'catastrophe naturelle',
| 'inauguration' 'inauguration',
| 'religion' 'religion',
| 'invention' 'invention',
| 'science' 'science',
| 'bénédiction' // Great event on a global scale 'bénédiction',
| 'joueurs' 'joueurs'
]
export type CalendarEventCategory = (typeof calendarEventCategories)[number]
export function isCalendarEvent(object: any): object is CalendarEvent { export function isCalendarEvent(object: any): object is CalendarEvent {
return 'date' in object return 'date' in object

12
src/utils/Strings.ts Normal file
View 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())