init repo
This commit is contained in:
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_ENV=development
|
||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_ENV=production
|
||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Vue 3 + Vite ❤
|
||||
|
||||
Este projeto é uma base para aplicações front-end Vue 3, utilizando Vite para um desenvolvimento rápido e eficiente. Abaixo estão as bibliotecas mais importantes utilizadas neste projeto:
|
||||
|
||||
## Dependências Principais
|
||||
|
||||
- **Vue**: Um framework progressivo para a construção de interfaces de usuário. Ele é projetado para ser incrementalmente adotável, o que significa que você pode usá-lo para desenvolver desde pequenas partes de uma aplicação até grandes aplicações de página única.
|
||||
|
||||
- **Quasar**: Um framework Vue.js de alto desempenho que permite construir websites responsivos, PWAs (Progressive Web Apps), aplicações SSR (Server-Side Rendered), SPAs (Single-Page Applications), aplicações móveis e aplicações desktop a partir de uma única base de código.
|
||||
|
||||
- **Vue Router**: O roteador oficial para Vue.js. Ele se integra profundamente com o Vue.js para facilitar a construção de Single Page Applications robustas com roteamento dinâmico de componentes.
|
||||
|
||||
- **Vuex**: A biblioteca oficial de gerenciamento de estado para aplicações Vue.js. Ele serve como um armazenamento centralizado para todos os componentes em uma aplicação, com regras que garantem que o estado só possa ser modificado de forma previsível.
|
||||
|
||||
- **Axios**: Um cliente HTTP baseado em Promises para o navegador e Node.js. É amplamente utilizado para fazer requisições HTTP (GET, POST, PUT, DELETE, etc.) a APIs externas ou internas.
|
||||
|
||||
- **Day.js**: Uma biblioteca JavaScript minimalista que analisa, valida, manipula e exibe datas e horas para navegadores modernos. Possui uma API amplamente compatível com Moment.js, mas com um tamanho de arquivo muito menor.
|
||||
|
||||
- **Lodash**: Uma biblioteca utilitária JavaScript moderna que oferece modularidade, desempenho e recursos extras. Ela fornece funções para manipulação de arrays, objetos, strings, números e muito mais, tornando o código mais conciso e legível.
|
||||
|
||||
- **@quasar/extras**: Fornece ativos extras para o Quasar, como ícones (Material Icons, Font Awesome, etc.) e fontes, que são essenciais para a estilização e a interface do usuário das aplicações Quasar.
|
||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta author="Leonardo Mortari" />
|
||||
<meta description="This project is a front-end base project with Vue app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<title>Vue Base JS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "vue-frontend-base-js",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "1.17.0",
|
||||
"@vee-validate/zod": "4.15.1",
|
||||
"axios": "1.11.0",
|
||||
"dayjs": "1.11.13",
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash": "4.17.21",
|
||||
"quasar": "2.18.2",
|
||||
"vee-validate": "4.15.1",
|
||||
"vue": "3.5.18",
|
||||
"vue-router": "4.5.1",
|
||||
"vuex": "4.1.0",
|
||||
"zod": "4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/vite-plugin": "1.10.0",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"sass-embedded": "1.90.0",
|
||||
"vite": "7.1.1"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal 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 |
78
src/App.vue
Normal file
78
src/App.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<q-layout view="lHh lpr lff">
|
||||
<q-header v-if="!isLoginPage" reveal class="bg-primary text-white">
|
||||
<q-toolbar>
|
||||
<Button dense flat round icon="mdi-menu" @click="toggleLeftDrawer" />
|
||||
<q-toolbar-title> {{ currentRouteTitle }} </q-toolbar-title>
|
||||
<Toggle v-model="darkMode" @toggle="toggleDarkMode" />
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-if="!isLoginPage" show-if-above v-model="leftDrawerOpen" side="left" bordered>
|
||||
<div class="row q-pa-md items-center">
|
||||
<div class="col q-ml-xs title">
|
||||
<span>Clipper AI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-list>
|
||||
<q-item v-for="route in routes" clickable v-ripple :to="route.path">
|
||||
<q-item-section>{{ route.title }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dark } from "quasar";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Toggle from "@components/Toggle";
|
||||
|
||||
import { routes } from "./auth/router";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Button,
|
||||
Toggle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
leftDrawerOpen: false,
|
||||
darkMode: Dark.isActive,
|
||||
routes,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleLeftDrawer() {
|
||||
this.leftDrawerOpen = !this.leftDrawerOpen;
|
||||
},
|
||||
toggleDarkMode() {
|
||||
Dark.toggle();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentRouteTitle() {
|
||||
const route = this.$route;
|
||||
return route.meta?.title || route.name || 'Clipper AI';
|
||||
},
|
||||
isLoginPage() {
|
||||
return this.$route.path === '/login';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
font-size: 24px;
|
||||
color: $primary;
|
||||
}
|
||||
</style>
|
||||
BIN
src/assets/logo_clipperai.png
Normal file
BIN
src/assets/logo_clipperai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal 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="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
3
src/auth/roles.js
Normal file
3
src/auth/roles.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
VIDEOS_LIST: "role_videos_6550",
|
||||
};
|
||||
100
src/auth/router.js
Normal file
100
src/auth/router.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
import { createWebHistory, createRouter } from "vue-router";
|
||||
|
||||
import roles from "@/auth/roles";
|
||||
|
||||
import Videos from "@/routes/videos";
|
||||
import Login from "@/routes/auth/Login.vue";
|
||||
|
||||
const getUserRoles = () => {
|
||||
const rolesFromCookie = Cookies.get("user_roles"); // TODO: Tirar as permissões do usuário
|
||||
|
||||
return rolesFromCookie ? JSON.parse(rolesFromCookie) : [];
|
||||
};
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
meta: {
|
||||
guest: true,
|
||||
title: "Login",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/videos",
|
||||
component: Videos,
|
||||
name: "Videos",
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "Vídeos",
|
||||
permissions: [roles.VIDEOS_LIST],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
redirect: (to) => {
|
||||
return Cookies.get("token") ? "/videos" : "/login";
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
redirect: (to) => {
|
||||
console.log(Cookies);
|
||||
return Cookies.get("token") ? "/videos" : "/login";
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export const isAuthenticated = () => {
|
||||
return !!Cookies.get("token");
|
||||
};
|
||||
|
||||
const hasPermission = (requiredPermissions = []) => {
|
||||
if (!requiredPermissions.length) return true;
|
||||
|
||||
const userRoles = getUserRoles();
|
||||
|
||||
return requiredPermissions.some((permission) =>
|
||||
userRoles.includes(permission)
|
||||
);
|
||||
};
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} | ClipperIA`;
|
||||
}
|
||||
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
if (!isAuthenticated()) {
|
||||
next({
|
||||
path: "/login",
|
||||
query: { redirect: to.fullPath },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredPermissions = to.meta.permissions || [];
|
||||
|
||||
if (requiredPermissions.length > 0 && !hasPermission(requiredPermissions)) {
|
||||
next({ path: "/unauthorized" });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (to.matched.some((record) => record.meta.guest) && isAuthenticated()) {
|
||||
next({ path: "/" });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
50
src/components/Button/index.vue
Normal file
50
src/components/Button/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<q-btn
|
||||
:icon="icon"
|
||||
:label="label"
|
||||
:loading="loading"
|
||||
:color="color"
|
||||
:type="type"
|
||||
:disable="loading || disabled"
|
||||
:text-color="textColor"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Button",
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
validator: (value) => ["primary", "secondary"].includes(value),
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "button",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
92
src/components/Table/index.vue
Normal file
92
src/components/Table/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<q-card class="q-pa-md" flat bordered>
|
||||
<q-table
|
||||
:title="title"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:row-key="rowKey"
|
||||
:loading="loading"
|
||||
row-key="name"
|
||||
flat
|
||||
bordered
|
||||
virtual-scroll
|
||||
hide-pagination
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<q-inner-loading color="primary" />
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<slot name="no-data" />
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-8 flex justify-end">
|
||||
<q-pagination
|
||||
v-model="pagination.currentPage"
|
||||
color="primary"
|
||||
:max="pagination.totalPages"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-4 flex justify-end items-center">
|
||||
<span class="text-grey-5"
|
||||
>Mostrando {{ pagination.currentPage }} de
|
||||
{{ pagination.totalItems }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import has from "lodash/has";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (value) => value.some((item) => has(item, ["name", "label"])),
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (value) => value.some((item) => has(item, ["name", "label"])),
|
||||
},
|
||||
rowName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "name",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {
|
||||
sortBy: "desc",
|
||||
descending: false,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
columns: props.columns,
|
||||
rows: props.rows,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
31
src/components/TextField/index.vue
Normal file
31
src/components/TextField/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="q-pa-sm">
|
||||
<div>
|
||||
<span>{{ label }} {{ required ? "*" : "" }}</span>
|
||||
</div>
|
||||
|
||||
<q-input outlined v-model="text" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from "vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
text: ref(""),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
30
src/components/Toggle/index.vue
Normal file
30
src/components/Toggle/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<q-toggle
|
||||
v-model="value"
|
||||
v-bind="$attrs"
|
||||
:color="color"
|
||||
@update:model-value="toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Toggle",
|
||||
model: {
|
||||
prop: "value",
|
||||
event: "update:model-value",
|
||||
},
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: "secondary",
|
||||
validator: (value) => ["primary", "secondary"].includes(value),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.$emit("toggle", this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
30
src/main.js
Normal file
30
src/main.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createApp } from "vue";
|
||||
import { Quasar } from "quasar";
|
||||
|
||||
import quasarLang from "quasar/lang/pt-BR";
|
||||
|
||||
import "@quasar/extras/roboto-font/roboto-font.css";
|
||||
import "@quasar/extras/material-icons/material-icons.css";
|
||||
import "@quasar/extras/material-icons-outlined/material-icons-outlined.css";
|
||||
import "@quasar/extras/material-symbols-outlined/material-symbols-outlined.css";
|
||||
import "@quasar/extras/mdi-v7/mdi-v7.css";
|
||||
|
||||
import "quasar/src/css/index.sass";
|
||||
|
||||
import App from "./App.vue";
|
||||
|
||||
import { router } from "./auth/router";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(Quasar, {
|
||||
config: {
|
||||
dark: "auto",
|
||||
},
|
||||
plugins: {},
|
||||
lang: quasarLang,
|
||||
});
|
||||
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
||||
11
src/quasar-variables.sass
Normal file
11
src/quasar-variables.sass
Normal file
@@ -0,0 +1,11 @@
|
||||
$primary : #8200ff
|
||||
$secondary : #eabdff
|
||||
$accent : #8200ff
|
||||
|
||||
$dark : #1c1e21
|
||||
$dark-page : #1c1e21
|
||||
|
||||
$success : #31cb00
|
||||
$error : #ee2e31
|
||||
$info : #226ce0
|
||||
$warning : #fcdc4d
|
||||
166
src/routes/auth/Login.vue
Normal file
166
src/routes/auth/Login.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<q-layout view="hHh lpr fFf">
|
||||
<q-drawer
|
||||
v-model="leftDrawerOpen"
|
||||
show-if-above
|
||||
:width="400"
|
||||
:breakpoint="700"
|
||||
bordered
|
||||
side="right"
|
||||
class="bg-primary text-white"
|
||||
>
|
||||
<q-scroll-area class="fit">
|
||||
<div class="q-pa-md">
|
||||
<q-form @submit.prevent="handleLogin" class="q-mt-lg">
|
||||
<q-input
|
||||
v-model="email"
|
||||
type="email"
|
||||
label="E-mail"
|
||||
lazy-rules
|
||||
:rules="[(val) => !!val || 'Campo obrigatório']"
|
||||
class="q-mb-md"
|
||||
dark
|
||||
filled
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="mail" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Senha"
|
||||
lazy-rules
|
||||
:rules="[(val) => !!val || 'Campo obrigatório']"
|
||||
class="q-mb-lg"
|
||||
dark
|
||||
filled
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="lock" />
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
label="Entrar"
|
||||
color="white"
|
||||
textColor="primary"
|
||||
full-width
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-form>
|
||||
|
||||
<p v-if="error" class="text-negative q-mt-md">{{ error }}</p>
|
||||
</div>
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<q-page class="flex flex-center bg-black-1">
|
||||
<div class="text-center q-pa-md">
|
||||
<div class="text-h4 q-mb-md">Clipper IA</div>
|
||||
|
||||
<p class="text-grey-8">Cortes automaticos de vídeos</p>
|
||||
</div>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Button from "@components/Button";
|
||||
|
||||
export default {
|
||||
name: "LoginView",
|
||||
components: {
|
||||
Button,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
leftDrawerOpen: true,
|
||||
email: "",
|
||||
password: "",
|
||||
showPassword: false,
|
||||
error: "",
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleLogin() {
|
||||
this.error = "";
|
||||
|
||||
if (!this.email || !this.password) {
|
||||
this.error = "Por favor, preencha todos os campos";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
// setTimeout(() => {
|
||||
// try {
|
||||
// localStorage.setItem("auth_token", "dummy_token");
|
||||
|
||||
// const redirectPath = this.$route.query.redirect || "/";
|
||||
// this.$router.push(redirectPath);
|
||||
// } catch (err) {
|
||||
// this.error = err.message || "Erro ao fazer login. Tente novamente.";
|
||||
// } finally {
|
||||
// this.loading = false;
|
||||
// }
|
||||
// }, 1000);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (window.innerWidth < 700) {
|
||||
this.leftDrawerOpen = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Estilos personalizados para o formulário de login */
|
||||
.q-drawer {
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Ajustes para responsividade */
|
||||
@media (max-width: 700px) {
|
||||
.q-drawer {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.q-page-container {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilo para o botão de login */
|
||||
.q-btn--actionable {
|
||||
transition: transform 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
.q-btn--actionable:active:not(.disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Melhorias na acessibilidade */
|
||||
.q-field--filled .q-field__control {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Efeito de hover nos inputs */
|
||||
.q-field--filled:not(.q-field--readonly):hover .q-field__control:before {
|
||||
border-color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
</style>
|
||||
192
src/routes/videos/index.vue
Normal file
192
src/routes/videos/index.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="user-list q-pa-md">
|
||||
<q-card flat bordered class="q-pa-sm q-mb-lg">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<TextField label="Nome" required />
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<Table :columns="columns" :rows="rows" :pagination="pagination">
|
||||
<template #no-data>
|
||||
<div
|
||||
class="full-width row flex-center q-gutter-sm"
|
||||
style="font-size: 1.3em"
|
||||
>
|
||||
<span> Infelizmente, não há dados para serem mostrados </span>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Table from "@components/Table";
|
||||
import TextField from "@components/TextField";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: "name",
|
||||
required: true,
|
||||
label: "Dessert (100g serving)",
|
||||
align: "left",
|
||||
field: (row) => row.name,
|
||||
format: (val) => `${val}`,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "calories",
|
||||
align: "center",
|
||||
label: "Calories",
|
||||
field: "calories",
|
||||
sortable: true,
|
||||
},
|
||||
{ name: "fat", label: "Fat (g)", field: "fat", sortable: true },
|
||||
{ name: "carbs", label: "Carbs (g)", field: "carbs" },
|
||||
{ name: "protein", label: "Protein (g)", field: "protein" },
|
||||
{ name: "sodium", label: "Sodium (mg)", field: "sodium" },
|
||||
{
|
||||
name: "calcium",
|
||||
label: "Calcium (%)",
|
||||
field: "calcium",
|
||||
sortable: true,
|
||||
sort: (a, b) => parseInt(a, 10) - parseInt(b, 10),
|
||||
},
|
||||
{
|
||||
name: "iron",
|
||||
label: "Iron (%)",
|
||||
field: "iron",
|
||||
sortable: true,
|
||||
sort: (a, b) => parseInt(a, 10) - parseInt(b, 10),
|
||||
},
|
||||
];
|
||||
|
||||
const rows = [
|
||||
{
|
||||
name: "Frozen Yogurt",
|
||||
calories: 159,
|
||||
fat: 6.0,
|
||||
carbs: 24,
|
||||
protein: 4.0,
|
||||
sodium: 87,
|
||||
calcium: "14%",
|
||||
iron: "1%",
|
||||
},
|
||||
{
|
||||
name: "Ice cream sandwich",
|
||||
calories: 237,
|
||||
fat: 9.0,
|
||||
carbs: 37,
|
||||
protein: 4.3,
|
||||
sodium: 129,
|
||||
calcium: "8%",
|
||||
iron: "1%",
|
||||
},
|
||||
{
|
||||
name: "Eclair",
|
||||
calories: 262,
|
||||
fat: 16.0,
|
||||
carbs: 23,
|
||||
protein: 6.0,
|
||||
sodium: 337,
|
||||
calcium: "6%",
|
||||
iron: "7%",
|
||||
},
|
||||
{
|
||||
name: "Cupcake",
|
||||
calories: 305,
|
||||
fat: 3.7,
|
||||
carbs: 67,
|
||||
protein: 4.3,
|
||||
sodium: 413,
|
||||
calcium: "3%",
|
||||
iron: "8%",
|
||||
},
|
||||
{
|
||||
name: "Gingerbread",
|
||||
calories: 356,
|
||||
fat: 16.0,
|
||||
carbs: 49,
|
||||
protein: 3.9,
|
||||
sodium: 327,
|
||||
calcium: "7%",
|
||||
iron: "16%",
|
||||
},
|
||||
{
|
||||
name: "Jelly bean",
|
||||
calories: 375,
|
||||
fat: 0.0,
|
||||
carbs: 94,
|
||||
protein: 0.0,
|
||||
sodium: 50,
|
||||
calcium: "0%",
|
||||
iron: "0%",
|
||||
},
|
||||
{
|
||||
name: "Lollipop",
|
||||
calories: 392,
|
||||
fat: 0.2,
|
||||
carbs: 98,
|
||||
protein: 0,
|
||||
sodium: 38,
|
||||
calcium: "0%",
|
||||
iron: "2%",
|
||||
},
|
||||
{
|
||||
name: "Honeycomb",
|
||||
calories: 408,
|
||||
fat: 3.2,
|
||||
carbs: 87,
|
||||
protein: 6.5,
|
||||
sodium: 562,
|
||||
calcium: "0%",
|
||||
iron: "45%",
|
||||
},
|
||||
{
|
||||
name: "Donut",
|
||||
calories: 452,
|
||||
fat: 25.0,
|
||||
carbs: 51,
|
||||
protein: 4.9,
|
||||
sodium: 326,
|
||||
calcium: "2%",
|
||||
iron: "22%",
|
||||
},
|
||||
{
|
||||
name: "KitKat",
|
||||
calories: 518,
|
||||
fat: 26.0,
|
||||
carbs: 65,
|
||||
protein: 7,
|
||||
sodium: 54,
|
||||
calcium: "12%",
|
||||
iron: "6%",
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "UserList",
|
||||
components: {
|
||||
Table,
|
||||
TextField,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 10,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-list {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
28
vite.config.js
Normal file
28
vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import path from "node:path";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { quasar, transformAssetUrls } from "@quasar/vite-plugin";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src/"),
|
||||
"@assets": path.resolve(__dirname, "src/assets/"),
|
||||
"@components": path.resolve(__dirname, "src/components/"),
|
||||
"@utils": path.resolve(__dirname, "src/utils/"),
|
||||
},
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
quasar({
|
||||
sassVariables: fileURLToPath(
|
||||
new URL("./src/quasar-variables.sass", import.meta.url)
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user