Refresh table on rew record, new create tag modal, fixed reviews drawer, cleaned up creation form.
parent
9282cde499
commit
0602a96af5
|
|
@ -40,3 +40,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dt-paging-button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-paging-button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
.dt-paging-button:focus,
|
||||||
|
.dt-paging-button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
|
||||||
|
import { toTypedSchema } from "@vee-validate/yup";
|
||||||
|
import { Form, Field, ErrorMessage, useForm } from "vee-validate";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { pb } from "../pocketbase";
|
||||||
|
|
||||||
|
const modal = ref<typeof ModalComponent>();
|
||||||
|
const error = ref<string>();
|
||||||
|
|
||||||
|
const { meta, errors, handleSubmit, resetForm, defineField } = useForm({
|
||||||
|
validationSchema: toTypedSchema(
|
||||||
|
yup.object({
|
||||||
|
name: yup.string().required("This field is required."),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = handleSubmit(async (values) => {
|
||||||
|
try {
|
||||||
|
pb.collection("tags").create({ name: values["name"] });
|
||||||
|
resetForm();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [name, nameAttrs] = defineField("name");
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
accepted: () => true,
|
||||||
|
rejected: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAccepted() {
|
||||||
|
const ok = await submit();
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
resetForm();
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRejected() {
|
||||||
|
resetForm();
|
||||||
|
emit("rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalComponent
|
||||||
|
id="create_tag_modal"
|
||||||
|
ref="modal"
|
||||||
|
title="Create Tag"
|
||||||
|
confirm-button-text="Create"
|
||||||
|
reject-button-text="Cancel"
|
||||||
|
@accepted="onAccepted"
|
||||||
|
@rejected="onRejected"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label"> Name </label>
|
||||||
|
<input
|
||||||
|
:class="errors.name === undefined ? 'form-field' : 'form-field-error'"
|
||||||
|
v-model="name"
|
||||||
|
v-bind="nameAttrs"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<label class="text-red-500">{{ errors.name }}</label>
|
||||||
|
</div>
|
||||||
|
<label v-if="error" class="text-red-500">{{ error }}</label>
|
||||||
|
</div>
|
||||||
|
</ModalComponent>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
confirmButtonText: string;
|
||||||
|
text?: string;
|
||||||
|
modalStyle?: string;
|
||||||
|
rejectButtonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const opened = ref<boolean>(false);
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
return props.modalStyle === undefined ? "" : props.modalStyle;
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["accepted", "rejected"]);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
opened.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
opened.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function accept() {
|
||||||
|
emit("accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
function reject() {
|
||||||
|
emit("rejected");
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open, close });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="'modal overflow-y-auto ' + (opened ? 'modal-open' : '')" :id="`modal-${props.id}`">
|
||||||
|
<div :class="'modal-box ' + style">
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" @click="reject">✕</button>
|
||||||
|
</form>
|
||||||
|
<h3 class="text-lg font-bold">{{ props.title }}</h3>
|
||||||
|
<h5 v-if="props.text" class="text-update-protocol">{{ props.text }}</h5>
|
||||||
|
<slot></slot>
|
||||||
|
<div class="modal-action">
|
||||||
|
<div class="buttons-container text-end">
|
||||||
|
<button class="btn btn-outline btn-success" @click="accept">
|
||||||
|
{{ props.confirmButtonText }}
|
||||||
|
</button>
|
||||||
|
<button v-if="rejectButtonText !== undefined" class="btn btn-outline btn-success" @click="reject">
|
||||||
|
{{ props.rejectButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -4,7 +4,7 @@ import "../assets/datatable.css";
|
||||||
import DataTable from "datatables.net-vue3";
|
import DataTable from "datatables.net-vue3";
|
||||||
import DataTablesCore from "datatables.net";
|
import DataTablesCore from "datatables.net";
|
||||||
import "datatables.net-select";
|
import "datatables.net-select";
|
||||||
import { PropType } from "vue";
|
import { PropType, ref } from "vue";
|
||||||
import RatingField from "./RatingField.vue";
|
import RatingField from "./RatingField.vue";
|
||||||
|
|
||||||
DataTable.use(DataTablesCore);
|
DataTable.use(DataTablesCore);
|
||||||
|
|
@ -19,18 +19,27 @@ const columns = [
|
||||||
{ data: "average_rating", title: "" },
|
{ data: "average_rating", title: "" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const table = ref<typeof DataTable>();
|
||||||
|
|
||||||
function onRowSelected(e, dt, type, indexes) {
|
function onRowSelected(e, dt, type, indexes) {
|
||||||
if (indexes.length != 0 && props.data !== undefined) {
|
if (indexes.length != 0 && props.data !== undefined) {
|
||||||
emit("restaurantSelected", props.data[indexes[0]]);
|
emit("restaurantSelected", props.data[indexes[0]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deselectAll() {
|
||||||
|
table.value?.dt.rows({ selected: true }).deselect();
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits(["restaurantSelected"]);
|
const emit = defineEmits(["restaurantSelected"]);
|
||||||
|
|
||||||
|
defineExpose({ deselectAll });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DataTable
|
<DataTable
|
||||||
class="table"
|
class="table"
|
||||||
|
ref="table"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="props.data"
|
:data="props.data"
|
||||||
:options="{
|
:options="{
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ function open() {
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
opened.value = false;
|
opened.value = false;
|
||||||
|
emit("closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPublishReview(rating: number, text: string) {
|
function onPublishReview(rating: number, text: string) {
|
||||||
|
|
@ -36,6 +37,9 @@ function onPublishReview(rating: number, text: string) {
|
||||||
reviewsList.value?.refresh();
|
reviewsList.value?.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
closed: () => true,
|
||||||
|
});
|
||||||
defineExpose({ open, close });
|
defineExpose({ open, close });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -43,7 +47,7 @@ defineExpose({ open, close });
|
||||||
<div class="drawer drawer-end z-[10000]">
|
<div class="drawer drawer-end z-[10000]">
|
||||||
<input id="my-drawer" type="checkbox" class="drawer-toggle" :checked="opened" />
|
<input id="my-drawer" type="checkbox" class="drawer-toggle" :checked="opened" />
|
||||||
<div class="drawer-side w-full">
|
<div class="drawer-side w-full">
|
||||||
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
<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="menu p-4 space-y-4 w-full lg:w-2/6 min-h-full bg-base-200 text-base-content">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
@click="close"
|
@click="close"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { type PropType } from "vue";
|
import { type PropType } from "vue";
|
||||||
|
|
||||||
|
import CreateTagModal from "./CreateTagModal.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: () => {} },
|
||||||
|
|
@ -14,6 +16,8 @@ const emit = defineEmits(["updateModelValue"]);
|
||||||
|
|
||||||
const query = ref<string>("");
|
const query = ref<string>("");
|
||||||
|
|
||||||
|
const createTagModal = ref<typeof CreateTagModal>();
|
||||||
|
|
||||||
function onBadgeClicked(value: string) {
|
function onBadgeClicked(value: string) {
|
||||||
if (model.value?.includes(value)) {
|
if (model.value?.includes(value)) {
|
||||||
model.value = model.value.filter((val) => val != value);
|
model.value = model.value.filter((val) => val != value);
|
||||||
|
|
@ -46,6 +50,10 @@ function onClear() {
|
||||||
model.value = [];
|
model.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCreateNewTag() {
|
||||||
|
createTagModal.value?.show();
|
||||||
|
}
|
||||||
|
|
||||||
function filteredOptions() {
|
function filteredOptions() {
|
||||||
if (!query.value) return props.options;
|
if (!query.value) return props.options;
|
||||||
const lcquery = query.value.toLocaleLowerCase();
|
const lcquery = query.value.toLocaleLowerCase();
|
||||||
|
|
@ -54,7 +62,7 @@ function filteredOptions() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="dropdown" class="combobox dropdown">
|
<div ref="dropdown" class="w-full combobox dropdown">
|
||||||
<div
|
<div
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -78,6 +86,7 @@ function filteredOptions() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul class="menu dropdown-content z-10 w-full rounded-lg border border-neutral-content bg-base-100 p-0 shadow">
|
<ul class="menu dropdown-content z-10 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">
|
<li v-for="option in filteredOptions()" :key="option" role="option" class="m-0 p-0">
|
||||||
<button :class="{ active: model?.includes(option) }" @click="onOptionClicked(option)">
|
<button :class="{ active: model?.includes(option) }" @click="onOptionClicked(option)">
|
||||||
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
||||||
|
|
@ -86,4 +95,6 @@ function filteredOptions() {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CreateTagModal ref="createTagModal" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -47,25 +47,6 @@ h2 {
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import * as yup from "yup";
|
||||||
import "@/assets/form.css";
|
import "@/assets/form.css";
|
||||||
|
|
||||||
import { pb } from "../pocketbase.ts";
|
import { pb } from "../pocketbase.ts";
|
||||||
import MultiSelectField from "../components/MultiSelectField.vue";
|
import TagsField from "../components/TagsField.vue";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
||||||
|
|
@ -59,6 +59,10 @@ const [price, priceAttrs] = defineField("price");
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
availableTags.value = await pb.collection("tags").getFullList();
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
|
|
||||||
|
pb.collection("tags").subscribe("*", async (e) => {
|
||||||
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -88,7 +92,7 @@ onMounted(async () => {
|
||||||
<div class="flex flex-row flex-nowrap w-96">
|
<div class="flex flex-row flex-nowrap w-96">
|
||||||
<div class="mx-5 my-4 w-full">
|
<div class="mx-5 my-4 w-full">
|
||||||
<label class="form-label"> Tags </label>
|
<label class="form-label"> Tags </label>
|
||||||
<MultiSelectField v-model="tags" v-bind="tagsAttrs" :options="tagsNames" />
|
<TagsField v-model="tags" v-bind="tagsAttrs" :options="tagsNames" />
|
||||||
<br />
|
<br />
|
||||||
<label class="text-red-500">{{ errors.tags }}</label>
|
<label class="text-red-500">{{ errors.tags }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ import { average } from "../utils/math";
|
||||||
const data = ref<Object[]>([]);
|
const data = ref<Object[]>([]);
|
||||||
|
|
||||||
const drawer = ref<typeof ReviewsDrawer>();
|
const drawer = ref<typeof ReviewsDrawer>();
|
||||||
|
const table = ref<typeof RestaurantsTable>();
|
||||||
|
|
||||||
const selectedRestaurant = ref<Object>();
|
const selectedRestaurant = ref<Object>();
|
||||||
|
|
||||||
onMounted(async () => {
|
async function load() {
|
||||||
const restaurants = await pb.collection("restaurants").getFullList({ expand: "tags,reviews_via_restaurant" });
|
const restaurants = await pb.collection("restaurants").getFullList({ expand: "tags,reviews_via_restaurant" });
|
||||||
|
|
||||||
restaurants.forEach((restaurant) => {
|
restaurants.forEach((restaurant) => {
|
||||||
|
|
@ -34,25 +35,41 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(restaurants);
|
|
||||||
data.value = restaurants;
|
data.value = restaurants;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await load();
|
||||||
|
|
||||||
|
pb.collection("restaurants").subscribe("*", (e) => {
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
pb.collection("reviews").subscribe("*", (e) => {
|
||||||
|
load();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function onRestaurantSelected(restaurant: Object) {
|
function onRestaurantSelected(restaurant: Object) {
|
||||||
selectedRestaurant.value = restaurant;
|
selectedRestaurant.value = restaurant;
|
||||||
drawer.value?.open();
|
drawer.value?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDrawerClosed() {
|
||||||
|
console.log("closed");
|
||||||
|
table.value?.deselectAll();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col-reverse sm:flex-row">
|
<div class="flex flex-col-reverse sm:flex-row">
|
||||||
<div class="w-full flex-1">
|
<div class="w-full flex-1">
|
||||||
<RestaurantsTable @restaurant-selected="onRestaurantSelected" :data="data" />
|
<RestaurantsTable ref="table" @restaurant-selected="onRestaurantSelected" :data="data" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 h-3/6 lg:h-full">
|
<div class="flex-1 h-3/6 lg:h-full">
|
||||||
<RestaurantsMap />
|
<RestaurantsMap />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ReviewsDrawer :restaurant="selectedRestaurant" ref="drawer" />
|
<ReviewsDrawer @closed="onDrawerClosed" :restaurant="selectedRestaurant" ref="drawer" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue