Initial commit

main
Eloi Zalczer 2024-06-10 18:13:30 +02:00
commit 29f8902f26
31 changed files with 3597 additions and 0 deletions

24
.gitignore vendored 100644
View File

@ -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?

3
.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

7
README.md 100644
View File

@ -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

13
index.html 100644
View File

@ -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>

2641
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

30
package.json 100644
View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
public/vite.svg 100644
View File

@ -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

13
src/App.vue 100644
View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(initialMap.value);
});
</script>
<template>
<div>
<div id="map" style="height: 90vh"></div>
</div>
</template>

View File

@ -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>

14
src/main.ts 100644
View File

@ -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");

14
src/pocketbase.ts 100644
View File

@ -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();
}

View File

@ -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;

View File

@ -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 }
})

101
src/style.css 100644
View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [require("daisyui")],
};

17
vite.config.js 100644
View File

@ -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,
},
});