Working on configuration panel for random restaurant
parent
fc7eb805b8
commit
1ebbe3579e
|
|
@ -25,6 +25,7 @@
|
||||||
"vee-validate": "^4.13.0",
|
"vee-validate": "^4.13.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.2",
|
"vue-router": "^4.3.2",
|
||||||
|
"vuetify": "^3.6.10",
|
||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -2668,6 +2669,39 @@
|
||||||
"vue": "^3.2.0"
|
"vue": "^3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vuetify": {
|
||||||
|
"version": "3.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.10.tgz",
|
||||||
|
"integrity": "sha512-Myd9+EFq4Gmu61yKPNVS0QdGQkcZ9cHom27wuvRw7jgDxM+X4MT9BwQRk/Dt1q3G3JlK8oh+ZYyq5Ps/Z73cMg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >=14.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.7",
|
||||||
|
"vite-plugin-vuetify": ">=1.0.0",
|
||||||
|
"vue": "^3.3.0",
|
||||||
|
"vue-i18n": "^9.0.0",
|
||||||
|
"webpack-plugin-vuetify": ">=2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue-i18n": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"vee-validate": "^4.13.0",
|
"vee-validate": "^4.13.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.2",
|
"vue-router": "^4.3.2",
|
||||||
|
"vuetify": "^3.6.10",
|
||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
.form-field {
|
.form-field {
|
||||||
@apply input;
|
@apply input;
|
||||||
|
@apply border-solid;
|
||||||
@apply input-bordered;
|
@apply input-bordered;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
.form-select {
|
.form-select {
|
||||||
@apply select;
|
@apply select;
|
||||||
@apply select-bordered;
|
@apply select-bordered;
|
||||||
|
@apply border-solid;
|
||||||
@apply my-0;
|
@apply my-0;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +26,7 @@
|
||||||
.form-field-error {
|
.form-field-error {
|
||||||
@apply input;
|
@apply input;
|
||||||
@apply input-bordered;
|
@apply input-bordered;
|
||||||
|
@apply border-solid;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply input-error;
|
@apply input-error;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +34,7 @@
|
||||||
.form-select-error {
|
.form-select-error {
|
||||||
@apply select;
|
@apply select;
|
||||||
@apply select-bordered;
|
@apply select-bordered;
|
||||||
|
@apply border-solid;
|
||||||
@apply my-0;
|
@apply my-0;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply input-error;
|
@apply input-error;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { type PropType } from "vue";
|
||||||
|
|
||||||
|
import CreateTagModal from "./CreateTagModal.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: { type: Array as PropType<string[]> },
|
||||||
|
labels: { type: Map as PropType<Map<string, string>>, default: () => {} },
|
||||||
|
clearable: { type: Boolean, default: true },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const model = defineModel<string[]>();
|
||||||
|
|
||||||
|
const query = ref<string>("");
|
||||||
|
|
||||||
|
const createTagModal = ref<typeof CreateTagModal>();
|
||||||
|
|
||||||
|
function onBadgeClicked(value: string) {
|
||||||
|
if (model.value?.includes(value)) {
|
||||||
|
model.value = model.value.filter((val) => val != value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOptionClicked(value: string) {
|
||||||
|
if (model.value === undefined) {
|
||||||
|
model.value = [value];
|
||||||
|
} else if (model.value?.includes(value)) {
|
||||||
|
model.value = model.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
|
model.value = [...model.value, value];
|
||||||
|
}
|
||||||
|
|
||||||
|
query.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showClear() {
|
||||||
|
if (!props.clearable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Array.isArray(model.value) && model.value?.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
model.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredOptions() {
|
||||||
|
if (!query.value) return props.options;
|
||||||
|
const lcquery = query.value.toLocaleLowerCase();
|
||||||
|
return props.options?.filter((opt) => opt.toLocaleLowerCase().startsWith(lcquery));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="dropdown" class="w-full combobox dropdown">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
'input join input-bordered flex h-fit min-h-[3rem] w-full flex-row items-stretch px-0 focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 ' +
|
||||||
|
(props.disabled ? 'input-disabled' : '')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="mx-2 flex flex-wrap items-center">
|
||||||
|
<div v-for="(value, idx) in model" :key="`${value}-${idx}`" class="badge m-1">
|
||||||
|
<button class="mr-2" @click="onBadgeClicked(value)">
|
||||||
|
<font-awesome-icon icon="xmark" />
|
||||||
|
</button>
|
||||||
|
{{ labels?.get(value) ?? value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="query"
|
||||||
|
class="input join-item h-auto min-w-[8ch] flex-1 border-0 p-0 focus:outline-0"
|
||||||
|
role="combobox"
|
||||||
|
type="text"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-show="showClear"
|
||||||
|
:class="'btn join-item btn-ghost h-auto self-stretch ' + (props.disabled ? 'btn-disabled' : '')"
|
||||||
|
@click="onClear"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="xmark" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-if="!props.disabled"
|
||||||
|
class="menu flex-nowrap dropdown-content overflow-y-auto z-10 max-h-96 w-full rounded-lg border border-neutral-content bg-base-100 p-0 shadow"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
<li v-for="option in filteredOptions()" :key="option" role="option" class="m-0 p-0">
|
||||||
|
<button :class="{ active: model?.includes(option) }" @click="onOptionClicked(option)">
|
||||||
|
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
||||||
|
{{ labels?.get(option) ?? option }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateTagModal ref="createTagModal" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.input-disabled {
|
||||||
|
background-color: gray !important;
|
||||||
|
opacity: 0.2;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background-color: gray !important;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-disabled {
|
||||||
|
background-color: gray;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import RandomConfigurationPanel from "./RandomConfigurationPanel.vue";
|
||||||
|
|
||||||
|
const opened = ref<boolean>(false);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
opened.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
opened.value = false;
|
||||||
|
emit("closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPreferencesSaved() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
closed: () => true,
|
||||||
|
});
|
||||||
|
defineExpose({ open, close });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="drawer drawer-end z-[10000]">
|
||||||
|
<input id="my-drawer" type="checkbox" class="drawer-toggle" :checked="opened" />
|
||||||
|
<div class="drawer-side w-full">
|
||||||
|
<label @click.prevent="close" for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
|
<div class="menu p-4 space-y-4 w-full lg:w-2/6 min-h-full bg-base-200 text-base-content">
|
||||||
|
<div class="flex flex-row w-full">
|
||||||
|
<font-awesome-icon
|
||||||
|
@click="close"
|
||||||
|
class="btn btn-circle btn-sm text-gray-500 bg-transparent"
|
||||||
|
icon="fa-solid fa-xmark"
|
||||||
|
/>
|
||||||
|
<h3 class="text-lg font-bold ml-5 self-center">Configure filters</h3>
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RandomConfigurationPanel @preferences-saved="onPreferencesSaved" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
|
import TagsField from "./TagsField.vue";
|
||||||
|
import { pb } from "../pocketbase";
|
||||||
|
import Tag from "../models/tag";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import MultiSelectField from "./MultiSelectField.vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRestaurantsStore } from "../stores/restaurants";
|
||||||
|
import { useForm } from "vee-validate";
|
||||||
|
import { toTypedSchema } from "@vee-validate/yup";
|
||||||
|
import Restaurant from "../models/restaurant";
|
||||||
|
import { filterValues } from "../utils/filters";
|
||||||
|
|
||||||
|
const priceTicks = {
|
||||||
|
1: "€",
|
||||||
|
2: "€€",
|
||||||
|
3: "€€€",
|
||||||
|
4: "€€€€",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { restaurants } = storeToRefs(useRestaurantsStore());
|
||||||
|
|
||||||
|
const restaurantOptions = computed(() => {
|
||||||
|
return restaurants.value.map((restaurant) => restaurant.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const restaurantLabels = computed(() => {
|
||||||
|
const labels: Map<string, string> = new Map();
|
||||||
|
restaurants.value.forEach((restaurant) => {
|
||||||
|
labels.set(restaurant.id, restaurant.name);
|
||||||
|
});
|
||||||
|
return labels;
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableTags = ref<Tag[]>([]);
|
||||||
|
|
||||||
|
const tagsNames = computed(() => {
|
||||||
|
return availableTags.value.map((tag) => tag.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { values, defineField } = useForm({
|
||||||
|
validationSchema: toTypedSchema(
|
||||||
|
yup.object({
|
||||||
|
priceRangeEnabled: yup.boolean(),
|
||||||
|
includeTagsEnabled: yup.boolean(),
|
||||||
|
excludeTagsEnabled: yup.boolean(),
|
||||||
|
includeRestaurantsEnabled: yup.boolean(),
|
||||||
|
excludeRestaurantsEnabled: yup.boolean(),
|
||||||
|
priceRange: yup.array().of(yup.number()).min(2).max(2),
|
||||||
|
includedTags: yup.array().of(yup.string()),
|
||||||
|
excludedTags: yup.array().of(yup.string()),
|
||||||
|
includedRestaurants: yup.array().of(yup.string()),
|
||||||
|
excludedRestaurants: yup.array().of(yup.string()),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredRestaurants = ref<number>(restaurants.value.length);
|
||||||
|
|
||||||
|
watch(values, async (newval, oldval) => {
|
||||||
|
const filters: ((_: Restaurant) => boolean)[] = [];
|
||||||
|
|
||||||
|
if (values.excludeRestaurantsEnabled && values.excludedRestaurants) {
|
||||||
|
filters.push(filterValues.bind(undefined, "id", values.excludedRestaurants, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredRestaurants.value = useRestaurantsStore().getFilteredRestaurants(filters).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [priceRangeEnabled, priceRangeEnabledAttrs] = defineField("priceRangeEnabled");
|
||||||
|
const [includeTagsEnabled, includeTagsEnabledAttrs] = defineField("includeTagsEnabled");
|
||||||
|
const [excludeTagsEnabled, excludeTagsEnabledAttrs] = defineField("excludeTagsEnabled");
|
||||||
|
const [includeRestaurantsEnabled, includeRestaurantsEnabledAttrs] = defineField("includeRestaurantsEnabled");
|
||||||
|
const [excludeRestaurantsEnabled, excludeRestaurantsEnabledAttrs] = defineField("excludeRestaurantsEnabled");
|
||||||
|
const [priceRange, priceRangeAttrs] = defineField("priceRange");
|
||||||
|
const [includedTags, includedTagsAttrs] = defineField("includedTags");
|
||||||
|
const [excludedTags, excludedTagsAttrs] = defineField("excludedTags");
|
||||||
|
const [includedRestaurants, includedRestaurantsAttrs] = defineField("includedRestaurants");
|
||||||
|
const [excludedRestaurants, excludedRestaurantsAttrs] = defineField("excludedRestaurants");
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
|
|
||||||
|
pb.collection("tags").subscribe("*", async (e) => {
|
||||||
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
preferencesSaved: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSavePreferences() {
|
||||||
|
emit("preferencesSaved");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col place-content-center items-center h-full">
|
||||||
|
<div class="flex flex-row flex-nowrap w-full">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label mb-2">
|
||||||
|
<input type="checkbox" v-model="priceRangeEnabled" class="checkbox border-solid" />
|
||||||
|
Price range
|
||||||
|
</label>
|
||||||
|
<v-range-slider
|
||||||
|
v-model="priceRange"
|
||||||
|
show-ticks="always"
|
||||||
|
:min="1"
|
||||||
|
:max="4"
|
||||||
|
:step="1"
|
||||||
|
:ticks="priceTicks"
|
||||||
|
track-size="8"
|
||||||
|
:disabled="!priceRangeEnabled"
|
||||||
|
strict
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-full">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label mb-2">
|
||||||
|
<input type="checkbox" v-model="includeTagsEnabled" class="checkbox border-solid" />
|
||||||
|
Include tags
|
||||||
|
</label>
|
||||||
|
<TagsField v-model="includedTags" :create="false" :options="tagsNames" :disabled="!includeTagsEnabled" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-full">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label mb-2">
|
||||||
|
<input type="checkbox" v-model="excludeTagsEnabled" class="checkbox border-solid" />
|
||||||
|
Exclude tags
|
||||||
|
</label>
|
||||||
|
<TagsField v-model="excludedTags" :create="false" :options="tagsNames" :disabled="!excludeTagsEnabled" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-full">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label mb-2">
|
||||||
|
<input type="checkbox" v-model="includeRestaurantsEnabled" class="checkbox border-solid" />
|
||||||
|
Include restaurants
|
||||||
|
</label>
|
||||||
|
<MultiSelectField
|
||||||
|
v-model="includedRestaurants"
|
||||||
|
:options="restaurantOptions"
|
||||||
|
:labels="restaurantLabels"
|
||||||
|
:disabled="!includeRestaurantsEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-full">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label mb-2">
|
||||||
|
<input type="checkbox" v-model="excludeRestaurantsEnabled" class="checkbox border-solid" />
|
||||||
|
Exclude restaurants
|
||||||
|
</label>
|
||||||
|
<MultiSelectField
|
||||||
|
v-model="excludedRestaurants"
|
||||||
|
:options="restaurantOptions"
|
||||||
|
:labels="restaurantLabels"
|
||||||
|
:disabled="!excludeRestaurantsEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<div>
|
||||||
|
{{ filteredRestaurants }} filtered restaurants
|
||||||
|
<button @click="onSavePreferences" class="btn btn-outline w-full">Save preferences</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -3,100 +3,37 @@ import { ref } from "vue";
|
||||||
import { type PropType } from "vue";
|
import { type PropType } from "vue";
|
||||||
|
|
||||||
import CreateTagModal from "./CreateTagModal.vue";
|
import CreateTagModal from "./CreateTagModal.vue";
|
||||||
|
import MultiSelectField from "./MultiSelectField.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
options: { type: Array as PropType<string[]> },
|
options: { type: Array as PropType<string[]> },
|
||||||
labels: { type: Map as PropType<Map<string, string>>, default: () => {} },
|
labels: { type: Map as PropType<Map<string, string>>, default: () => {} },
|
||||||
clearable: { type: Boolean, default: true },
|
clearable: { type: Boolean, default: true },
|
||||||
|
create: { type: Boolean, default: true },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const model = defineModel<string[]>();
|
const model = defineModel<string[]>();
|
||||||
|
|
||||||
const emit = defineEmits(["updateModelValue"]);
|
|
||||||
|
|
||||||
const query = ref<string>("");
|
|
||||||
|
|
||||||
const createTagModal = ref<typeof CreateTagModal>();
|
const createTagModal = ref<typeof CreateTagModal>();
|
||||||
|
|
||||||
function onBadgeClicked(value: string) {
|
|
||||||
if (model.value?.includes(value)) {
|
|
||||||
model.value = model.value.filter((val) => val != value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOptionClicked(value: string) {
|
|
||||||
if (model.value === undefined) {
|
|
||||||
model.value = [value];
|
|
||||||
} else if (model.value?.includes(value)) {
|
|
||||||
model.value = model.value?.filter((val) => val !== value);
|
|
||||||
} else {
|
|
||||||
model.value = [...model.value, value];
|
|
||||||
}
|
|
||||||
|
|
||||||
query.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function showClear() {
|
|
||||||
if (!props.clearable) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Array.isArray(model.value) && model.value?.length == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClear() {
|
|
||||||
model.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCreateNewTag() {
|
function onCreateNewTag() {
|
||||||
|
if (props.create) {
|
||||||
createTagModal.value?.show();
|
createTagModal.value?.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function filteredOptions() {
|
|
||||||
if (!query.value) return props.options;
|
|
||||||
const lcquery = query.value.toLocaleLowerCase();
|
|
||||||
return props.options?.filter((opt) => opt.toLocaleLowerCase().startsWith(lcquery));
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="dropdown" class="w-full combobox dropdown">
|
<MultiSelectField
|
||||||
<div
|
v-model="model"
|
||||||
class="input input-bordered flex h-fit min-h-[3rem] w-full flex-row items-stretch px-0 focus-within:outline focus-within:outline-2 focus-within:outline-offset-2"
|
:options="props.options"
|
||||||
|
:labels="props.labels"
|
||||||
|
:clearable="props.clearable"
|
||||||
|
:disabled="props.disabled"
|
||||||
>
|
>
|
||||||
<div class="mx-2 flex flex-wrap items-center">
|
<li v-if="props.create"><button @click="onCreateNewTag">Create new tag...</button></li>
|
||||||
<div v-for="(value, idx) in model" :key="`${value}-${idx}`" class="badge m-1">
|
</MultiSelectField>
|
||||||
<button class="mr-2" @click="onBadgeClicked(value)">
|
|
||||||
<font-awesome-icon icon="xmark" />
|
|
||||||
</button>
|
|
||||||
{{ labels?.get(value) ?? value }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref="input"
|
|
||||||
v-model="query"
|
|
||||||
class="input input-ghost h-auto min-w-[8ch] flex-1 border-0 p-0 focus:outline-0"
|
|
||||||
role="combobox"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<button v-show="showClear" class="btn btn-ghost h-auto self-stretch" @click="onClear">
|
|
||||||
<font-awesome-icon icon="xmark" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="menu flex-nowrap dropdown-content overflow-y-auto z-10 max-h-96 w-full rounded-lg border border-neutral-content bg-base-100 p-0 shadow"
|
|
||||||
>
|
|
||||||
<li><button @click="onCreateNewTag">Create new tag...</button></li>
|
|
||||||
<li v-for="option in filteredOptions()" :key="option" role="option" class="m-0 p-0">
|
|
||||||
<button :class="{ active: model?.includes(option) }" @click="onOptionClicked(option)">
|
|
||||||
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
|
||||||
{{ option }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CreateTagModal ref="createTagModal" />
|
<CreateTagModal ref="createTagModal" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
10
src/main.ts
10
src/main.ts
|
|
@ -1,5 +1,10 @@
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
|
// Vuetify
|
||||||
|
import { createVuetify } from "vuetify";
|
||||||
|
// import VRangeSlider from "vuetify/components";
|
||||||
|
import * as components from "vuetify/components";
|
||||||
|
|
||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
|
|
@ -13,10 +18,15 @@ library.add(faBars, faUser, faXmark, faCheck, faGitAlt, faExclamationCircle);
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
components,
|
||||||
|
});
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
app.use(vuetify);
|
||||||
|
|
||||||
app.component("font-awesome-icon", FontAwesomeIcon);
|
app.component("font-awesome-icon", FontAwesomeIcon);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,31 @@ export const useRestaurantsStore = defineStore("restaurants", {
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
getRandomRestaurant(state) {
|
getRandomRestaurant(state) {
|
||||||
return () => _.sample(state.restaurants);
|
return (filters: ((_: Restaurant) => boolean)[]) => {
|
||||||
|
const filtered = state.restaurants.filter((restaurant) => {
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (!filter(restaurant)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return _.sample(filtered);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getFilteredRestaurants(state) {
|
||||||
|
return (filters: ((_: Restaurant) => boolean)[]) => {
|
||||||
|
const filtered = state.restaurants.filter((restaurant) => {
|
||||||
|
console.log(restaurant);
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (!filter(restaurant)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function filterValues(field: string, values: any[], negative: boolean = false, obj: Object) {
|
||||||
|
console.log("fielf", field, "values", values, "negative", negative, "obj", obj);
|
||||||
|
if (negative) {
|
||||||
|
return !values.includes(obj[field]);
|
||||||
|
} else {
|
||||||
|
return values.includes(obj[field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import "@/assets/form.css";
|
||||||
|
|
||||||
|
import RandomConfigurationDrawer from "../components/RandomConfigurationDrawer.vue";
|
||||||
|
|
||||||
import { useRestaurantsStore } from "../stores/restaurants.ts";
|
import { useRestaurantsStore } from "../stores/restaurants.ts";
|
||||||
|
import Restaurant from "../models/restaurant.ts";
|
||||||
|
|
||||||
const restaurant = ref<string>();
|
const restaurant = ref<Restaurant>();
|
||||||
|
|
||||||
const store = useRestaurantsStore();
|
const store = useRestaurantsStore();
|
||||||
|
|
||||||
function getRandomRestaurant() {
|
function getRandomRestaurant() {
|
||||||
restaurant.value = store.getRandomRestaurant();
|
restaurant.value = store.getRandomRestaurant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const drawer = ref<typeof RandomConfigurationDrawer>();
|
||||||
|
|
||||||
|
function onConfigureClicked() {
|
||||||
|
drawer.value?.open();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -18,4 +28,14 @@ function getRandomRestaurant() {
|
||||||
<p class="">{{ restaurant?.name }}</p>
|
<p class="">{{ restaurant?.name }}</p>
|
||||||
<button class="btn btn-outline btn-primary mt-10" @click="getRandomRestaurant">Get random restaurant</button>
|
<button class="btn btn-outline btn-primary mt-10" @click="getRandomRestaurant">Get random restaurant</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" @click="onConfigureClicked">Configure</button>
|
||||||
|
|
||||||
|
<RandomConfigurationDrawer ref="drawer" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn {
|
||||||
|
@apply border-solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue