Initial commit
commit
29f8902f26
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously Volar) and disable Vetur
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "doxfood-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vee-validate/yup": "^4.13.0",
|
||||||
|
"daisyui": "^4.12.2",
|
||||||
|
"datatables.net-vue3": "^3.0.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"pocketbase": "^0.21.3",
|
||||||
|
"vee-validate": "^4.13.0",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-router": "^4.3.2",
|
||||||
|
"yup": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"vite": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from "vue-router";
|
||||||
|
import "./style.css";
|
||||||
|
import NavigationBar from "./components/NavigationBar.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-screen w-full flex flex-col m-0">
|
||||||
|
<NavigationBar />
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Fira Sans",
|
||||||
|
"Droid Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
.dt-input {
|
||||||
|
@apply input;
|
||||||
|
@apply input-sm;
|
||||||
|
@apply input-bordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-full:has(.dt-length) {
|
||||||
|
@apply grid;
|
||||||
|
@apply grid-cols-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-length {
|
||||||
|
@apply m-3;
|
||||||
|
@apply justify-self-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-search {
|
||||||
|
@apply m-3;
|
||||||
|
@apply px-3;
|
||||||
|
@apply justify-self-end;
|
||||||
|
|
||||||
|
.dt-input {
|
||||||
|
@apply mx-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTable {
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
@apply hover:bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
.form-label {
|
||||||
|
@apply flex;
|
||||||
|
@apply items-center;
|
||||||
|
@apply gap-2;
|
||||||
|
@apply text-sm;
|
||||||
|
@apply font-bold;
|
||||||
|
@apply uppercase;
|
||||||
|
@apply text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
@apply input;
|
||||||
|
@apply input-bordered;
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
@apply select;
|
||||||
|
@apply select-bordered;
|
||||||
|
@apply my-0;
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-error {
|
||||||
|
@apply input;
|
||||||
|
@apply input-bordered;
|
||||||
|
@apply w-full;
|
||||||
|
@apply input-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select-error {
|
||||||
|
@apply select;
|
||||||
|
@apply select-bordered;
|
||||||
|
@apply my-0;
|
||||||
|
@apply w-full;
|
||||||
|
@apply input-error;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { type PropType } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
model: { type: Array as PropType<string[]> },
|
||||||
|
options: { type: Array as PropType<string[]> },
|
||||||
|
labels: { type: Map as PropType<Map<string, string>>, default: () => {} },
|
||||||
|
clearable: { type: Boolean, default: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["updateModelValue"]);
|
||||||
|
|
||||||
|
const query = ref<string>("");
|
||||||
|
|
||||||
|
function onBadgeClicked(value: string) {
|
||||||
|
if (props.model === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newval = props.model.includes(value)
|
||||||
|
? props.model.filter((val) => val != value)
|
||||||
|
: [...props.model, value];
|
||||||
|
|
||||||
|
emit("updateModelValue", newval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOptionClicked(value: string) {
|
||||||
|
if (props.model === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newval = props.model?.includes(value)
|
||||||
|
? props.model?.filter((val) => val !== value)
|
||||||
|
: [...props.model, value];
|
||||||
|
|
||||||
|
query.value = "";
|
||||||
|
emit("updateModelValue", newval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showClear() {
|
||||||
|
if (!props.clearable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Array.isArray(props.model) && props.model?.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
emit("updateModelValue", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredOptions() {
|
||||||
|
if (!query.value) return props.options;
|
||||||
|
const lcquery = query.value.toLocaleLowerCase();
|
||||||
|
return props.options?.filter((opt) =>
|
||||||
|
opt.toLocaleLowerCase().startsWith(lcquery)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="dropdown" class="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"
|
||||||
|
>
|
||||||
|
<div class="mx-2 flex flex-wrap items-center">
|
||||||
|
<div
|
||||||
|
v-for="(value, idx) in model"
|
||||||
|
:key="`${value}-${idx}`"
|
||||||
|
class="badge m-1"
|
||||||
|
>
|
||||||
|
<button class="mr-2" @click="onBadgeClicked(value)">
|
||||||
|
<font-awesome-icon icon="xmark" />
|
||||||
|
</button>
|
||||||
|
{{ labels?.get(value) ?? value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="query"
|
||||||
|
class="input input-ghost h-auto min-w-[8ch] flex-1 border-0 p-0 focus:outline-0"
|
||||||
|
role="combobox"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-show="showClear"
|
||||||
|
class="btn btn-ghost h-auto self-stretch"
|
||||||
|
@click="onClear"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="xmark" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
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)"
|
||||||
|
>
|
||||||
|
<font-awesome-icon v-show="model?.includes(option)" icon="check" />
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
|
import { currentUser, signOut } from "../pocketbase";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="navbar w-full bg-base-100 sm:border-b sm:border-b-neutral-200 flex flex-row place-content-center"
|
||||||
|
>
|
||||||
|
<div class="flex-1 flex-row">
|
||||||
|
<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="/list">List</RouterLink>
|
||||||
|
<RouterLink class="btn btn-ghost" to="/create"
|
||||||
|
>Create Restaurant</RouterLink
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<div class="flex-none">
|
||||||
|
<div class="flex flew-row" v-if="currentUser">
|
||||||
|
<label>
|
||||||
|
Logged in as {{ currentUser.username }}
|
||||||
|
<button
|
||||||
|
v-if="currentUser"
|
||||||
|
@click="signOut"
|
||||||
|
class="btn btn-ghost text-red-600"
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RouterLink v-else class="btn btn-ghost" to="/login">
|
||||||
|
Sign In
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import * as L from "leaflet";
|
||||||
|
|
||||||
|
const initialMap = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initialMap.value = L.map("map").setView([23.8041, 90.4152], 6);
|
||||||
|
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution:
|
||||||
|
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
}).addTo(initialMap.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div id="map" style="height: 90vh"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import "../assets/datatable.css";
|
||||||
|
|
||||||
|
import DataTable from "datatables.net-vue3";
|
||||||
|
import DataTablesCore from "datatables.net";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
|
||||||
|
DataTable.use(DataTablesCore);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Array as PropType<Object[]> },
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ data: "name", title: "Name" },
|
||||||
|
{ data: "tags", title: "Tags" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DataTable
|
||||||
|
class="table"
|
||||||
|
:columns="columns"
|
||||||
|
:data="props.data"
|
||||||
|
:options="{
|
||||||
|
layout: {
|
||||||
|
top: ['pageLength', null, 'search'],
|
||||||
|
topStart: null,
|
||||||
|
topEnd: null,
|
||||||
|
bottom: 'paging',
|
||||||
|
bottomStart: null,
|
||||||
|
bottomEnd: null,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
|
import App from "./App.vue";
|
||||||
|
import router from "./router";
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.use(createPinia());
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const pb = new PocketBase("http://127.0.0.1:8080");
|
||||||
|
|
||||||
|
export const currentUser = ref();
|
||||||
|
|
||||||
|
pb.authStore.onChange(() => {
|
||||||
|
currentUser.value = pb.authStore.model;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
export function signOut() {
|
||||||
|
pb.authStore.clear();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import HomeView from "../views/HomeView.vue";
|
||||||
|
|
||||||
|
import { currentUser } from "../pocketbase";
|
||||||
|
|
||||||
|
async function redirectToLogin(to: any, from: any) {
|
||||||
|
if (!currentUser.value) {
|
||||||
|
return { name: "login" };
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preventLoginAccess(to: any, from: any) {
|
||||||
|
if (currentUser.value || from.name === undefined) {
|
||||||
|
return { name: "home" };
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/about",
|
||||||
|
name: "about",
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import("../views/AboutView.vue"),
|
||||||
|
beforeEnter: redirectToLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/random",
|
||||||
|
name: "random",
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (random.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import("../views/RandomView.vue"),
|
||||||
|
beforeEnter: redirectToLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/create",
|
||||||
|
name: "create",
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (create.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import("../views/CreateView.vue"),
|
||||||
|
beforeEnter: redirectToLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/list",
|
||||||
|
name: "list",
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (list.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import("../views/ListView.vue"),
|
||||||
|
beforeEnter: redirectToLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: () => import("../views/LoginView.vue"),
|
||||||
|
beforeEnter: preventLoginAccess,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/signup",
|
||||||
|
name: "signup",
|
||||||
|
component: () => import("../views/SignUpView.vue"),
|
||||||
|
beforeEnter: preventLoginAccess,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
@import "./assets/base.css";
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 100%;
|
||||||
|
@apply m-0;
|
||||||
|
@apply p-0;
|
||||||
|
text-align: center;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col place-content-center items-center h-full">
|
||||||
|
<h1>DOXFOOD</h1>
|
||||||
|
<h4>Developed by Eloi Zalczer</h4>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<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 MultiSelectField from "../components/MultiSelectField.vue";
|
||||||
|
import { 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.string().required("This field is required."),
|
||||||
|
price_range: yup.string().required("This field is required."),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((values) => {
|
||||||
|
pb.collection("restaurants").create(values);
|
||||||
|
resetForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagsModel = ref<string[]>([]);
|
||||||
|
|
||||||
|
const [name, nameAttrs] = defineField("name");
|
||||||
|
const [tags, tagsAttrs] = defineField("tags");
|
||||||
|
const [price, priceAttrs] = defineField("price_range");
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const tags = await pb.collection("tags").getFullList();
|
||||||
|
tagsModel.value = tags.map((record) => record.name);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="flex flex-col place-content-center items-center h-full"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<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">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Tags </label>
|
||||||
|
<MultiSelectField :model="tags" :options="tagsModel" />
|
||||||
|
<input
|
||||||
|
:class="
|
||||||
|
errors.cuisine_type === undefined
|
||||||
|
? 'form-field'
|
||||||
|
: 'form-field-error'
|
||||||
|
"
|
||||||
|
v-model="cuisine"
|
||||||
|
v-bind="cuisineAttrs"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<label class="text-red-500">{{ errors.cuisine_type }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap w-1/2 place-content-center">
|
||||||
|
<div class="mx-5 my-4">
|
||||||
|
<label class="form-label"> Price </label>
|
||||||
|
<input
|
||||||
|
:class="
|
||||||
|
errors.price_range === undefined ? 'form-field' : 'form-field-error'
|
||||||
|
"
|
||||||
|
v-model="price"
|
||||||
|
v-bind="priceAttrs"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<label class="text-red-500">{{ errors.price_range }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline">Create</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-column place-content-center items-center h-full">
|
||||||
|
<h1 class="">DOXFOOD</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { pb } from "../pocketbase";
|
||||||
|
import RestaurantsTable from "../components/RestaurantsTable.vue";
|
||||||
|
import RestaurantsMap from "../components/RestaurantsMap.vue";
|
||||||
|
|
||||||
|
const data = ref<Object[]>([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const restaurants = await pb.collection("restaurants").getFullList();
|
||||||
|
data.value = restaurants;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col-reverse sm:flex-row">
|
||||||
|
<div class="w-full flex-1">
|
||||||
|
<RestaurantsTable :data="data" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<RestaurantsMap />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { currentUser, pb } from "../pocketbase";
|
||||||
|
import "@/assets/form.css";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
const username = ref<string>();
|
||||||
|
const password = ref<string>();
|
||||||
|
|
||||||
|
const valid = computed(() => {
|
||||||
|
return username.value !== undefined && password.value !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
if (!valid.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pb.collection("users").authWithPassword(username.value, password.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p v-if="currentUser">Signed in as {{ currentUser.username }}</p>
|
||||||
|
<form
|
||||||
|
v-else
|
||||||
|
class="flex flex-col place-content-center items-center h-full"
|
||||||
|
@submit.prevent=""
|
||||||
|
>
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Username </label>
|
||||||
|
<input class="form-field" type="text" v-model="username" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Password </label>
|
||||||
|
<input class="form-field" type="password" v-model="password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="login" :disabled="!valid">Log In</button>
|
||||||
|
<p>
|
||||||
|
New to DOXFOOD ? <RouterLink to="/signup">Create an account !</RouterLink>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
import { pb } from "../pocketbase.ts";
|
||||||
|
|
||||||
|
const restaurant = ref<string>();
|
||||||
|
|
||||||
|
async function getRandomRestaurant() {
|
||||||
|
const result = await pb
|
||||||
|
.collection("restaurants")
|
||||||
|
.getFirstListItem("", { sort: "@random" });
|
||||||
|
restaurant.value = result.name;
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from "vee-validate";
|
||||||
|
import { toTypedSchema } from "@vee-validate/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { currentUser, pb } from "../pocketbase";
|
||||||
|
import "@/assets/form.css";
|
||||||
|
|
||||||
|
const error = ref<string>();
|
||||||
|
|
||||||
|
const { meta, errors, handleSubmit, defineField, resetForm } = useForm({
|
||||||
|
validationSchema: toTypedSchema(
|
||||||
|
yup.object({
|
||||||
|
username: yup.string().required("This field is required."),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.required("This field is required.")
|
||||||
|
.min(8, "Password too short.")
|
||||||
|
.max(72, "Password too long."),
|
||||||
|
passwordConfirm: yup
|
||||||
|
.string()
|
||||||
|
.required("This field is required.")
|
||||||
|
.oneOf([yup.ref("password")], "Passwords do not match."),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
username: values["username"],
|
||||||
|
password: values["password"],
|
||||||
|
passwordConfirm: values["passwordConfirm"],
|
||||||
|
};
|
||||||
|
|
||||||
|
await pb.collection("users").create(data);
|
||||||
|
await pb
|
||||||
|
.collection("users")
|
||||||
|
.authWithPassword(values["username"], values["password"]);
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [username, usernameAttrs] = defineField("username");
|
||||||
|
const [password, passwordAttrs] = defineField("password");
|
||||||
|
const [passwordConfirm, passwordConfirmAttrs] = defineField("passwordConfirm");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p v-if="currentUser">Signed in as {{ currentUser.username }}</p>
|
||||||
|
<form
|
||||||
|
v-else
|
||||||
|
class="flex flex-col place-content-center items-center h-full"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Username </label>
|
||||||
|
<input
|
||||||
|
class="form-field"
|
||||||
|
type="text"
|
||||||
|
v-model="username"
|
||||||
|
v-bind="usernameAttrs"
|
||||||
|
/>
|
||||||
|
<label class="text-red-500">{{ errors.username }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Password </label>
|
||||||
|
<input
|
||||||
|
class="form-field"
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
v-bind="passwordAttrs"
|
||||||
|
/>
|
||||||
|
<label class="text-red-500">{{ errors.password }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<div class="mx-5 my-4 w-full">
|
||||||
|
<label class="form-label"> Confirm password </label>
|
||||||
|
<input
|
||||||
|
class="form-field"
|
||||||
|
type="password"
|
||||||
|
v-model="passwordConfirm"
|
||||||
|
v-bind="passwordConfirmAttrs"
|
||||||
|
/>
|
||||||
|
<label class="text-red-500">{{ errors.passwordConfirm }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error != undefined">{{ error }}</p>
|
||||||
|
|
||||||
|
<button class="btn btn-outline">Sign Up</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("daisyui")],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
cors: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue