Improvements to reviews and creation form
parent
d0cb6eeca9
commit
9282cde499
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
"@vee-validate/yup": "^4.13.0",
|
"@vee-validate/yup": "^4.13.0",
|
||||||
|
|
@ -445,6 +446,18 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
"@vee-validate/yup": "^4.13.0",
|
"@vee-validate/yup": "^4.13.0",
|
||||||
|
|
|
||||||
|
|
@ -3,61 +3,53 @@ import { ref } from "vue";
|
||||||
import { type PropType } from "vue";
|
import { type PropType } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: { type: Array as PropType<string[]> },
|
|
||||||
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: () => {} },
|
||||||
clearable: { type: Boolean, default: true },
|
clearable: { type: Boolean, default: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const model = defineModel<string[]>();
|
||||||
|
|
||||||
const emit = defineEmits(["updateModelValue"]);
|
const emit = defineEmits(["updateModelValue"]);
|
||||||
|
|
||||||
const query = ref<string>("");
|
const query = ref<string>("");
|
||||||
|
|
||||||
function onBadgeClicked(value: string) {
|
function onBadgeClicked(value: string) {
|
||||||
if (props.model === undefined) {
|
if (model.value?.includes(value)) {
|
||||||
return;
|
model.value = model.value.filter((val) => val != value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newval = props.model.includes(value)
|
|
||||||
? props.model.filter((val) => val != value)
|
|
||||||
: [...props.model, value];
|
|
||||||
|
|
||||||
emit("updateModelValue", newval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOptionClicked(value: string) {
|
function onOptionClicked(value: string) {
|
||||||
if (props.model === undefined) {
|
if (model.value === undefined) {
|
||||||
return;
|
model.value = [value];
|
||||||
|
} else if (model.value?.includes(value)) {
|
||||||
|
model.value = model.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
|
model.value = [...model.value, value];
|
||||||
}
|
}
|
||||||
|
|
||||||
const newval = props.model?.includes(value)
|
|
||||||
? props.model?.filter((val) => val !== value)
|
|
||||||
: [...props.model, value];
|
|
||||||
|
|
||||||
query.value = "";
|
query.value = "";
|
||||||
emit("updateModelValue", newval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showClear() {
|
function showClear() {
|
||||||
if (!props.clearable) {
|
if (!props.clearable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Array.isArray(props.model) && props.model?.length == 0) {
|
if (Array.isArray(model.value) && model.value?.length == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClear() {
|
function onClear() {
|
||||||
emit("updateModelValue", []);
|
model.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
return props.options?.filter((opt) =>
|
return props.options?.filter((opt) => opt.toLocaleLowerCase().startsWith(lcquery));
|
||||||
opt.toLocaleLowerCase().startsWith(lcquery)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -67,11 +59,7 @@ function filteredOptions() {
|
||||||
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"
|
||||||
>
|
>
|
||||||
<div class="mx-2 flex flex-wrap items-center">
|
<div class="mx-2 flex flex-wrap items-center">
|
||||||
<div
|
<div v-for="(value, idx) in model" :key="`${value}-${idx}`" class="badge m-1">
|
||||||
v-for="(value, idx) in model"
|
|
||||||
:key="`${value}-${idx}`"
|
|
||||||
class="badge m-1"
|
|
||||||
>
|
|
||||||
<button class="mr-2" @click="onBadgeClicked(value)">
|
<button class="mr-2" @click="onBadgeClicked(value)">
|
||||||
<font-awesome-icon icon="xmark" />
|
<font-awesome-icon icon="xmark" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -85,27 +73,13 @@ function filteredOptions() {
|
||||||
role="combobox"
|
role="combobox"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
<button
|
<button v-show="showClear" class="btn btn-ghost h-auto self-stretch" @click="onClear">
|
||||||
v-show="showClear"
|
|
||||||
class="btn btn-ghost h-auto self-stretch"
|
|
||||||
@click="onClear"
|
|
||||||
>
|
|
||||||
<font-awesome-icon icon="xmark" />
|
<font-awesome-icon icon="xmark" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul class="menu dropdown-content z-10 w-full rounded-lg border border-neutral-content bg-base-100 p-0 shadow">
|
||||||
class="menu dropdown-content z-10 w-full rounded-lg border border-neutral-content bg-base-100 p-0 shadow"
|
<li v-for="option in filteredOptions()" :key="option" role="option" class="m-0 p-0">
|
||||||
>
|
<button :class="{ active: model?.includes(option) }" @click="onOptionClicked(option)">
|
||||||
<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" />
|
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
||||||
{{ option }}
|
{{ option }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,9 @@ import { currentUser, signOut } from "../pocketbase";
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle lg:hidden">
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle lg:hidden">
|
||||||
<font-awesome-icon icon="fa-solid fa-bars" />
|
<font-awesome-icon icon="fa-solid fa-bars" />
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
tabindex="0"
|
|
||||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
|
||||||
>
|
|
||||||
<li><RouterLink class="btn btn-ghost" to="/">Home</RouterLink></li>
|
<li><RouterLink class="btn btn-ghost" to="/">Home</RouterLink></li>
|
||||||
<li>
|
|
||||||
<RouterLink class="btn btn-ghost" to="/about">About</RouterLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<RouterLink class="btn btn-ghost" to="/random">Random</RouterLink>
|
<RouterLink class="btn btn-ghost" to="/random">Random</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -27,18 +22,19 @@ import { currentUser, signOut } from "../pocketbase";
|
||||||
<li>
|
<li>
|
||||||
<RouterLink class="btn btn-ghost" to="/create">Create Restaurant</RouterLink>
|
<RouterLink class="btn btn-ghost" to="/create">Create Restaurant</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<RouterLink class="btn btn-ghost" to="/about">About</RouterLink>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-center hidden lg:flex lg:flex-row">
|
<div class="navbar-center hidden lg:flex lg:flex-row">
|
||||||
<div class="menu menu-horizontal px-1">
|
<div class="menu menu-horizontal px-1">
|
||||||
<RouterLink class="btn btn-ghost" to="/">Home</RouterLink>
|
<RouterLink class="btn btn-ghost" to="/">Home</RouterLink>
|
||||||
<RouterLink class="btn btn-ghost" to="/about">About</RouterLink>
|
|
||||||
<RouterLink class="btn btn-ghost" to="/random">Random</RouterLink>
|
<RouterLink class="btn btn-ghost" to="/random">Random</RouterLink>
|
||||||
<RouterLink class="btn btn-ghost" to="/list">List</RouterLink>
|
<RouterLink class="btn btn-ghost" to="/list">List</RouterLink>
|
||||||
<RouterLink class="btn btn-ghost" to="/create"
|
<RouterLink class="btn btn-ghost" to="/create">Create Restaurant</RouterLink>
|
||||||
>Create Restaurant</RouterLink
|
<RouterLink class="btn btn-ghost" to="/about">About</RouterLink>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
|
@ -46,16 +42,11 @@ 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
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-40">
|
||||||
tabindex="0"
|
|
||||||
class="menu menu-sm dropdown-content mt-3 z-[1] 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>
|
||||||
<RouterLink class="text-current" to="/login">
|
<RouterLink class="text-current" to="/login"> Sign In </RouterLink>
|
||||||
Sign In
|
|
||||||
</RouterLink>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { round } from "../utils/math";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: { type: [String, Number] },
|
||||||
|
showText: { type: Boolean, default: true },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
size: { type: String as () => "xs" | "sm" | "md" | "lg", default: "md" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const model = defineModel<number>();
|
||||||
|
|
||||||
|
function onUpdateRating(change) {
|
||||||
|
model.value = change.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rounded = computed(() => {
|
||||||
|
if (model.value === undefined || model.value === null) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return Math.round(model.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
if (model.value === undefined || model.value === null) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
console.log(model.value);
|
||||||
|
return `${round(model.value, 1)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ratingClass = computed(() => {
|
||||||
|
const ratingSize = {
|
||||||
|
xs: "rating-xs",
|
||||||
|
sm: "rating-sm",
|
||||||
|
md: "rating-md",
|
||||||
|
lg: "rating-lg",
|
||||||
|
};
|
||||||
|
return ratingSize[props.size];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="flex-0 align-bottom flex-grow-0 text-gray-500" v-if="showText" for="rating=input">{{ text }}</label>
|
||||||
|
<div id="rating-input" :class="'rating align-bottom flex-0 flex-grow-0 ' + ratingClass">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:name="`rating-${props.id}`"
|
||||||
|
value="0"
|
||||||
|
class="rating-hidden"
|
||||||
|
:checked="rounded == 0"
|
||||||
|
@change="onUpdateRating"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:name="`rating-${props.id}`"
|
||||||
|
value="1"
|
||||||
|
class="bg-amber-400 mask mask-star"
|
||||||
|
:checked="rounded == 1"
|
||||||
|
@change="onUpdateRating"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:name="`rating-${props.id}`"
|
||||||
|
value="2"
|
||||||
|
class="bg-amber-400 mask mask-star"
|
||||||
|
:checked="rounded == 2"
|
||||||
|
@change="onUpdateRating"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:name="`rating-${props.id}`"
|
||||||
|
value="3"
|
||||||
|
class="bg-amber-400 mask mask-star"
|
||||||
|
:checked="rounded == 3"
|
||||||
|
@change="onUpdateRating"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:name="`rating-${props.id}`"
|
||||||
|
value="4"
|
||||||
|
class="bg-amber-400 mask mask-star"
|
||||||
|
:checked="rounded == 4"
|
||||||
|
@change="onUpdateRating"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:name="`rating-${props.id}`"
|
||||||
|
value="5"
|
||||||
|
class="bg-amber-400 mask mask-star"
|
||||||
|
:checked="rounded == 5"
|
||||||
|
@change="onUpdateRating"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -5,6 +5,7 @@ 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 } from "vue";
|
||||||
|
import RatingField from "./RatingField.vue";
|
||||||
|
|
||||||
DataTable.use(DataTablesCore);
|
DataTable.use(DataTablesCore);
|
||||||
|
|
||||||
|
|
@ -48,13 +49,14 @@ const emit = defineEmits(["restaurantSelected"]);
|
||||||
@select="onRowSelected"
|
@select="onRowSelected"
|
||||||
>
|
>
|
||||||
<template #column-2="props">
|
<template #column-2="props">
|
||||||
<div class="rating">
|
<RatingField
|
||||||
<input type="radio" disabled :name="`rating-${props.rowIndex}`" class="mask mask-star" :checked="false" />
|
v-if="props.cellData !== null"
|
||||||
<input type="radio" disabled :name="`rating-${props.rowIndex}`" class="mask mask-star" :checked="false" />
|
:disabled="true"
|
||||||
<input type="radio" disabled :name="`rating-${props.rowIndex}`" class="mask mask-star" :checked="false" />
|
:id="props.rowIndex"
|
||||||
<input type="radio" disabled :name="`rating-${props.rowIndex}`" class="mask mask-star" :checked="false" />
|
v-model="props.cellData"
|
||||||
<input type="radio" disabled :name="`rating-${props.rowIndex}`" class="mask mask-star" :checked="false" />
|
:size="'sm'"
|
||||||
</div>
|
/>
|
||||||
|
<p class="text-gray-500" v-else>No rating yet...</p>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import RatingField from "./RatingField.vue";
|
||||||
|
|
||||||
|
const rating = ref<number>(0);
|
||||||
|
const text = ref<string>();
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
rating.value = 0;
|
||||||
|
text.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ clear });
|
||||||
|
|
||||||
|
defineEmits({
|
||||||
|
publish: (rating: number, text: string) => true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card card-compact bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex flex-row mb-3">
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<RatingField v-model="rating" id="field" />
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-model="text"
|
||||||
|
class="textarea textarea-bordered resize-none w-full block box-border border"
|
||||||
|
placeholder="Add your own review!"
|
||||||
|
></textarea>
|
||||||
|
<div class="text-right mt-4">
|
||||||
|
<button
|
||||||
|
@click="$emit('publish', rating, text)"
|
||||||
|
:class="'btn btn-sm btn-primary btn-outline ' + (text?.length > 0 ? '' : 'btn-disabled')"
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-body {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 0.5em 1em 0.5em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25px;
|
||||||
|
right: 25px;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import ReviewsList from "./ReviewsList.vue";
|
import ReviewsList from "./ReviewsList.vue";
|
||||||
|
import ReviewField from "./ReviewField.vue";
|
||||||
|
import { currentUser, pb } from "../pocketbase";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
restaurant: { type: Object },
|
restaurant: { type: Object },
|
||||||
});
|
});
|
||||||
|
|
||||||
const opened = ref<boolean>(false);
|
const opened = ref<boolean>(false);
|
||||||
|
const reviewField = ref<typeof ReviewField>();
|
||||||
|
const reviewsList = ref<typeof ReviewsList>();
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
opened.value = true;
|
opened.value = true;
|
||||||
|
|
@ -16,6 +20,22 @@ function close() {
|
||||||
opened.value = false;
|
opened.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPublishReview(rating: number, text: string) {
|
||||||
|
if (props.restaurant === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.collection("reviews").create({
|
||||||
|
text: text,
|
||||||
|
rating: rating,
|
||||||
|
user: currentUser.value.id,
|
||||||
|
restaurant: props.restaurant.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
reviewField.value?.clear();
|
||||||
|
reviewsList.value?.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open, close });
|
defineExpose({ open, close });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -24,14 +44,15 @@ defineExpose({ open, close });
|
||||||
<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 for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
<div class="menu p-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"
|
||||||
class="btn btn-circle btn-sm text-gray-500 bg-transparent"
|
class="btn btn-circle btn-sm text-gray-500 bg-transparent"
|
||||||
icon="fa-solid fa-xmark"
|
icon="fa-solid fa-xmark"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ReviewsList :restaurant="props.restaurant" />
|
<ReviewField ref="reviewField" @publish="onPublishReview" />
|
||||||
|
<ReviewsList ref="reviewsList" :restaurant="props.restaurant" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { pb } from "../pocketbase";
|
import { pb } from "../pocketbase";
|
||||||
|
import RatingField from "./RatingField.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
restaurant: { type: Object },
|
restaurant: { type: Object },
|
||||||
|
|
@ -8,6 +9,15 @@ const props = defineProps({
|
||||||
|
|
||||||
const reviews = ref<Object[]>([]);
|
const reviews = ref<Object[]>([]);
|
||||||
|
|
||||||
|
async function load(restaurantId: string) {
|
||||||
|
reviews.value = await pb.collection("reviews").getFullList({
|
||||||
|
filter: pb.filter("restaurant ~ {:id}", { id: restaurantId }),
|
||||||
|
expand: "user",
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
console.log(reviews.value);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.restaurant,
|
() => props.restaurant,
|
||||||
async (newval, oldval) => {
|
async (newval, oldval) => {
|
||||||
|
|
@ -15,12 +25,25 @@ watch(
|
||||||
if (props.restaurant === undefined) {
|
if (props.restaurant === undefined) {
|
||||||
reviews.value = [];
|
reviews.value = [];
|
||||||
} else {
|
} else {
|
||||||
reviews.value = await pb
|
await load(props.restaurant.id);
|
||||||
.collection("reviews")
|
pb.collection("reviews").subscribe("*", (e) => {
|
||||||
.getFullList({ filter: pb.filter("restaurant ~ {:id}", { id: props.restaurant.id }), expand: "user" });
|
if (e.action === "create" && e.record.restaurant === props.restaurant?.id) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (props.restaurant === undefined) {
|
||||||
|
reviews.value = [];
|
||||||
|
} else {
|
||||||
|
await load(props.restaurant.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ refresh });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -32,16 +55,9 @@ watch(
|
||||||
<p class="text-left">{{ review.text }}</p>
|
<p class="text-left">{{ review.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row">
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<p class="flex-0 align-bottom flex-grow-0 mx-3">{{ review.rating }}/5</p>
|
<RatingField :disabled="true" v-model="review.rating" :id="review.id" />
|
||||||
<div class="rating align-bottom flex-0 flex-grow-0">
|
|
||||||
<input type="radio" disabled name="rating-1" class="mask mask-star" :checked="false" />
|
|
||||||
<input type="radio" disabled name="rating-1" class="mask mask-star" :checked="false" />
|
|
||||||
<input type="radio" disabled name="rating-1" class="mask mask-star" :checked="true" />
|
|
||||||
<input type="radio" disabled name="rating-1" class="mask mask-star" :checked="false" />
|
|
||||||
<input type="radio" disabled name="rating-1" class="mask mask-star" :checked="false" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import { createPinia } from "pinia";
|
||||||
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { faBars, faUser, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faBars, faCheck, faUser, faXmark, faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faGitAlt } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
library.add(faBars, faUser, faXmark);
|
library.add(faBars, faUser, faXmark, faCheck, faGitAlt, faExclamationCircle);
|
||||||
|
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const average = (array) => array.reduce((a, b) => a + b) / array.length;
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const average = (array) => array.reduce((a, b) => a + b) / array.length;
|
||||||
|
|
||||||
|
export function round(num, decimalPlaces = 0) {
|
||||||
|
num = Math.round(num + "e" + decimalPlaces);
|
||||||
|
return Number(num + "e" + -decimalPlaces);
|
||||||
|
}
|
||||||
|
|
@ -2,5 +2,8 @@
|
||||||
<div class="flex flex-col place-content-center items-center h-full">
|
<div class="flex flex-col place-content-center items-center h-full">
|
||||||
<h1>DOXFOOD</h1>
|
<h1>DOXFOOD</h1>
|
||||||
<h4>Developed by Eloi Zalczer</h4>
|
<h4>Developed by Eloi Zalczer</h4>
|
||||||
|
<a href="https://git.zalczer.fr/eloi/doxfood">
|
||||||
|
Code hosted on Gitea <font-awesome-icon :icon="['fab', 'git-alt']" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -6,42 +6,74 @@ import "@/assets/form.css";
|
||||||
|
|
||||||
import { pb } from "../pocketbase.ts";
|
import { pb } from "../pocketbase.ts";
|
||||||
import MultiSelectField from "../components/MultiSelectField.vue";
|
import MultiSelectField from "../components/MultiSelectField.vue";
|
||||||
import { onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
||||||
validationSchema: toTypedSchema(
|
validationSchema: toTypedSchema(
|
||||||
yup.object({
|
yup.object({
|
||||||
name: yup.string().required("This field is required."),
|
name: yup.string().required("This field is required."),
|
||||||
tags: yup.string().required("This field is required."),
|
tags: yup.array().of(yup.string()).required("This field is required."),
|
||||||
price_range: yup.string().required("This field is required."),
|
price: yup.string().required("This field is required."),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((values) => {
|
const onSubmit = handleSubmit((values) => {
|
||||||
pb.collection("restaurants").create(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();
|
resetForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
const tagsModel = ref<string[]>([]);
|
const errorMessage = ref<string>();
|
||||||
|
const successMessage = ref<string>();
|
||||||
|
|
||||||
|
const availableTags = ref<Object[]>([]);
|
||||||
|
|
||||||
|
const tagsNames = computed(() => {
|
||||||
|
return availableTags.value.map((tag) => tag.name);
|
||||||
|
});
|
||||||
|
|
||||||
const [name, nameAttrs] = defineField("name");
|
const [name, nameAttrs] = defineField("name");
|
||||||
const [tags, tagsAttrs] = defineField("tags");
|
const [tags, tagsAttrs] = defineField("tags");
|
||||||
const [price, priceAttrs] = defineField("price_range");
|
const [price, priceAttrs] = defineField("price");
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const tags = await pb.collection("tags").getFullList();
|
availableTags.value = await pb.collection("tags").getFullList();
|
||||||
tagsModel.value = tags.map((record) => record.name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function onUpdateSelectedTags(selected: string[]) {
|
|
||||||
tagsModel.value = selected;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="toast toast-top toast-end">
|
||||||
|
<div v-if="errorMessage" class="alert alert-error">
|
||||||
|
<span>{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="successMessage" class="alert alert-success">
|
||||||
|
<span>{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="flex flex-col place-content-center items-center h-full" @submit.prevent="onSubmit">
|
<form class="flex flex-col place-content-center items-center h-full" @submit.prevent="onSubmit">
|
||||||
<div class="flex flex-row flex-nowrap">
|
<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"> Name </label>
|
<label class="form-label"> Name </label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -53,23 +85,27 @@ function onUpdateSelectedTags(selected: string[]) {
|
||||||
<label class="text-red-500">{{ errors.name }}</label>
|
<label class="text-red-500">{{ errors.name }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-nowrap">
|
<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 :model="tags" :options="tagsModel" @update-model-value="onUpdateSelectedTags" />
|
<MultiSelectField v-model="tags" v-bind="tagsAttrs" :options="tagsNames" />
|
||||||
<label class="text-red-500">{{ errors.cuisine_type }}</label>
|
<br />
|
||||||
|
<label class="text-red-500">{{ errors.tags }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-nowrap w-1/2 place-content-center">
|
<div class="flex flex-row flex-nowrap w-96">
|
||||||
<div class="mx-5 my-4">
|
<div class="mx-5 my-4 w-full">
|
||||||
<label class="form-label"> Price </label>
|
<label class="form-label"> Price </label>
|
||||||
<input
|
<select
|
||||||
:class="errors.price_range === undefined ? 'form-field' : 'form-field-error'"
|
:class="errors.price === undefined ? 'form-select' : 'form-select-error'"
|
||||||
v-model="price"
|
v-model="price"
|
||||||
v-bind="priceAttrs"
|
v-bind="priceAttrs"
|
||||||
type="text"
|
>
|
||||||
/>
|
<option value="€">1-10€</option>
|
||||||
<label class="text-red-500">{{ errors.price_range }}</label>
|
<option value="€€">10-20€</option>
|
||||||
|
<option value="€€€">20-30€</option>
|
||||||
|
</select>
|
||||||
|
<label class="text-red-500">{{ errors.price }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-outline">Create</button>
|
<button class="btn btn-outline">Create</button>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { pb } from "../pocketbase";
|
||||||
import RestaurantsTable from "../components/RestaurantsTable.vue";
|
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/average";
|
import { average } from "../utils/math";
|
||||||
|
|
||||||
const data = ref<Object[]>([]);
|
const data = ref<Object[]>([]);
|
||||||
|
|
||||||
|
|
@ -13,11 +13,11 @@ const drawer = ref<typeof ReviewsDrawer>();
|
||||||
const selectedRestaurant = ref<Object>();
|
const selectedRestaurant = ref<Object>();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const restaurants = await pb.collection("restaurants").getFullList({ expand: "reviews_via_restaurant" });
|
const restaurants = await pb.collection("restaurants").getFullList({ expand: "tags,reviews_via_restaurant" });
|
||||||
|
|
||||||
restaurants.forEach((restaurant) => {
|
restaurants.forEach((restaurant) => {
|
||||||
const ratings: number[] = [];
|
const ratings: number[] = [];
|
||||||
restaurant.expand?.reviews_via_restaurant.forEach((review) => {
|
restaurant.expand?.reviews_via_restaurant?.forEach((review) => {
|
||||||
ratings.push(review.rating);
|
ratings.push(review.rating);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -26,6 +26,12 @@ onMounted(async () => {
|
||||||
} else {
|
} else {
|
||||||
restaurant.average_rating = null;
|
restaurant.average_rating = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (restaurant.expand?.tags !== undefined) {
|
||||||
|
restaurant.tags = restaurant.expand.tags.map((tag) => tag.name);
|
||||||
|
} else {
|
||||||
|
restaurant.tags = [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(restaurants);
|
console.log(restaurants);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue