Skip to content

Commit

Permalink
Merge pull request #84
Browse files Browse the repository at this point in the history
feature/no-ref/add-tag-multiselect
  • Loading branch information
botmaster authored Feb 7, 2024
2 parents 67ba02b + d48249e commit b762d36
Show file tree
Hide file tree
Showing 22 changed files with 494 additions and 240 deletions.
10 changes: 0 additions & 10 deletions assets/scss/base/_forms.scss
Original file line number Diff line number Diff line change
@@ -1,10 +0,0 @@
// Select
select {
@apply block w-full rounded-md border-0 py-1.5 text-body-txt shadow-sm ring-1 ring-inset ring-polarnight-nord-3
focus:ring-2 focus:ring-inset focus:ring-accent sm:max-w-xs sm:text-sm sm:leading-6;
}

input[type="text"] {
@apply block w-full rounded-md border-0 py-1.5 text-body-txt shadow-sm ring-1 ring-inset ring-polarnight-nord-3
focus:ring-2 focus:ring-inset focus:ring-accent sm:max-w-xs sm:text-sm sm:leading-6;
}
2 changes: 2 additions & 0 deletions assets/scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@
@layer components {
@import 'components/index';
}


2 changes: 2 additions & 0 deletions assets/scss/vendors/_index.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@

@forward "lenis";
@forward "tailwindcss-form";

14 changes: 14 additions & 0 deletions assets/scss/vendors/_tailwindcss-form.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Select
.form-select, .form-multiselect {
@apply block w-full rounded-md border-0 py-1.5 text-body-txt shadow-sm ring-1 ring-inset ring-polarnight-nord-3
focus:ring-2 focus:ring-inset focus:ring-accent sm:max-w-xs sm:text-sm sm:leading-6;
}

input[type="text"].form-input, .input-text {
@apply block w-full rounded-md border-0 py-1.5 text-body-txt shadow-sm ring-1 ring-inset ring-polarnight-nord-3
focus:ring-2 focus:ring-inset focus:ring-accent sm:max-w-xs sm:text-sm sm:leading-6;
}

input[type="checkbox"] {
@apply rounded border-b-polarnight-nord-0 text-body-txt shadow-sm focus:border-accent focus:ring focus:ring-offset-0 focus:ring-accent focus:ring-opacity-50;
}
89 changes: 89 additions & 0 deletions components/ArticleListActionBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script setup lang="ts">
// Define props
import type { IOption } from '~/components/MultiSelectTag.vue';
defineProps<{
tags: IOption[]
pending: boolean
}>();
// Define emits
const emit = defineEmits(['clearFilters']);
const { t } = useI18n();
// Define model
const selectedOptions = defineModel<IOption[]>('selectedOptions', { required: true });
const search = defineModel<string>('search', { required: true });
const status = defineModel<string>('status', { required: true });
const sort = defineModel<string>('sort', { required: true });
// Computed - Has any filters
const hasFilters = computed<boolean>(() => {
return status.value !== '' || search.value !== '';
});
</script>

<template>
<div class="flex flex-col flex-wrap gap-x-6 gap-y-1.5 lg:flex-row lg:items-end">
<div class="lg:mr-8">
<input
v-model.lazy="search" autocomplete="search" name="search" type="text"
placeholder="Search an article" class="form-input min-w-64"
>
</div>

<div>
<label class="mb-1 block" for="selectStatus">{{ t('pages.readings.filters.statusLabel') }}</label>
<select id="selectStatus" v-model="status" class="form-select">
<option value="">
{{ t('pages.readings.filters.status.all') }}
</option>
<option value="To read">
{{ t('pages.readings.filters.status.toRead') }}
</option>
<option value="Read">
{{ t('pages.readings.filters.status.read') }}
</option>
<option value="Reading">
{{ t('pages.readings.filters.status.inProgress') }}
</option>
<option value="Canceled">
{{ t('pages.readings.filters.status.canceled') }}
</option>
</select>
</div>

<div>
<MultiSelectTag v-model="selectedOptions" :options="tags" />
</div>

<div>
<label class="mb-1 block" for="selectSort">{{ t('pages.readings.sort.sortLabel') }}</label>
<select id="selectSort" v-model="sort" class="form-select">
<option value="Created time">
{{ t('pages.readings.sort.createdTime') }}
</option>
<option value="Last edited time">
{{ t('pages.readings.sort.lastEditedTime') }}
</option>
<option value="Name">
{{ t('pages.readings.sort.name') }}
</option>
<option value="Score">
{{ t('pages.readings.sort.score') }}
</option>
</select>
</div>

<div v-if="hasFilters" class="leading-none lg:mb-2">
<button class="inline-flex items-center" @click="emit('clearFilters')">
<Icon class="mr-1.5" name="material-symbols:cancel" />
{{ t('common.clearFilters') }}
</button>
</div>
<Transition name="fade">
<AppLoader v-if="pending" class="m-1 text-2xl" />
</Transition>
</div>
</template>
160 changes: 160 additions & 0 deletions components/MultiSelectTag.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<script lang="ts" setup>
import { Combobox, ComboboxButton, ComboboxInput, ComboboxLabel, ComboboxOption, ComboboxOptions } from '@headlessui/vue';
// Interfaces
export interface IOption {
id: string
name: string
color: 'blue' | 'brown' | 'default' | 'gray' | 'green' | 'orange' | 'pink' | 'purple' | 'red' | 'yellow'
}
// Props
const props = defineProps<{
options: IOption[]
}>();
// Model
const selectedOptions = defineModel<IOption[]>({ required: true });
const query = ref('');
const filteredOption = computed(() =>
query.value === ''
? props.options
: props.options.filter(option =>
option.name
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.value.toLowerCase().replace(/\s+/g, '')),
),
);
function displayValue(options: IOption[]) {
return options.map(option => option.name).join(', ');
}
</script>

<template>
<Combobox v-model="selectedOptions" as="div" multiple nullable by="id">
<ComboboxLabel class="mb-1 block">
Tags
</ComboboxLabel>
<div class="multi-select form-multiselect">
<div
class="multi-select__values-container"
>
<p class="multi-select__value">
<span v-if="selectedOptions.length === 0" class="multi-select__label">Select an option</span> <span
v-else
>{{ displayValue(selectedOptions) }}</span>
</p>
</div>

<ComboboxButton
class="multi-select__button" tabindex="0"
/>

<ComboboxOptions
class="multi-select__options"
as="div"
>
<div class="multi-select__options-header">
<ComboboxInput
class="multi-select__search-input"
placeholder="Search..."
@change="query = $event.target.value"
/>
</div>
<ul class="multi-select__option-list" data-lenis-prevent>
<li
v-if="filteredOption.length === 0 && query !== ''"
class="multi-select__nothing-found"
>
Nothing found.
</li>
<ComboboxOption
v-for="option in filteredOption" :key="option.id" v-slot="{ active, selected }"
:value="option" as="template"
>
<li
class="multi-select__option" :aria-selected="selected" :class="{ active }"
>
<input class="form-checkbox" type="checkbox" :checked="selected"><span
class="badge badge--is-small badge--is-neutral"
:class="[selected && 'font-semibold']"
>{{ option.name }}</span>
</li>
</ComboboxOption>
</ul>
<div class="multi-select__options-footer">
<div class="flex gap-1.5">
<span class="text-xs">
{{ selectedOptions.length }} / {{ props.options.length }} selected
</span>
<button v-if="selectedOptions.length > 0" class="ml-auto text-xs" @click="selectedOptions = []">
Clear selection
</button>
</div>
</div>
</ComboboxOptions>
</div>
</Combobox>
</template>

<style scoped lang="scss">
.multi-select {
@apply w-full relative flex items-center bg-white min-w-48 p-0;
&__values-container {
@apply min-w-0 max-w-64 flex-grow;
padding-top: 0.375rem;
padding-bottom: 0.375rem;
padding-left: 0.75rem;
}
&__value {
@apply truncate;
}
&__button {
@apply ml-auto w-8 inline-block leading-none self-stretch;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
}
&__options {
@apply absolute left-0 top-[100%] z-10 mt-1 w-full rounded-md bg-white text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm;
}
&__options-header {
@apply px-2 py-2;
}
&__search-input {
@apply w-full px-3 py-1.5 text-body-txt border-0 rounded-md shadow-sm;
}
&__option-list {
@apply relative overflow-auto max-h-60;
}
&__options-footer {
@apply px-2 py-2;
}
&__option {
@apply relative flex items-center gap-2 cursor-default select-none py-1.5 px-3;
&.active {
@apply bg-accent text-accent-content;
}
}
&__nothing-found {
@apply relative cursor-default select-none px-4 py-2;
}
}
</style>
2 changes: 1 addition & 1 deletion components/ScrobbleListItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Size, type Track } from '~/types/types';
import { Size, type Track } from '~/types/last-fm/types';
// Props type definition
interface Props {
Expand Down
34 changes: 34 additions & 0 deletions components/TestComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup>
import { ref } from 'vue';
import {
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue';
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
];
const selectedPeople = ref([people[0], people[1]]);
</script>

<template>
<Combobox v-model="selectedPeople" as="div" multiple>
<ul v-if="selectedPeople.length > 0">
<li v-for="person in selectedPeople" :key="person.id">
{{ person.name }}
</li>
</ul>
<ComboboxInput />
<ComboboxOptions>
<ComboboxOption v-for="person in people" :key="person.id" :value="person">
{{ person.name }}
</ComboboxOption>
</ComboboxOptions>
</Combobox>
</template>
2 changes: 1 addition & 1 deletion components/shared/AppLoader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<style scoped>
.app-loader {
--uib-size: 1em;
--uib-color: black;
--uib-color: var(--color-body-text-hex, #000);
--uib-speed: 2s;
position: relative;
width: var(--uib-size);
Expand Down
3 changes: 3 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"common": {
"clearFilters": "Clear filters",
"go": "Go",
"goToPage": "Go to page",
"languages": "Language",
"loadMore": "Load more",
"no": "No",
"noData": "No data",
"yes": "Yes"
},
"layouts": {
Expand Down Expand Up @@ -46,6 +48,7 @@
},
"statusLabel": "Status"
},
"filtersTooRestrictive": "🤔 Filters too restrictive ?",
"sort": {
"createdTime": "Created Time",
"lastEditedTime": "Last edited time",
Expand Down
3 changes: 3 additions & 0 deletions locales/fr.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"common": {
"clearFilters": "Effacer les filtres",
"go": "Allez",
"goToPage": "Aller à la page",
"languages": "Langue",
"loadMore": "Charger la suite",
"no": "Non",
"noData": "Pas de données",
"yes": "Oui"
},
"layouts": {
Expand Down Expand Up @@ -46,6 +48,7 @@
},
"statusLabel": "Statut"
},
"filtersTooRestrictive": "🤔 Filtres trop restrictifs ?",
"sort": {
"createdTime": "Date de création",
"lastEditedTime": "Date de dernière édition",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@fontsource/inter": "^5.0.16",
"@fontsource/rubik": "^5.0.18",
"@headlessui/tailwindcss": "^0.2.0",
"@notionhq/client": "^2.2.14",
"@studio-freight/lenis": "^1.0.34",
"@vueuse/components": "^10.7.2",
Expand Down
3 changes: 2 additions & 1 deletion pages/lab.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { v4 as uuidv4 } from 'uuid';
import type { IPage, Recenttracks, Track } from '~/types/types';
import type { Recenttracks, Track } from '~/types/last-fm/types';
import type { IPage } from '~/types/types';
definePageMeta({
layout: false,
Expand Down
Loading

0 comments on commit b762d36

Please sign in to comment.