Reworked create page to add location, added restaurants store.
parent
0602a96af5
commit
ad56794b95
|
|
@ -18,6 +18,7 @@
|
||||||
"datatables.net-select": "^2.0.3",
|
"datatables.net-select": "^2.0.3",
|
||||||
"datatables.net-vue3": "^3.0.1",
|
"datatables.net-vue3": "^3.0.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pocketbase": "^0.21.3",
|
"pocketbase": "^0.21.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.2",
|
"prettier-plugin-tailwindcss": "^0.6.2",
|
||||||
|
|
@ -1637,6 +1638,11 @@
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.2.2",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"datatables.net-select": "^2.0.3",
|
"datatables.net-select": "^2.0.3",
|
||||||
"datatables.net-vue3": "^3.0.1",
|
"datatables.net-vue3": "^3.0.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pocketbase": "^0.21.3",
|
"pocketbase": "^0.21.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.2",
|
"prettier-plugin-tailwindcss": "^0.6.2",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import NavigationBar from "./components/NavigationBar.vue";
|
import NavigationBar from "./components/NavigationBar.vue";
|
||||||
|
import { useRestaurantsStore } from "./stores/restaurants";
|
||||||
|
|
||||||
|
useRestaurantsStore().init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
|
||||||
|
import { toTypedSchema } from "@vee-validate/yup";
|
||||||
|
import { useForm } from "vee-validate";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { pb } from "../pocketbase";
|
||||||
|
import TagsField from "./TagsField.vue";
|
||||||
|
import LatLng from "../models/map";
|
||||||
|
import Tag from "../models/tag";
|
||||||
|
|
||||||
|
const modal = ref<typeof ModalComponent>();
|
||||||
|
|
||||||
|
const { errors, handleSubmit, defineField, resetForm } = useForm({
|
||||||
|
validationSchema: toTypedSchema(
|
||||||
|
yup.object({
|
||||||
|
name: yup.string().required("This field is required."),
|
||||||
|
tags: yup.array().of(yup.string()),
|
||||||
|
price: yup.string().required("This field is required."),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = handleSubmit((values) => {
|
||||||
|
const tags: string[] = [];
|
||||||
|
|
||||||
|
values.tags?.forEach((name) => {
|
||||||
|
const tag = availableTags.value.filter((tag) => tag.name === name)[0];
|
||||||
|
if (tag !== undefined) {
|
||||||
|
tags.push(tag.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
pb.collection("restaurants").create({
|
||||||
|
name: values.name,
|
||||||
|
price_range: values.price,
|
||||||
|
tags: tags,
|
||||||
|
latitude: position.value?.lat,
|
||||||
|
longitude: position.value?.lng,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit("succeeded", `Restaurant ${values.name} created successfully!`);
|
||||||
|
} catch (err) {
|
||||||
|
emit("failed", err);
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableTags = ref<Tag[]>([]);
|
||||||
|
|
||||||
|
const tagsNames = computed(() => {
|
||||||
|
return availableTags.value.map((tag) => tag.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
|
|
||||||
|
pb.collection("tags").subscribe("*", async (e) => {
|
||||||
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const [name, nameAttrs] = defineField("name");
|
||||||
|
const [tags, tagsAttrs] = defineField("tags");
|
||||||
|
const [price, priceAttrs] = defineField("price");
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
succeeded: (message: string) => true,
|
||||||
|
failed: (message: string) => true,
|
||||||
|
rejected: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAccepted() {
|
||||||
|
await submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRejected() {
|
||||||
|
resetForm();
|
||||||
|
emit("rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = ref<LatLng>();
|
||||||
|
|
||||||
|
function setRestaurantPosition(latlng: LatLng) {
|
||||||
|
position.value = latlng;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide, setRestaurantPosition });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalComponent
|
||||||
|
id="create_restaurant_modal"
|
||||||
|
ref="modal"
|
||||||
|
title="Create restaurant"
|
||||||
|
confirm-button-text="Create"
|
||||||
|
reject-button-text="Cancel"
|
||||||
|
@accepted="onAccepted"
|
||||||
|
@rejected="onRejected"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col place-content-center items-center h-full">
|
||||||
|
<div class="flex flex-row flex-nowrap w-96">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-96">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Tags </label>
|
||||||
|
<TagsField v-model="tags" v-bind="tagsAttrs" :options="tagsNames" />
|
||||||
|
<br />
|
||||||
|
<label class="text-red-500">{{ errors.tags }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-96">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Price </label>
|
||||||
|
<select
|
||||||
|
:class="errors.price === undefined ? 'form-select' : 'form-select-error'"
|
||||||
|
v-model="price"
|
||||||
|
v-bind="priceAttrs"
|
||||||
|
>
|
||||||
|
<option value="€">1-10€</option>
|
||||||
|
<option value="€€">10-20€</option>
|
||||||
|
<option value="€€€">20-30€</option>
|
||||||
|
</select>
|
||||||
|
<label class="text-red-500">{{ errors.price }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalComponent>
|
||||||
|
</template>
|
||||||
|
|
@ -3,7 +3,7 @@ import { ref } from "vue";
|
||||||
import ModalComponent from "./ModalComponent.vue";
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
import { toTypedSchema } from "@vee-validate/yup";
|
||||||
import { Form, Field, ErrorMessage, useForm } from "vee-validate";
|
import { useForm } from "vee-validate";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { pb } from "../pocketbase";
|
import { pb } from "../pocketbase";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ defineExpose({ open, close });
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="'modal overflow-y-auto ' + (opened ? 'modal-open' : '')" :id="`modal-${props.id}`">
|
<div :class="'modal overflow-y-auto ' + (opened ? 'modal-open' : '')" :id="`modal-${props.id}`">
|
||||||
<div :class="'modal-box ' + style">
|
<div :class="'modal-box overflow-y-visible' + style">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" @click="reject">✕</button>
|
<button class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" @click="reject">✕</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -54,7 +54,7 @@ defineExpose({ open, close });
|
||||||
<button class="btn btn-outline btn-success" @click="accept">
|
<button class="btn btn-outline btn-success" @click="accept">
|
||||||
{{ props.confirmButtonText }}
|
{{ props.confirmButtonText }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="rejectButtonText !== undefined" class="btn btn-outline btn-success" @click="reject">
|
<button v-if="rejectButtonText !== undefined" class="ml-3 btn btn-outline btn-success" @click="reject">
|
||||||
{{ props.rejectButtonText }}
|
{{ props.rejectButtonText }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import { currentUser, signOut } from "../pocketbase";
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
||||||
<font-awesome-icon icon="fa-solid fa-user" />
|
<font-awesome-icon icon="fa-solid fa-user" />
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-40">
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[20000] p-2 shadow bg-base-100 rounded-box w-40">
|
||||||
<li v-if="currentUser" class="mb-2">Logged in as {{ currentUser.username }}</li>
|
<li v-if="currentUser" class="mb-2">Logged in as {{ currentUser.username }}</li>
|
||||||
<li v-if="currentUser" @click="signOut" class="text-red-600 btn btn-xs">Sign Out</li>
|
<li v-if="currentUser" @click="signOut" class="text-red-600 btn btn-xs">Sign Out</li>
|
||||||
<li v-else>
|
<li v-else>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,16 @@ import { ref, onMounted, PropType, watch } from "vue";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
import "../assets/map.css";
|
import "../assets/map.css";
|
||||||
|
import LatLng from "../models/map";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRestaurantsStore } from "../stores/restaurants";
|
||||||
|
|
||||||
const initialMap = ref(null);
|
const initialMap = ref<L.Map | null>(null);
|
||||||
|
|
||||||
const props = defineProps({
|
const { restaurants } = storeToRefs(useRestaurantsStore());
|
||||||
restaurants: { type: Array as PropType<Object[]> },
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
clicked: (latlng: LatLng) => true,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -17,15 +22,17 @@ onMounted(() => {
|
||||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
}).addTo(initialMap.value);
|
}).addTo(initialMap.value);
|
||||||
|
|
||||||
initialMap.value.on("click", function (e) {
|
initialMap.value!.on("click", function (e) {
|
||||||
console.log(e.latlng);
|
emit("clicked", e.latlng);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.restaurants,
|
() => restaurants.value,
|
||||||
async (newval, oldval) => {
|
async (newval, oldval) => {
|
||||||
props.restaurants?.forEach((restaurant) => {
|
restaurants.value?.forEach((restaurant) => {
|
||||||
new L.marker({ lat: 22.353953049824614, lng: 86.0591123082797 }).addTo(initialMap);
|
const marker = L.marker({ lat: restaurant.latitude, lng: restaurant.longitude });
|
||||||
|
marker.bindPopup(`<p>${restaurant.name}</p>`);
|
||||||
|
marker.addTo(initialMap.value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
|
|
@ -35,6 +42,6 @@ onMounted(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div @click="onClick" id="map"></div>
|
<div id="map"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface LatLng {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default interface Restaurant {
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
price: string;
|
||||||
|
averageRating: number;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface Tag {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { pb } from "../pocketbase";
|
||||||
|
import { average } from "../utils/math";
|
||||||
|
import Restaurant from "../models/restaurant";
|
||||||
|
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
export const useRestaurantsStore = defineStore("restaurants", {
|
||||||
|
state: () => ({
|
||||||
|
restaurants: [] as Restaurant[],
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getRandomRestaurant(state) {
|
||||||
|
return () => _.sample(state.restaurants);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async load() {
|
||||||
|
const restaurants = await pb.collection("restaurants").getFullList({ expand: "tags,reviews_via_restaurant" });
|
||||||
|
|
||||||
|
restaurants.forEach((restaurant) => {
|
||||||
|
const ratings: number[] = [];
|
||||||
|
restaurant.expand?.reviews_via_restaurant?.forEach((review) => {
|
||||||
|
ratings.push(review.rating);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ratings.length !== 0) {
|
||||||
|
restaurant.average_rating = average(ratings);
|
||||||
|
} else {
|
||||||
|
restaurant.average_rating = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restaurant.expand?.tags !== undefined) {
|
||||||
|
restaurant.tags = restaurant.expand.tags.map((tag) => tag.name);
|
||||||
|
} else {
|
||||||
|
restaurant.tags = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.restaurants = restaurants;
|
||||||
|
},
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.load();
|
||||||
|
|
||||||
|
pb.collection("restaurants").subscribe("*", async (e) => {
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
|
||||||
|
pb.collection("reviews").subscribe("*", async (e) => {
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,69 +1,31 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm } from "vee-validate";
|
|
||||||
import { toTypedSchema } from "@vee-validate/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
import "@/assets/form.css";
|
import "@/assets/form.css";
|
||||||
|
|
||||||
import { pb } from "../pocketbase.ts";
|
import RestaurantsMap from "../components/RestaurantsMap.vue";
|
||||||
import TagsField from "../components/TagsField.vue";
|
import { ref } from "vue";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import CreateRestaurantModal from "../components/CreateRestaurantModal.vue";
|
||||||
|
|
||||||
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
|
||||||
validationSchema: toTypedSchema(
|
|
||||||
yup.object({
|
|
||||||
name: yup.string().required("This field is required."),
|
|
||||||
tags: yup.array().of(yup.string()).required("This field is required."),
|
|
||||||
price: yup.string().required("This field is required."),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit((values) => {
|
|
||||||
const tags: Object[] = [];
|
|
||||||
|
|
||||||
values.tags.forEach((name) => {
|
|
||||||
const tag = availableTags.value.filter((tag) => tag.name === name)[0];
|
|
||||||
if (tag !== undefined) {
|
|
||||||
tags.push(tag.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
pb.collection("restaurants").create({
|
|
||||||
name: values.name,
|
|
||||||
price_range: values.price,
|
|
||||||
tags: tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
successMessage.value = `Restaurant ${values.name} created successfully!`;
|
|
||||||
errorMessage.value = undefined;
|
|
||||||
} catch (err) {
|
|
||||||
errorMessage.value = err;
|
|
||||||
successMessage.value = undefined;
|
|
||||||
}
|
|
||||||
resetForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorMessage = ref<string>();
|
const errorMessage = ref<string>();
|
||||||
const successMessage = ref<string>();
|
const successMessage = ref<string>();
|
||||||
|
|
||||||
const availableTags = ref<Object[]>([]);
|
const createRestaurantModal = ref<typeof CreateRestaurantModal>();
|
||||||
|
|
||||||
const tagsNames = computed(() => {
|
function onMapClicked(latlng) {
|
||||||
return availableTags.value.map((tag) => tag.name);
|
createRestaurantModal.value?.setRestaurantPosition(latlng);
|
||||||
});
|
createRestaurantModal.value?.show();
|
||||||
|
}
|
||||||
|
|
||||||
const [name, nameAttrs] = defineField("name");
|
function onRestaurantCreationSucceeded(message: string) {
|
||||||
const [tags, tagsAttrs] = defineField("tags");
|
successMessage.value = message;
|
||||||
const [price, priceAttrs] = defineField("price");
|
errorMessage.value = undefined;
|
||||||
|
createRestaurantModal.value?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
function onRestaurantCreationFailed(message: string) {
|
||||||
availableTags.value = await pb.collection("tags").getFullList();
|
successMessage.value = undefined;
|
||||||
|
errorMessage.value = message;
|
||||||
pb.collection("tags").subscribe("*", async (e) => {
|
createRestaurantModal.value?.hide();
|
||||||
availableTags.value = await pb.collection("tags").getFullList();
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -76,42 +38,11 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="flex flex-col place-content-center items-center h-full" @submit.prevent="onSubmit">
|
<RestaurantsMap @clicked="onMapClicked" />
|
||||||
<div class="flex flex-row flex-nowrap w-96">
|
|
||||||
<div class="mx-5 my-4 w-full">
|
<CreateRestaurantModal
|
||||||
<label class="form-label"> Name </label>
|
@succeeded="onRestaurantCreationSucceeded"
|
||||||
<input
|
@failed="onRestaurantCreationFailed"
|
||||||
:class="errors.name === undefined ? 'form-field' : 'form-field-error'"
|
ref="createRestaurantModal"
|
||||||
v-model="name"
|
/>
|
||||||
v-bind="nameAttrs"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<label class="text-red-500">{{ errors.name }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row flex-nowrap w-96">
|
|
||||||
<div class="mx-5 my-4 w-full">
|
|
||||||
<label class="form-label"> Tags </label>
|
|
||||||
<TagsField v-model="tags" v-bind="tagsAttrs" :options="tagsNames" />
|
|
||||||
<br />
|
|
||||||
<label class="text-red-500">{{ errors.tags }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row flex-nowrap w-96">
|
|
||||||
<div class="mx-5 my-4 w-full">
|
|
||||||
<label class="form-label"> Price </label>
|
|
||||||
<select
|
|
||||||
:class="errors.price === undefined ? 'form-select' : 'form-select-error'"
|
|
||||||
v-model="price"
|
|
||||||
v-bind="priceAttrs"
|
|
||||||
>
|
|
||||||
<option value="€">1-10€</option>
|
|
||||||
<option value="€€">10-20€</option>
|
|
||||||
<option value="€€€">20-30€</option>
|
|
||||||
</select>
|
|
||||||
<label class="text-red-500">{{ errors.price }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-outline">Create</button>
|
|
||||||
</form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import RestaurantsTable from "../components/RestaurantsTable.vue";
|
||||||
import RestaurantsMap from "../components/RestaurantsMap.vue";
|
import RestaurantsMap from "../components/RestaurantsMap.vue";
|
||||||
import ReviewsDrawer from "../components/ReviewsDrawer.vue";
|
import ReviewsDrawer from "../components/ReviewsDrawer.vue";
|
||||||
import { average } from "../utils/math";
|
import { average } from "../utils/math";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRestaurantsStore } from "../stores/restaurants";
|
||||||
|
|
||||||
const data = ref<Object[]>([]);
|
const data = ref<Object[]>([]);
|
||||||
|
|
||||||
|
|
@ -13,42 +15,7 @@ const table = ref<typeof RestaurantsTable>();
|
||||||
|
|
||||||
const selectedRestaurant = ref<Object>();
|
const selectedRestaurant = ref<Object>();
|
||||||
|
|
||||||
async function load() {
|
const { restaurants } = storeToRefs(useRestaurantsStore());
|
||||||
const restaurants = await pb.collection("restaurants").getFullList({ expand: "tags,reviews_via_restaurant" });
|
|
||||||
|
|
||||||
restaurants.forEach((restaurant) => {
|
|
||||||
const ratings: number[] = [];
|
|
||||||
restaurant.expand?.reviews_via_restaurant?.forEach((review) => {
|
|
||||||
ratings.push(review.rating);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ratings.length !== 0) {
|
|
||||||
restaurant.average_rating = average(ratings);
|
|
||||||
} else {
|
|
||||||
restaurant.average_rating = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restaurant.expand?.tags !== undefined) {
|
|
||||||
restaurant.tags = restaurant.expand.tags.map((tag) => tag.name);
|
|
||||||
} else {
|
|
||||||
restaurant.tags = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -64,7 +31,7 @@ function onDrawerClosed() {
|
||||||
<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 ref="table" @restaurant-selected="onRestaurantSelected" :data="data" />
|
<RestaurantsTable ref="table" @restaurant-selected="onRestaurantSelected" :data="restaurants" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 h-3/6 lg:h-full">
|
<div class="flex-1 h-3/6 lg:h-full">
|
||||||
<RestaurantsMap />
|
<RestaurantsMap />
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
import { pb } from "../pocketbase.ts";
|
import { useRestaurantsStore } from "../stores/restaurants.ts";
|
||||||
|
|
||||||
const restaurant = ref<string>();
|
const restaurant = ref<string>();
|
||||||
|
|
||||||
async function getRandomRestaurant() {
|
const store = useRestaurantsStore();
|
||||||
const result = await pb
|
|
||||||
.collection("restaurants")
|
function getRandomRestaurant() {
|
||||||
.getFirstListItem("", { sort: "@random" });
|
restaurant.value = store.getRandomRestaurant();
|
||||||
restaurant.value = result.name;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col place-content-center items-center h-full">
|
<div class="flex flex-col place-content-center items-center h-full">
|
||||||
<h2 class="mb-10" v-if="restaurant !== undefined">How about going to...</h2>
|
<h2 class="mb-10" v-if="restaurant !== undefined">How about going to...</h2>
|
||||||
<p class="">{{ restaurant }}</p>
|
<p class="">{{ restaurant?.name }}</p>
|
||||||
<button
|
<button class="btn btn-outline btn-primary mt-10" @click="getRandomRestaurant">Get random restaurant</button>
|
||||||
class="btn btn-outline btn-primary mt-10"
|
|
||||||
@click="getRandomRestaurant"
|
|
||||||
>
|
|
||||||
Get random restaurant
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue