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