feat: login
This commit is contained in:
parent
8a9bad2eb3
commit
5e0f9e4cd8
|
|
@ -1,3 +1,3 @@
|
|||
<template>
|
||||
<h1>Asia Golf</h1>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import Logo from "@/assets/images/logo-white.jpg";
|
||||
import LoginForm from "@/features/auth/components/LoginForm.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto border bg-white p-8 shadow-xl shadow-white/20 md:w-[500px] md:rounded-lg"
|
||||
>
|
||||
<img :src="Logo" title="Asia Golf" alt="asia-golf" class="w-full" />
|
||||
<hr class="my-8" />
|
||||
<login-form />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useForm } from "vee-validate";
|
||||
import { object, string } from "zod";
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import { useLogin } from "@/features/auth/composables/useLogin";
|
||||
import LoginSuccess from "@/features/auth/components/partials/feedbacks/LoginSuccess.vue";
|
||||
import LoginFailed from "@/features/auth/components/partials/feedbacks/LoginFailed.vue";
|
||||
import { sleep } from "@/helpers/sleep";
|
||||
|
||||
const router = useRouter();
|
||||
const { login } = useLogin();
|
||||
const state = reactive({
|
||||
showPassword: false,
|
||||
loggingIn: false,
|
||||
disableForm: false,
|
||||
showLoginSuccessFeedback: false,
|
||||
showLoginFailedFeedback: false,
|
||||
});
|
||||
|
||||
const { errors, handleSubmit, defineField } = useForm({
|
||||
validationSchema: toTypedSchema(
|
||||
object({
|
||||
email: string({ message: "Email is required" }),
|
||||
password: string({ message: "Password is required" }),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const [email, emailAttrs] = defineField("email");
|
||||
const [password, passwordAttrs] = defineField("password");
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
try {
|
||||
state.disableForm = true;
|
||||
state.showLoginSuccessFeedback = false;
|
||||
state.showLoginFailedFeedback = false;
|
||||
state.loggingIn = true;
|
||||
await login(values);
|
||||
state.showLoginSuccessFeedback = true;
|
||||
await sleep(2000);
|
||||
router.push("/");
|
||||
} catch (e) {
|
||||
state.showLoginFailedFeedback = true;
|
||||
state.disableForm = false;
|
||||
} finally {
|
||||
state.loggingIn = false;
|
||||
}
|
||||
});
|
||||
|
||||
function handleToggleShowPassword() {
|
||||
if (state.showPassword) {
|
||||
state.showPassword = false;
|
||||
return;
|
||||
}
|
||||
|
||||
state.showPassword = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-form @submit="onSubmit">
|
||||
<login-success v-if="state.showLoginSuccessFeedback" />
|
||||
<login-failed v-if="state.showLoginFailedFeedback" />
|
||||
|
||||
<div class="mb-2">
|
||||
<label for="email" class="mb-2 font-semibold">Email</label>
|
||||
<v-text-field
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
variant="outlined"
|
||||
rounded="md"
|
||||
class="mb-3 mt-2"
|
||||
:error-messages="errors.email"
|
||||
:disabled="state.disableForm"
|
||||
v-model="email"
|
||||
v-bind="emailAttrs"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="password" class="mb-2 font-semibold">Password</label>
|
||||
<v-text-field
|
||||
:type="state.showPassword ? 'text' : 'password'"
|
||||
placeholder="Enter your password"
|
||||
variant="outlined"
|
||||
rounded="md"
|
||||
class="mb-3 mt-2"
|
||||
:append-inner-icon="state.showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:error-messages="errors.password"
|
||||
:disabled="state.disableForm"
|
||||
v-model="password"
|
||||
v-bind="passwordAttrs"
|
||||
@click:append-inner="handleToggleShowPassword"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-btn
|
||||
type="submit"
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
class="bg-black p-6 text-white"
|
||||
:loading="state.loggingIn"
|
||||
:disabled="state.disableForm"
|
||||
block
|
||||
>
|
||||
Login
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<v-alert
|
||||
closable
|
||||
icon="mdi-alert-circle-outline"
|
||||
title="Failed to login"
|
||||
text="Please check your account credentials"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-8"
|
||||
></v-alert>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<v-alert
|
||||
closable
|
||||
icon="mdi-check-circle-outline"
|
||||
title="Login success"
|
||||
text="Please wait, redirecting..."
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-8"
|
||||
></v-alert>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { useAuth } from "@/features/auth/stores/useAuth";
|
||||
|
||||
export function useCurrentUser() {
|
||||
const { auth } = useAuth();
|
||||
|
||||
return { user: auth.auth?.user };
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import LoginCard from "@/features/auth/components/LoginCard.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-dvh w-full bg-black">
|
||||
<main class="flex h-full justify-center md:items-center">
|
||||
<login-card />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -10,6 +10,7 @@ import * as components from "vuetify/components";
|
|||
import * as directives from "vuetify/directives";
|
||||
|
||||
import App from "@/App.vue";
|
||||
import { router } from "@/router";
|
||||
|
||||
import "@mdi/font/css/materialdesignicons.css";
|
||||
import "@fontsource/poppins";
|
||||
|
|
@ -33,5 +34,6 @@ const app = createApp(App);
|
|||
|
||||
app.use(vuetify);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(VueQueryPlugin);
|
||||
app.mount("#app");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCurrentUser } from "@/features/auth/composables/useCurrentUser";
|
||||
import { useLogout } from "@/features/auth/composables/useLogout";
|
||||
import Logo from "@/assets/images/logo-white.jpg";
|
||||
|
||||
const router = useRouter();
|
||||
const { user } = useCurrentUser();
|
||||
const { logout } = useLogout();
|
||||
const state = reactive({ isLoggingOut: false });
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
state.isLoggingOut = true;
|
||||
await logout();
|
||||
router.push("/login");
|
||||
} catch (e) {
|
||||
state.isLoggingOut = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 border-b bg-slate-50/50 p-4"
|
||||
>
|
||||
<RouterLink to="/"
|
||||
><img :src="Logo" title="Asia Golf" alt="asia-golf" class="h-8"
|
||||
/></RouterLink>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<v-tooltip :text="user?.name" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-avatar
|
||||
:image="user?.photo"
|
||||
class="border"
|
||||
v-bind="props"
|
||||
></v-avatar> </template
|
||||
></v-tooltip>
|
||||
<v-btn
|
||||
type="submit"
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
class="bg-black text-white"
|
||||
:loading="state.isLoggingOut"
|
||||
@click="handleLogout"
|
||||
>
|
||||
Logout
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { useAuth } from "@/features/auth/stores/useAuth";
|
||||
|
||||
import LoginPage from "@/features/auth/pages/LoginPage.vue";
|
||||
import DashboardPage from "@/pages/DashboardPage.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: "/", component: DashboardPage },
|
||||
{ path: "/login", component: LoginPage },
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach((to, _, next) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
if (to.path === "/login" && isAuthenticated) {
|
||||
next({ path: "/" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (to.path !== "/login" && !isAuthenticated) {
|
||||
next({ path: "/login" });
|
||||
return;
|
||||
} else next();
|
||||
});
|
||||
Loading…
Reference in New Issue