Refresh table on rew record, new create tag modal, fixed reviews drawer, cleaned up creation form.

main
Eloi Zalczer 2024-06-13 16:07:16 +02:00
parent 9282cde499
commit 0602a96af5
9 changed files with 224 additions and 29 deletions

View File

@ -39,4 +39,25 @@
@apply hover:bg-gray-300;
}
}
}
.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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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="{

View File

@ -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"

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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>