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 DataTablesCore from "datatables.net";
|
||||
import "datatables.net-select";
|
||||
import { PropType } from "vue";
|
||||
import { PropType, ref } from "vue";
|
||||
import RatingField from "./RatingField.vue";
|
||||
|
||||
DataTable.use(DataTablesCore);
|
||||
|
|
@ -19,18 +19,27 @@ const columns = [
|
|||
{ data: "average_rating", title: "" },
|
||||
];
|
||||
|
||||
const table = ref<typeof DataTable>();
|
||||
|
||||
function onRowSelected(e, dt, type, indexes) {
|
||||
if (indexes.length != 0 && props.data !== undefined) {
|
||||
emit("restaurantSelected", props.data[indexes[0]]);
|
||||
}
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
table.value?.dt.rows({ selected: true }).deselect();
|
||||
}
|
||||
|
||||
const emit = defineEmits(["restaurantSelected"]);
|
||||
|
||||
defineExpose({ deselectAll });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DataTable
|
||||
class="table"
|
||||
ref="table"
|
||||
:columns="columns"
|
||||
:data="props.data"
|
||||
:options="{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function open() {
|
|||
|
||||
function close() {
|
||||
opened.value = false;
|
||||
emit("closed");
|
||||
}
|
||||
|
||||
function onPublishReview(rating: number, text: string) {
|
||||
|
|
@ -36,6 +37,9 @@ function onPublishReview(rating: number, text: string) {
|
|||
reviewsList.value?.refresh();
|
||||
}
|
||||
|
||||
const emit = defineEmits({
|
||||
closed: () => true,
|
||||
});
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
|
|
@ -43,7 +47,7 @@ defineExpose({ open, close });
|
|||
<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 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">
|
||||
<font-awesome-icon
|
||||
@click="close"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
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: () => {} },
|
||||
|
|
@ -14,6 +16,8 @@ const emit = defineEmits(["updateModelValue"]);
|
|||
|
||||
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);
|
||||
|
|
@ -46,6 +50,10 @@ function onClear() {
|
|||
model.value = [];
|
||||
}
|
||||
|
||||
function onCreateNewTag() {
|
||||
createTagModal.value?.show();
|
||||
}
|
||||
|
||||
function filteredOptions() {
|
||||
if (!query.value) return props.options;
|
||||
const lcquery = query.value.toLocaleLowerCase();
|
||||
|
|
@ -54,7 +62,7 @@ function filteredOptions() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="dropdown" class="combobox dropdown">
|
||||
<div ref="dropdown" class="w-full combobox dropdown">
|
||||
<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"
|
||||
>
|
||||
|
|
@ -78,6 +86,7 @@ function filteredOptions() {
|
|||
</button>
|
||||
</div>
|
||||
<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">
|
||||
<button :class="{ active: model?.includes(option) }" @click="onOptionClicked(option)">
|
||||
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
||||
|
|
@ -86,4 +95,6 @@ function filteredOptions() {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<CreateTagModal ref="createTagModal" />
|
||||
</template>
|
||||
|
|
@ -47,25 +47,6 @@ h2 {
|
|||
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 {
|
||||
padding: 2em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import * as yup from "yup";
|
|||
import "@/assets/form.css";
|
||||
|
||||
import { pb } from "../pocketbase.ts";
|
||||
import MultiSelectField from "../components/MultiSelectField.vue";
|
||||
import TagsField from "../components/TagsField.vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
||||
|
|
@ -59,6 +59,10 @@ const [price, priceAttrs] = defineField("price");
|
|||
|
||||
onMounted(async () => {
|
||||
availableTags.value = await pb.collection("tags").getFullList();
|
||||
|
||||
pb.collection("tags").subscribe("*", async (e) => {
|
||||
availableTags.value = await pb.collection("tags").getFullList();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -88,7 +92,7 @@ onMounted(async () => {
|
|||
<div class="flex flex-row flex-nowrap w-96">
|
||||
<div class="mx-5 my-4 w-full">
|
||||
<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 />
|
||||
<label class="text-red-500">{{ errors.tags }}</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ import { average } from "../utils/math";
|
|||
const data = ref<Object[]>([]);
|
||||
|
||||
const drawer = ref<typeof ReviewsDrawer>();
|
||||
const table = ref<typeof RestaurantsTable>();
|
||||
|
||||
const selectedRestaurant = ref<Object>();
|
||||
|
||||
onMounted(async () => {
|
||||
async function load() {
|
||||
const restaurants = await pb.collection("restaurants").getFullList({ expand: "tags,reviews_via_restaurant" });
|
||||
|
||||
restaurants.forEach((restaurant) => {
|
||||
|
|
@ -34,25 +35,41 @@ onMounted(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
console.log(restaurants);
|
||||
data.value = restaurants;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
|
||||
pb.collection("restaurants").subscribe("*", (e) => {
|
||||
load();
|
||||
});
|
||||
|
||||
pb.collection("reviews").subscribe("*", (e) => {
|
||||
load();
|
||||
});
|
||||
});
|
||||
|
||||
function onRestaurantSelected(restaurant: Object) {
|
||||
selectedRestaurant.value = restaurant;
|
||||
drawer.value?.open();
|
||||
}
|
||||
|
||||
function onDrawerClosed() {
|
||||
console.log("closed");
|
||||
table.value?.deselectAll();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col-reverse sm:flex-row">
|
||||
<div class="w-full flex-1">
|
||||
<RestaurantsTable @restaurant-selected="onRestaurantSelected" :data="data" />
|
||||
<RestaurantsTable ref="table" @restaurant-selected="onRestaurantSelected" :data="data" />
|
||||
</div>
|
||||
<div class="flex-1 h-3/6 lg:h-full">
|
||||
<RestaurantsMap />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReviewsDrawer :restaurant="selectedRestaurant" ref="drawer" />
|
||||
<ReviewsDrawer @closed="onDrawerClosed" :restaurant="selectedRestaurant" ref="drawer" />
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in New Issue