Skip to content

Commit

Permalink
feat: add tag multiply select
Browse files Browse the repository at this point in the history
update deps
  • Loading branch information
ufaboy committed Dec 22, 2024
1 parent 12a7e4f commit 85ac7ca
Show file tree
Hide file tree
Showing 9 changed files with 2,174 additions and 2,046 deletions.
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lib-vue",
"private": true,
"version": "1.14.2",
"version": "1.15.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -14,33 +14,33 @@
"lint:fix": "eslint ./src --fix"
},
"dependencies": {
"pinia": "^2.2.2",
"pinia": "^2.3.0",
"typograf": "^7.4.1",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.25.1",
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@vitejs/plugin-vue": "^4.6.2",
"@vitest/coverage-v8": "^0.33.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-tailwindcss": "^3.17.4",
"eslint-plugin-vue": "^9.27.0",
"eslint": "^9.17.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-tailwindcss": "^3.17.5",
"eslint-plugin-vue": "^9.32.0",
"happy-dom": "^10.11.2",
"msw": "^1.3.3",
"postcss": "^8.4.41",
"msw": "^1.3.5",
"postcss": "^8.4.49",
"prettier": "3.1.1",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.4",
"vite": "^5.4.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vite-plugin-pwa": "^0.16.7",
"vitest": "^0.32.4",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.0.29"
"vue-tsc": "^2.1.10"
}
}
4,003 changes: 2,009 additions & 1,994 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/assets/style/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@
}

@layer utilities {


.th-shadow {
@apply shadow-[0px_20px_30px_-12px_rgba(0, 0, 0, 0.75)]
}
Expand Down
39 changes: 12 additions & 27 deletions src/components/IconBurger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
y="6.25" />
<rect
class="rect"
width="18"
width="15"
height="1.5"
fill="currentColor"
ry="0.75"
x="3"
x="6"
y="11.25" />
<rect
class="rect"
Expand All @@ -35,38 +35,23 @@
.rect {
transform-box: fill-box;
transform-origin: 50% 50%;
fill: hsl(0 0% 98%);
}
.rect {
transition:
rotate 0.2s 0s,
translate 0.2s 0.2s;
transition: transform 0.5s, opacity 0.5s;
}
}
.icon-burger.active {
rotate: 90deg;
transition: rotate 500ms 0.4s;
.rect {
transition:
translate 0.2s,
rotate 0.2s 0.3s;
}
.rect:nth-of-type(1) {
translate: 0 333%;
rotate: -45deg;
}
.active .rect:nth-of-type(1) {
transform: rotate(45deg) translate(20%, 230%) scaleX(1.25);
}
.rect:nth-of-type(2) {
rotate: 45deg;
}
.active .rect:nth-of-type(2) {
opacity: 0;
transform: translate(0, 0) scale(0.01);
}
.rect:nth-of-type(3) {
translate: 0 -333%;
rotate: 45deg;
}
.active .rect:nth-of-type(3) {
transform: rotate(-45deg) translate(20%, -230%) scaleX(1.25);
}
</style>
114 changes: 114 additions & 0 deletions src/components/MultiSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script setup>
import { ref, computed, defineProps, defineModel, watch, nextTick } from 'vue';
const props = defineProps({
options: Array,
keyLabel: String,
keyValue: String,
});
const model = defineModel()
const searchString = ref('');
const dropdownOpen = ref(false);
const selectedOptions = ref([]);
const checkboxAll = ref(null);
const filteredOptions = computed(() => {
return props.options?.filter(option => {
return option[props.keyLabel]?.includes(searchString.value);
});
});
const modelName = computed(() => {
if (model.value?.length === props.options?.length) {
return 'All';
} else if (model.value?.length > 2) {
return `${model.value[0][props.keyLabel]}, ${model.value[1][props.keyLabel]} + ${model.value.length - 2} more`;
} else {
return model.value?.map(item => item[props.keyLabel]).join(', ');
}
});
const allSelected = computed({
get() {
return selectedOptions.value.length === props.options?.length;
},
set(value) {
selectedOptions.value = value ? props.options : [];
}
})
watch(() => selectedOptions.value, (value) => {
if (value.length === props.options.length) {
checkboxAll.value.indeterminate = false;
} else if (value.length > 0 && value.length < filteredOptions.value.length) {
checkboxAll.value.indeterminate = true;
} else {
checkboxAll.value.indeterminate = false;
}
});
function confirm() {
if (selectedOptions.value.length === props.options.length) {
model.value = undefined;
} else {
model.value = selectedOptions.value;
}
dropdownOpen.value = false;
}
function toggleAllOptions() {
if (checkboxAll.value.checked) {
selectedOptions.value = filteredOptions.value;
} else {
selectedOptions.value = [];
}
}
async function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value;
await nextTick();
if (!model.value) {
selectedOptions.value = props.options;
}
}
</script>
<template>
<div class="w-full h-full">
<select name="" id="" multiple class="xl:hidden" v-model="model">
<option v-for="(option, index) in options" :value="option" :key="index">
{{ option[keyLabel] }}
</option>
</select>
<div class="relative w-full h-full hidden xl:block">
<div class="w-full h-full input min-h-[34px] cursor-pointer truncate whitespace-nowrap"
@click="toggleDropdown">
{{ modelName }}
</div>
<div v-show="dropdownOpen"
class="absolute top-9 left-0 transition-all z-10 bg-slate-800 p-4 rounded-lg flex flex-col gap-2">
<input type="search" placeholder="Search"
class="w-full p-2 border border-gray-300 rounded-md text-black" v-model="searchString">
<label class="flex w-full cursor-pointer items-center gap-2 text-sm py-1.5" @click="toggleAllOptions">
<input ref="checkboxAll" type="checkbox" style="outline: none" class="checkbox" :value="undefined"
v-model="allSelected">
<div class="inline-block w-[calc(100%_-_24px)] truncate text-start text-sm text-white" title="All">
All
</div>
</label>
<label v-for="(option, index) in filteredOptions" :key="index"
class="flex w-full cursor-pointer items-center gap-2 text-sm py-1.5">
<input v-model="selectedOptions" type="checkbox" style="outline: none" class="checkbox"
:value="option">
<div class="inline-block w-[calc(100%_-_24px)] truncate text-start text-sm text-white"
:title="option[keyLabel]">
{{ option[keyLabel] }}
</div>
</label>
<button class="p-2 rounded-lg border hover:bg-slate-700" @click="confirm">
Confirm
</button>
</div>
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion src/composables/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function useBook() {
name: undefined,
text: undefined,
text_length: undefined,
tag: undefined,
'tag[]': undefined,
view_count: undefined,
rating: undefined,
authorName: undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface BookResponse {

interface QueryBooks extends Partial<Omit<BookTableIem, 'tags' | 'author' | 'series' | 'media'>>, BaseQuery {
[key: string]: string | number | undefined | null;
tag?: string;
'tag[]'?: string;
text?: string;
text_length?: number;
authorName?: string;
Expand Down
14 changes: 11 additions & 3 deletions src/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Toast from '@/plugins/toaster/toast';
import { REDIRECT_LOGIN_URL } from '@/constants';
import { Tag } from '@/interfaces/tag';

function getHeaders(): Headers {
const token = sessionStorage.getItem('lib-token');
Expand All @@ -12,18 +13,25 @@ function getHeaders(): Headers {
});
}

function getUrl(baseUrl: string, query?: { [key: string]: string | number | undefined | null }): URL {
function getUrl(baseUrl: string, query?: { [key: string]: string | number | undefined | null | Tag[] }): URL {
const url = new URL(baseUrl);
for (const key in query) {
const value = query[key];

if (value !== '' && value !== undefined && value !== null) {
let stringValue: string;
let stringValue: string = '';
if (['updated_at', 'last_read'].includes(key) && typeof value === 'number') {
stringValue = (new Date(value).getTime() / 1000).toString();
} else if(key === 'tag[]' && Array.isArray(value)) {
for (const item of value) {
url.searchParams.append('tag[]', item.name);
}
} else if (typeof value === 'string') {
stringValue = value;
} else stringValue = value.toString();
} else {
stringValue = value.toString();
}

url.searchParams.append(key, stringValue);
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/views/books/BookTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import TheLoader from '@/components/TheLoader.vue';
import TablePaginator from '@/components/TablePaginator.vue';
import SkeletonTableRow from '@/components/SkeletonTableRow.vue';
import TableRowEmptyResult from '@/components/TableRowEmptyResult.vue';
import MultiSelect from '@/components/MultiSelect.vue';
const props = defineProps({
scrollProgress: Number,
Expand Down Expand Up @@ -393,7 +394,7 @@ if (!series.value) getSeries({ perPage: 100, page: 1, sort: 'name' });
@search="getBooksByFilter">
</th>
<th class="th">
<select
<!-- <select
v-model="queryBooks.tag"
form="searchForm"
name="tag"
Expand All @@ -407,7 +408,14 @@ if (!series.value) getSeries({ perPage: 100, page: 1, sort: 'name' });
<option v-for="tag in tags" :key="tag.id" :value="tag.name">
{{ tag.name }}
</option>
</select>
</select> -->
<MultiSelect
v-model="queryBooks['tag[]']"
:options="tags"
keyLabel="name"
keyValue="id"
@update:modelValue="getBooksByFilter"
:class="{ hidden: shortColumns.includes('tags') }" />
</th>
<th class="th" :class="{ 'w-2': shortColumns.includes('rating') }">
<select
Expand Down

0 comments on commit 85ac7ca

Please sign in to comment.