443 lines
10 KiB
Vue
443 lines
10 KiB
Vue
<template>
|
|
<q-page class="login-page">
|
|
<div class="login-page__container">
|
|
<section class="login-page__hero">
|
|
<div class="login-page__hero-content">
|
|
<div class="login-page__brand">
|
|
<q-avatar size="48px" color="white" text-color="primary"
|
|
>CI</q-avatar
|
|
>
|
|
<div>
|
|
<div class="login-page__brand-title">Clipper IA</div>
|
|
<div class="login-page__brand-subtitle">
|
|
Cortes automáticos com inteligência
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>Produza cortes envolventes sem perder tempo na edição.</h2>
|
|
<p>
|
|
Envie a url do vídeo no Youtube e receba os melhores momentos
|
|
prontos para divulgar em minutos. Nossa IA identifica picos de
|
|
retenção, fala e emoção para gerar clipes certeiros.
|
|
</p>
|
|
|
|
<div class="login-page__stats">
|
|
<div>
|
|
<span class="login-page__stat-value">+3x</span>
|
|
<span class="login-page__stat-label">mais alcance</span>
|
|
</div>
|
|
<div>
|
|
<span class="login-page__stat-value">24/7</span>
|
|
<span class="login-page__stat-label">operando</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="login-card__wrapper">
|
|
<q-card flat bordered class="login-card">
|
|
<div class="login-card__header">
|
|
<div class="login-card__badge">
|
|
<q-icon name="sym_o_bolt" size="20px" />
|
|
<span>Beta exclusivo</span>
|
|
</div>
|
|
<h1>Bem-vindo de volta</h1>
|
|
<p>
|
|
Entre para continuar gerando clipes automaticamente com o Clipper
|
|
IA.
|
|
</p>
|
|
</div>
|
|
|
|
<q-form class="login-card__form">
|
|
<TextField
|
|
v-model="username"
|
|
label="Email ou usuário"
|
|
placeholder="seuemail@clipper.ai"
|
|
required
|
|
>
|
|
<template #prepend>
|
|
<q-icon name="sym_o_person" color="primary" />
|
|
</template>
|
|
</TextField>
|
|
|
|
<TextField
|
|
v-model="password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
label="Senha"
|
|
required
|
|
placeholder="••••••••"
|
|
>
|
|
<template #prepend>
|
|
<q-icon name="sym_o_lock" color="primary" />
|
|
</template>
|
|
<template #append>
|
|
<q-icon
|
|
:name="
|
|
showPassword ? 'sym_o_visibility_off' : 'sym_o_visibility'
|
|
"
|
|
class="cursor-pointer"
|
|
color="primary"
|
|
@click="showPassword = !showPassword"
|
|
/>
|
|
</template>
|
|
</TextField>
|
|
|
|
<div class="login-card__links">
|
|
<Button
|
|
variant="link"
|
|
color="primary"
|
|
text-color="primary"
|
|
label="Esqueci minha senha"
|
|
@click.prevent="handleForgotPassword"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
label="Entrar"
|
|
:loading="loading"
|
|
@click="handleLogin"
|
|
full-width
|
|
/>
|
|
</q-form>
|
|
|
|
<p v-if="error" class="login-card__error">{{ error }}</p>
|
|
</q-card>
|
|
</section>
|
|
</div>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script>
|
|
import Cookies from "js-cookie";
|
|
|
|
import Button from "@components/Button";
|
|
import TextField from "@components/TextField";
|
|
|
|
import { API } from "@config/axios";
|
|
import {
|
|
buildUserProfileFromToken,
|
|
extractRolesFromToken,
|
|
} from "@utils/keycloak";
|
|
|
|
export default {
|
|
name: "LoginView",
|
|
components: {
|
|
Button,
|
|
TextField,
|
|
},
|
|
data() {
|
|
return {
|
|
username: "",
|
|
password: "",
|
|
showPassword: false,
|
|
error: "",
|
|
loading: false,
|
|
};
|
|
},
|
|
methods: {
|
|
async handleLogin() {
|
|
if (!this.username || !this.password) {
|
|
this.error = "Por favor, preencha todos os campos";
|
|
return;
|
|
}
|
|
|
|
this.error = "";
|
|
|
|
try {
|
|
this.loading = true;
|
|
|
|
const params = new URLSearchParams();
|
|
params.append("username", this.username.trim());
|
|
params.append("password", this.password);
|
|
|
|
const { data } = await API.post("/auth/login", params.toString(), {
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
});
|
|
|
|
const accessTokenDays = data.expires_in
|
|
? data.expires_in / (60 * 60 * 24)
|
|
: undefined;
|
|
const refreshTokenDays = data.refresh_expires_in
|
|
? data.refresh_expires_in / (60 * 60 * 24)
|
|
: undefined;
|
|
|
|
const cookieOptions = {
|
|
sameSite: "lax",
|
|
secure: window.location.protocol === "https:",
|
|
path: "/",
|
|
};
|
|
|
|
Cookies.set("token", data.access_token, {
|
|
...cookieOptions,
|
|
expires: accessTokenDays,
|
|
});
|
|
|
|
Cookies.set("refresh_token", data.refresh_token, {
|
|
...cookieOptions,
|
|
expires: refreshTokenDays,
|
|
});
|
|
|
|
const profile = buildUserProfileFromToken(data.access_token);
|
|
const roles = profile?.roles?.length
|
|
? profile.roles
|
|
: extractRolesFromToken(data.access_token);
|
|
|
|
if (roles?.length) {
|
|
Cookies.set("user_roles", JSON.stringify(roles), {
|
|
...cookieOptions,
|
|
expires: accessTokenDays,
|
|
});
|
|
}
|
|
|
|
if (profile) {
|
|
Cookies.set("user_profile", JSON.stringify(profile), {
|
|
...cookieOptions,
|
|
expires: accessTokenDays,
|
|
});
|
|
}
|
|
|
|
await this.$router.push("/videos");
|
|
} catch (error) {
|
|
console.log("ok");
|
|
|
|
this.$q.notify({
|
|
type: "negative",
|
|
message: "Não foi possível entrar. Verifique as credenciais.",
|
|
});
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
handleForgotPassword() {
|
|
this.$q.notify({
|
|
type: "info",
|
|
message: "Entre em contato com o suporte para redefinir sua senha.",
|
|
});
|
|
},
|
|
handleSupport() {
|
|
this.$q.notify({
|
|
type: "info",
|
|
message:
|
|
"Envie um email para suporte@clipper.ai e responderemos rapidamente.",
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.login-page {
|
|
height: 100vh;
|
|
display: flex;
|
|
align-items: stretch;
|
|
justify-content: center;
|
|
background: radial-gradient(
|
|
circle at top left,
|
|
rgba(127, 86, 217, 0.18),
|
|
transparent 55%
|
|
),
|
|
radial-gradient(
|
|
circle at bottom right,
|
|
rgba(83, 56, 158, 0.22),
|
|
transparent 45%
|
|
),
|
|
#f5f6fa;
|
|
}
|
|
|
|
.login-page__container {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(360px, 420px);
|
|
width: min(1100px, 92vw);
|
|
gap: clamp(32px, 6vw, 56px);
|
|
align-items: center;
|
|
padding: clamp(32px, 5vw, 64px) 0;
|
|
}
|
|
|
|
.login-page__hero {
|
|
color: #f8f9ff;
|
|
background: linear-gradient(160deg, #53389e 0%, #7f56d9 60%, #b692f6 100%);
|
|
border-radius: 32px;
|
|
padding: clamp(32px, 5vw, 56px);
|
|
box-shadow: 0 24px 64px rgba(83, 56, 158, 0.35);
|
|
}
|
|
|
|
.login-page__hero h2 {
|
|
font-size: clamp(28px, 3vw, 36px);
|
|
font-weight: 700;
|
|
margin: 32px 0 16px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.login-page__hero p {
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
color: rgba(255, 255, 255, 0.82);
|
|
margin-bottom: 36px;
|
|
}
|
|
|
|
.login-page__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.login-page__brand-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.login-page__brand-subtitle {
|
|
font-size: 13px;
|
|
color: rgba(255, 255, 255, 0.72);
|
|
}
|
|
|
|
.login-page__stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 18px;
|
|
}
|
|
|
|
.login-page__stat-value {
|
|
display: block;
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.login-page__stat-label {
|
|
font-size: 13px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: rgba(255, 255, 255, 0.72);
|
|
}
|
|
|
|
.login-card__wrapper {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.login-card {
|
|
width: 100%;
|
|
border-radius: 24px;
|
|
padding: clamp(28px, 4vw, 40px);
|
|
box-shadow: 0 22px 40px rgba(16, 24, 40, 0.12);
|
|
backdrop-filter: blur(10px);
|
|
background: rgba(255, 255, 255, 0.92);
|
|
}
|
|
|
|
.login-card__header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.login-card__badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
padding: 6px 12px;
|
|
border-radius: 999px;
|
|
background: rgba(127, 86, 217, 0.12);
|
|
color: #53389e;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.login-card__header h1 {
|
|
font-size: clamp(24px, 3vw, 30px);
|
|
font-weight: 700;
|
|
margin: 0;
|
|
color: #101828;
|
|
}
|
|
|
|
.login-card__header p {
|
|
margin: 0;
|
|
color: #475467;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.login-card__form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.login-card__links {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.login-card__error {
|
|
margin-top: 18px;
|
|
color: #f97066;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.login-card__footer {
|
|
margin-top: 28px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
color: #475467;
|
|
font-size: 14px;
|
|
}
|
|
|
|
body.body--dark .login-page {
|
|
background: radial-gradient(
|
|
circle at top left,
|
|
rgba(83, 56, 158, 0.5),
|
|
transparent 55%
|
|
),
|
|
#0b1120;
|
|
}
|
|
|
|
body.body--dark .login-card {
|
|
background: rgba(16, 24, 40, 0.94);
|
|
box-shadow: 0 22px 44px rgba(12, 18, 32, 0.65);
|
|
}
|
|
|
|
body.body--dark .login-card__header h1 {
|
|
color: rgba(255, 255, 255, 0.92);
|
|
}
|
|
|
|
body.body--dark .login-card__header p,
|
|
body.body--dark .login-card__footer {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.login-page__container {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
.login-card {
|
|
max-width: 460px;
|
|
margin: 0 auto;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.login-page {
|
|
background: #f5f6fa;
|
|
}
|
|
|
|
.login-page__container {
|
|
width: 100%;
|
|
padding: 24px 16px 64px;
|
|
}
|
|
|
|
.login-page__hero {
|
|
display: none;
|
|
}
|
|
|
|
.login-card {
|
|
border-radius: 20px;
|
|
padding: 28px 20px;
|
|
}
|
|
}
|
|
</style>
|