Skip to content

Commit

Permalink
Merge pull request #542 from bcc-code/feature/better-contributor-ux
Browse files Browse the repository at this point in the history
add component for selecting contributors
  • Loading branch information
kkuepper authored Jan 3, 2025
2 parents 2b69c32 + 2e48830 commit 83a44b2
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 85 deletions.
4 changes: 4 additions & 0 deletions assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ html {
0px 1px 4px 0px #0000000d,
0px 0px 0px 1px #ffffff14;
}

* {
outline-color: currentColor;
}
49 changes: 30 additions & 19 deletions components/ComboSearchBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ComboboxInput,
ComboboxOption,
ComboboxOptions,
ComboboxButton,
ComboboxLabel,
} from "@headlessui/vue";
Expand All @@ -21,32 +20,44 @@ defineProps<{
const modelValue = defineModel<TOption[]>();
const search = defineModel<string>("search");
function removeOption(option: TOption) {
modelValue.value = modelValue.value?.filter((o) => o !== option);
}
</script>
<template>
<Combobox v-model="modelValue" as="div" multiple>
<ComboboxLabel v-if="label" class="type-subtitle-2 mb-1 block text-label-1">
{{ label }}
</ComboboxLabel>
<div class="relative w-full">
<ComboboxInput
class="w-full truncate rounded-lg border border-label-separator bg-background-2 px-4 py-2"
:display-value="
(options) => (options as TOption[]).map(displayValue).join(', ')
"
@change="search = $event.target.value"
/>
<div class="absolute right-3 top-1/2 flex -translate-y-1/2 items-center">
<button
v-if="modelValue?.length"
class="rounded-md p-2"
@click="modelValue = []"
<div
class="relative w-full rounded-lg border border-label-separator bg-background-2 p-2 outline-2 focus-within:outline"
>
<div class="relative flex flex-wrap gap-2">
<TransitionGroup
move-class="transition-all duration-300 ease-out"
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 scale-95"
leave-active-class="transition-all duration-300 ease-out absolute"
leave-to-class="opacity-0 scale-95"
>
<NuxtIcon name="icon.close.small" class="opacity-50" />
</button>
<ComboboxButton>
<NuxtIcon name="icon.chevron.down" />
</ComboboxButton>
<span
v-for="option in modelValue"
:key="optionKey(option)"
class="type-subtitle-2 flex gap-2 truncate rounded-md border border-label-separator bg-background-3 py-1 pl-3 pr-2"
>
{{ displayValue(option) }}
<button class="aspect-square h-full" @click="removeOption(option)">
<NuxtIcon name="icon.close.small" class="opacity-50" />
</button>
</span>
</TransitionGroup>
<ComboboxInput
class="w-full truncate bg-[transparent] px-2 focus-visible:outline-none"
placeholder="Search..."
@change="search = $event.target.value"
/>
</div>
</div>
<ComboboxOptions class="absolute z-50 rounded-xl p-1">
Expand Down
69 changes: 69 additions & 0 deletions components/ContributorSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script
setup
lang="ts"
generic="TOption extends string | number | boolean | object"
>
import type { ContributorModel } from "@bcc-code/bmm-sdk-fetch";
import { ContributorApi } from "@bcc-code/bmm-sdk-fetch";
defineProps<{
label?: string;
}>();
const modelValue = defineModel<ContributorModel[]>();
const searchTerm = ref<string>("");
const contributors = ref<ContributorModel[]>([]);
watchDebounced(
searchTerm,
async (search) => {
if (search === "" || !search) {
contributors.value = [];
return;
}
contributors.value =
await new ContributorApi().contributorSearchUnpublishedTermGet({
term: search,
});
},
{ debounce: 100, immediate: true },
);
watch(modelValue, (newValue, oldValue) => {
if (!newValue || !oldValue) return;
if (newValue !== oldValue) {
searchTerm.value = "";
contributors.value = [];
}
});
</script>

<template>
<ComboSearchBox
v-model:search="searchTerm"
v-model="modelValue"
:label="label"
:options="contributors"
:option-key="(c) => c.id"
:display-value="(option) => option.name!"
>
<template #option="{ option, selected }">
<div class="flex grow items-baseline gap-2">
<span>{{ option.name }}</span>
<div class="type-subtitle-3 flex items-baseline gap-0.5 text-label-3">
(<span>{{ option.interpretReferences }}</span
>|<span>{{ option.otherReferences }}</span
>)
</div>
<NuxtIcon
:class="[
'ml-auto',
{ 'opacity-100': selected, 'opacity-0': !selected },
]"
name="icon.checkmark"
/>
</div>
</template>
</ComboSearchBox>
</template>
70 changes: 4 additions & 66 deletions pages/lyrics/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ async function saveLyrics() {
}
}
const contributors = ref<ContributorModel[]>([]);
const contributorSearch = ref("");
const composers = ref<ContributorModel[]>();
const lyricists = ref<ContributorModel[]>();
Expand Down Expand Up @@ -96,18 +94,6 @@ watch([composers, lyricists], ([c, l]) => {
}
});
watchDebounced(
contributorSearch,
async (search) => {
if (search === "" || !search) return;
contributors.value =
await new ContributorApi().contributorSearchUnpublishedTermGet({
term: search,
});
},
{ debounce: 100, immediate: true },
);
// Delete lyrics dialog
const confirmDelete = useConfirmDialog();
confirmDelete.onConfirm(async () => {
Expand Down Expand Up @@ -169,62 +155,14 @@ function useDefaultLongCopyright() {
</header>
<div class="grid gap-8 lg:grid-cols-2 lg:grid-rows-1">
<div class="row-start-1 flex flex-col gap-6 md:col-start-2">
<ComboSearchBox
v-model:search="contributorSearch"
<ContributorSelector
v-model="lyricists"
:label="$t('track.details.text')"
:options="contributors"
:option-key="(c) => c.id"
:display-value="(option) => option.name!"
>
<template #option="{ option, selected }">
<div class="flex grow items-baseline gap-2">
<span>{{ option.name }}</span>
<div
class="type-subtitle-3 flex items-baseline gap-0.5 text-label-3"
>
<span>{{ option.interpretReferences }}</span>
·
<span>{{ option.otherReferences }}</span>
</div>
<NuxtIcon
:class="[
'ml-auto',
{ 'opacity-100': selected, 'opacity-0': !selected },
]"
name="icon.checkmark"
/>
</div>
</template>
</ComboSearchBox>
<ComboSearchBox
v-model:search="contributorSearch"
/>
<ContributorSelector
v-model="composers"
:label="$t('track.details.melody')"
:options="contributors"
:option-key="(c) => c.id"
:display-value="(option) => option.name!"
>
<template #option="{ option, selected }">
<div class="flex grow items-baseline gap-2">
<span>{{ option.name }}</span>
<div
class="type-subtitle-3 flex items-baseline gap-0.5 text-label-3"
>
<span>{{ option.interpretReferences }}</span>
·
<span>{{ option.otherReferences }}</span>
</div>
<NuxtIcon
:class="[
'ml-auto',
{ 'opacity-100': selected, 'opacity-0': !selected },
]"
name="icon.checkmark"
/>
</div>
</template>
</ComboSearchBox>
/>
<div>
<label
class="type-subtitle-2 mb-1 block text-label-1"
Expand Down

0 comments on commit 83a44b2

Please sign in to comment.