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-vue3": "^3.0.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"pocketbase": "^0.21.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.2",
|
||||
|
|
@ -1637,6 +1638,11 @@
|
|||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"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": {
|
||||
"version": "10.2.2",
|
||||
"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-vue3": "^3.0.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"pocketbase": "^0.21.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.2",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
import { RouterView } from "vue-router";
|
||||
import "./style.css";
|
||||
import NavigationBar from "./components/NavigationBar.vue";
|
||||
import { useRestaurantsStore } from "./stores/restaurants";
|
||||
|
||||
useRestaurantsStore().init();
|
||||
</script>
|
||||
|
||||
<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 { toTypedSchema } from "@vee-validate/yup";
|
||||
import { Form, Field, ErrorMessage, useForm } from "vee-validate";
|
||||
import { useForm } from "vee-validate";
|
||||
import * as yup from "yup";
|
||||
import { pb } from "../pocketbase";
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ defineExpose({ open, close });
|
|||
|
||||
<template>
|
||||
<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">
|
||||
<button class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" @click="reject">✕</button>
|
||||
</form>
|
||||
|
|
@ -54,7 +54,7 @@ defineExpose({ open, close });
|
|||
<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">
|
||||
<button v-if="rejectButtonText !== undefined" class="ml-3 btn btn-outline btn-success" @click="reject">
|
||||
{{ props.rejectButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import { currentUser, signOut } from "../pocketbase";
|
|||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
||||
<font-awesome-icon icon="fa-solid fa-user" />
|
||||
</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" @click="signOut" class="text-red-600 btn btn-xs">Sign Out</li>
|
||||
<li v-else>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@ import { ref, onMounted, PropType, watch } from "vue";
|
|||
import "leaflet/dist/leaflet.css";
|
||||
import * as L from "leaflet";
|
||||
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({
|
||||
restaurants: { type: Array as PropType<Object[]> },
|
||||
const { restaurants } = storeToRefs(useRestaurantsStore());
|
||||
|
||||
const emit = defineEmits({
|
||||
clicked: (latlng: LatLng) => true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -17,15 +22,17 @@ onMounted(() => {
|
|||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).addTo(initialMap.value);
|
||||
|
||||
initialMap.value.on("click", function (e) {
|
||||
console.log(e.latlng);
|
||||
initialMap.value!.on("click", function (e) {
|
||||
emit("clicked", e.latlng);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.restaurants,
|
||||
() => restaurants.value,
|
||||
async (newval, oldval) => {
|
||||
props.restaurants?.forEach((restaurant) => {
|
||||
new L.marker({ lat: 22.353953049824614, lng: 86.0591123082797 }).addTo(initialMap);
|
||||
restaurants.value?.forEach((restaurant) => {
|
||||
const marker = L.marker({ lat: restaurant.latitude, lng: restaurant.longitude });
|
||||
marker.bindPopup(`<p>${restaurant.name}</p>`);
|
||||
marker.addTo(initialMap.value);
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
|
|
@ -35,6 +42,6 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div @click="onClick" id="map"></div>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</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">
|
||||
import { useForm } from "vee-validate";
|
||||
import { toTypedSchema } from "@vee-validate/yup";
|
||||
import * as yup from "yup";
|
||||
import "@/assets/form.css";
|
||||
|
||||
import { pb } from "../pocketbase.ts";
|
||||
import TagsField from "../components/TagsField.vue";
|
||||
import { computed, onMounted, ref } from "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();
|
||||
});
|
||||
import RestaurantsMap from "../components/RestaurantsMap.vue";
|
||||
import { ref } from "vue";
|
||||
import CreateRestaurantModal from "../components/CreateRestaurantModal.vue";
|
||||
|
||||
const errorMessage = ref<string>();
|
||||
const successMessage = ref<string>();
|
||||
|
||||
const availableTags = ref<Object[]>([]);
|
||||
const createRestaurantModal = ref<typeof CreateRestaurantModal>();
|
||||
|
||||
const tagsNames = computed(() => {
|
||||
return availableTags.value.map((tag) => tag.name);
|
||||
});
|
||||
function onMapClicked(latlng) {
|
||||
createRestaurantModal.value?.setRestaurantPosition(latlng);
|
||||
createRestaurantModal.value?.show();
|
||||
}
|
||||
|
||||
const [name, nameAttrs] = defineField("name");
|
||||
const [tags, tagsAttrs] = defineField("tags");
|
||||
const [price, priceAttrs] = defineField("price");
|
||||
function onRestaurantCreationSucceeded(message: string) {
|
||||
successMessage.value = message;
|
||||
errorMessage.value = undefined;
|
||||
createRestaurantModal.value?.hide();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
availableTags.value = await pb.collection("tags").getFullList();
|
||||
|
||||
pb.collection("tags").subscribe("*", async (e) => {
|
||||
availableTags.value = await pb.collection("tags").getFullList();
|
||||
});
|
||||
});
|
||||
function onRestaurantCreationFailed(message: string) {
|
||||
successMessage.value = undefined;
|
||||
errorMessage.value = message;
|
||||
createRestaurantModal.value?.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -76,42 +38,11 @@ onMounted(async () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-col place-content-center items-center h-full" @submit.prevent="onSubmit">
|
||||
<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>
|
||||
<button class="btn btn-outline">Create</button>
|
||||
</form>
|
||||
<RestaurantsMap @clicked="onMapClicked" />
|
||||
|
||||
<CreateRestaurantModal
|
||||
@succeeded="onRestaurantCreationSucceeded"
|
||||
@failed="onRestaurantCreationFailed"
|
||||
ref="createRestaurantModal"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import RestaurantsTable from "../components/RestaurantsTable.vue";
|
|||
import RestaurantsMap from "../components/RestaurantsMap.vue";
|
||||
import ReviewsDrawer from "../components/ReviewsDrawer.vue";
|
||||
import { average } from "../utils/math";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRestaurantsStore } from "../stores/restaurants";
|
||||
|
||||
const data = ref<Object[]>([]);
|
||||
|
||||
|
|
@ -13,42 +15,7 @@ const table = ref<typeof RestaurantsTable>();
|
|||
|
||||
const selectedRestaurant = ref<Object>();
|
||||
|
||||
async function 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 = [];
|
||||
}
|
||||
});
|
||||
|
||||
data.value = restaurants;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
|
||||
pb.collection("restaurants").subscribe("*", (e) => {
|
||||
load();
|
||||
});
|
||||
|
||||
pb.collection("reviews").subscribe("*", (e) => {
|
||||
load();
|
||||
});
|
||||
});
|
||||
const { restaurants } = storeToRefs(useRestaurantsStore());
|
||||
|
||||
function onRestaurantSelected(restaurant: Object) {
|
||||
selectedRestaurant.value = restaurant;
|
||||
|
|
@ -64,7 +31,7 @@ function onDrawerClosed() {
|
|||
<template>
|
||||
<div class="flex flex-col-reverse sm:flex-row">
|
||||
<div class="w-full flex-1">
|
||||
<RestaurantsTable ref="table" @restaurant-selected="onRestaurantSelected" :data="data" />
|
||||
<RestaurantsTable ref="table" @restaurant-selected="onRestaurantSelected" :data="restaurants" />
|
||||
</div>
|
||||
<div class="flex-1 h-3/6 lg:h-full">
|
||||
<RestaurantsMap />
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
import { pb } from "../pocketbase.ts";
|
||||
import { useRestaurantsStore } from "../stores/restaurants.ts";
|
||||
|
||||
const restaurant = ref<string>();
|
||||
|
||||
async function getRandomRestaurant() {
|
||||
const result = await pb
|
||||
.collection("restaurants")
|
||||
.getFirstListItem("", { sort: "@random" });
|
||||
restaurant.value = result.name;
|
||||
const store = useRestaurantsStore();
|
||||
|
||||
function getRandomRestaurant() {
|
||||
restaurant.value = store.getRandomRestaurant();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<p class="">{{ restaurant }}</p>
|
||||
<button
|
||||
class="btn btn-outline btn-primary mt-10"
|
||||
@click="getRandomRestaurant"
|
||||
>
|
||||
Get random restaurant
|
||||
</button>
|
||||
<p class="">{{ restaurant?.name }}</p>
|
||||
<button class="btn btn-outline btn-primary mt-10" @click="getRandomRestaurant">Get random restaurant</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in New Issue