Compare commits

..

56 Commits

Author SHA1 Message Date
f1f09029ba base de datos pocketBase 2023-04-02 18:27:34 +02:00
221f89c1e4 eliminado console.log 2023-03-23 15:04:51 +01:00
1753c6ee33 elimina items 2023-03-14 14:34:16 +01:00
14f7d0e67e carga producto 2023-03-14 14:01:38 +01:00
109ad712bf crear item y guardar en la lista 2023-03-14 13:07:09 +01:00
ee21c44afb rutas de navbar 2023-03-14 12:36:08 +01:00
b17f4e7f1e al darle a la papelera entra
- enlaces azules
- botón de crear item y back
- nombre de la lista arriba
- nombre de la lista en el navbar
2023-03-14 11:53:54 +01:00
0da30c0563 pagina de listas individuales 2023-03-13 19:24:02 +01:00
6029364240 q-file restringido por formato o tamaño 2023-03-13 13:11:10 +01:00
6e60580f9d thumbs 2023-03-13 12:51:38 +01:00
4f329ba46a eliminado essentialLinks y rutas en navbar 2023-03-13 12:38:28 +01:00
62a225633c badge items 2023-03-13 12:13:08 +01:00
9fdc68296b estilos y bordes 2023-03-13 12:04:00 +01:00
8ccd713672 espacio con avatar 2023-03-13 12:03:39 +01:00
6fd6b82440 si es una lista campoartida saca al usuario 2023-03-13 11:08:51 +01:00
51b1086341 refresca imagen 2023-03-13 10:53:15 +01:00
525f3f4da7 alinear vista movil 2023-03-13 10:32:38 +01:00
5a2bffe57a eliminar lista 2023-03-12 11:55:59 +01:00
c59133df8d foto de parfil mas arriba 2023-03-12 11:42:33 +01:00
0bbe44b739 papelera puesta 2023-03-10 15:55:31 +01:00
80237f72c9 crear listas 2023-03-10 15:31:29 +01:00
5464b71e3d delete comment 2023-03-10 15:10:01 +01:00
7cc30ee302 alinear btn y disable, rules 2023-03-10 15:09:50 +01:00
fabb6a7c98 rules username perfil 2023-03-10 12:23:56 +01:00
2d7b1dee75 nginx.conf 2023-03-10 11:45:33 +01:00
2b6a40f795 nuevas listas 2023-03-09 12:53:52 +01:00
a6dc62496e push dockerizado y comprobado 2023-03-09 11:38:13 +01:00
4291ccbe63 script dockeriza 2023-03-09 10:52:12 +01:00
347d6ab147 dockerizar 2023-03-09 10:49:16 +01:00
60d295a5c5 dockerizar 2023-03-09 10:48:42 +01:00
a9a9124513 pagina de listas 2023-03-08 19:09:20 +01:00
e029307aac lista page 2023-03-08 15:47:06 +01:00
0595570cf0 cambiada index por listas 2023-03-08 15:46:31 +01:00
1396fe531a Guardar y cargar datos 2023-03-08 15:46:09 +01:00
cedf74fcfe update user 2023-03-06 18:43:58 +01:00
55d48a7d76 updateUser 2023-03-06 17:04:18 +01:00
3fd3f30ae1 email al inicio existe 2023-03-06 17:01:32 +01:00
f717544e59 sube archivps 2023-03-06 14:09:33 +01:00
8bfd3a98a0 botones cancel and save 2023-03-05 18:29:27 +01:00
7fd1f10e57 eliminado css sin usar 2023-03-02 08:01:50 +01:00
ce47cd8e84 nombres y colores de los enlaces 2023-03-02 07:59:46 +01:00
61592604de login terminado 2023-03-01 22:48:53 +01:00
f9a43e2cc6 creando login 2023-03-01 14:17:33 +01:00
45e2a28412 Avisos registro 2023-03-01 13:18:30 +01:00
dc6c9ae938 login si está verificado 2023-02-28 13:50:32 +01:00
a892499c3d username sin caracteres extraños 2023-02-28 11:48:47 +01:00
585188cc14 registra y manda email 2023-02-28 11:39:28 +01:00
d8c0fec665 eliminada configuración vscode 2023-02-28 11:39:14 +01:00
0add3fb306 falta que registre, email y tal 2023-02-27 15:58:19 +01:00
9a500cbde2 :rules username, name and email 2023-02-27 14:57:38 +01:00
56675be741 todavia comprobarUsername no acepta null 2023-02-26 19:04:04 +01:00
b55a1cdead añadidos q-inputs en register 2023-02-26 15:50:56 +01:00
31d12e7d0c logout() 2023-02-25 18:40:30 +01:00
97c17d3672 añadido refresh y rutas protegidas 2023-02-25 16:49:26 +01:00
27add5f2bc login isValid() 2023-02-25 12:32:49 +01:00
7b2d1db15e Nuevo login and register 2023-02-24 17:22:18 +01:00
34 changed files with 4724 additions and 243 deletions

10
.vscode/settings.json vendored
View File

@@ -1,10 +0,0 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"editor.fontFamily": "Hack Nerd Font Mono Regular",
"editor.fontSize": 16
}

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
# develop stage
FROM node:14.21.2-alpine as develop-stage
WORKDIR /app
COPY package*.json ./
RUN npm install -g npm@latest
RUN npm i -g @quasar/cli
COPY . .
# build stage
FROM develop-stage as build-stage
RUN npm i
RUN quasar build
# production stage
FROM nginx:1.17.5-alpine as production-stage
COPY --from=build-stage /app/dist/spa /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,41 +1,20 @@
# PocketBase (frontend-pocketbase)
Quasar App (ejercicio_quasar)
frontend of pocketbase backend
### Crear la imagen
## Install the dependencies
```bash
yarn
# or
npm install
```
docker build -t dockerize-quasar-listas .
```
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
quasar dev
### borrar y crear contenedor
```
docker rm -f listas && docker run -d --name=listas --restart unless-stopped -p 33329:80 dockerize-quasar-listas
```
### purgar docker
### Lint the files
```bash
yarn lint
# or
npm run lint
```
### Format the files
```bash
yarn format
# or
npm run format
docker system prune
docker image prune -a
```
### Build the app for production
```bash
quasar build
```
### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).

6
dockerizar.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
git pull
docker build -t dockerize-quasar-listas .
docker rm -f listas && docker run -d --name=listas --restart unless-stopped -p 33329:80 dockerize-quasar-listas
docker system prune
docker image prune -a

46
nginx.conf Normal file
View File

@@ -0,0 +1,46 @@
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

3460
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,14 @@
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0",
"postcss": "^8.4.14",
"prettier": "^2.5.1"
"prettier": "^2.5.1",
"workbox-build": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4"
},
"engines": {
"node": "^18 || ^16 || ^14.19",

159
pb_schema.json Normal file
View File

@@ -0,0 +1,159 @@
[
{
"id": "_pb_users_auth_",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"id": "users_name",
"name": "name",
"type": "text",
"system": false,
"required": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "users_avatar",
"name": "avatar",
"type": "file",
"system": false,
"required": false,
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null
}
}
],
"indexes": [
"CREATE INDEX `__pb_users_auth__created_idx` ON `users` (`created`)"
],
"listRule": "",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "wuqkbeb48hlovkc",
"name": "lista",
"type": "base",
"system": false,
"schema": [
{
"id": "pmy5olxm",
"name": "nombre",
"type": "text",
"system": false,
"required": true,
"options": {
"min": 2,
"max": 50,
"pattern": ""
}
},
{
"id": "6is0rpp0",
"name": "usuarios",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "_pb_users_auth_",
"cascadeDelete": true,
"minSelect": null,
"maxSelect": null,
"displayFields": []
}
},
{
"id": "a6udx3z4",
"name": "items",
"type": "relation",
"system": false,
"required": false,
"options": {
"collectionId": "g4g4sojia3p5qr2",
"cascadeDelete": true,
"minSelect": null,
"maxSelect": null,
"displayFields": [
"nombre"
]
}
}
],
"indexes": [
"CREATE INDEX `_wuqkbeb48hlovkc_created_idx` ON `lista` (`created`)"
],
"listRule": "usuarios.id ?= @request.auth.id",
"viewRule": null,
"createRule": "@request.auth.id != \"\"",
"updateRule": null,
"deleteRule": "usuarios.id ?= @request.auth.id",
"options": {}
},
{
"id": "g4g4sojia3p5qr2",
"name": "items",
"type": "base",
"system": false,
"schema": [
{
"id": "hjfkfx3u",
"name": "nombre",
"type": "text",
"system": false,
"required": true,
"options": {
"min": 2,
"max": 50,
"pattern": ""
}
},
{
"id": "4jogigzv",
"name": "cantidad",
"type": "number",
"system": false,
"required": true,
"options": {
"min": 1,
"max": 50
}
}
],
"indexes": [
"CREATE INDEX `_g4g4sojia3p5qr2_created_idx` ON `items` (`created`)",
"CREATE UNIQUE INDEX \"idx_unique_hjfkfx3u\" on \"items\" (\"nombre\")"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -85,7 +85,21 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {},
config: {
brand: {
primary: "#00a352",
secondary: "#2668a6",
accent: "#9C27B0",
dark: "#1d1d1d",
"dark-page": "#121212",
positive: "#21BA45",
negative: "#C10015",
info: "#31CCEC",
warning: "#F2C037",
},
},
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
@@ -98,7 +112,7 @@ module.exports = configure(function (/* ctx */) {
// directives: [],
// Quasar plugins
plugins: [],
plugins: ["Dialog"],
},
// animations: 'all', // --- includes all animations

View File

@@ -0,0 +1,30 @@
/* eslint-env serviceworker */
/*
* This file (which will be your service worker)
* is picked up by the build system ONLY if
* quasar.config.js > pwa > workboxMode is set to "injectManifest"
*/
import { clientsClaim } from 'workbox-core'
import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from 'workbox-precaching'
import { registerRoute, NavigationRoute } from 'workbox-routing'
self.skipWaiting()
clientsClaim()
// Use with precache injection
precacheAndRoute(self.__WB_MANIFEST)
cleanupOutdatedCaches()
// Non-SSR fallback to index.html
// Production SSR fallback to offline.html (except for dev)
if (process.env.MODE !== 'ssr' || process.env.PROD) {
registerRoute(
new NavigationRoute(
createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML),
{ denylist: [/sw\.js$/, /workbox-(.)*\.js$/] }
)
)
}

32
src-pwa/manifest.json Normal file
View File

@@ -0,0 +1,32 @@
{
"orientation": "portrait",
"background_color": "#ffffff",
"theme_color": "#027be3",
"icons": [
{
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

10
src-pwa/pwa-flag.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
pwa: true;
}
}

View File

@@ -0,0 +1,41 @@
import { register } from 'register-service-worker'
// The ready(), registered(), cached(), updatefound() and updated()
// events passes a ServiceWorkerRegistration instance in their arguments.
// ServiceWorkerRegistration: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
register(process.env.SERVICE_WORKER_FILE, {
// The registrationOptions object will be passed as the second argument
// to ServiceWorkerContainer.register()
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter
// registrationOptions: { scope: './' },
ready (/* registration */) {
// console.log('Service worker is active.')
},
registered (/* registration */) {
// console.log('Service worker has been registered.')
},
cached (/* registration */) {
// console.log('Content has been cached for offline use.')
},
updatefound (/* registration */) {
// console.log('New content is downloading.')
},
updated (/* registration */) {
// console.log('New content is available; please refresh.')
},
offline () {
// console.log('No internet connection found. App is running in offline mode.')
},
error (/* err */) {
// console.error('Error during service worker registration:', err)
}
})

View File

@@ -1,49 +0,0 @@
<template>
<q-item
clickable
tag="a"
target="_blank"
:href="link"
>
<q-item-section
v-if="icon"
avatar
>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'EssentialLink',
props: {
title: {
type: String,
required: true
},
caption: {
type: String,
default: ''
},
link: {
type: String,
default: '#'
},
icon: {
type: String,
default: ''
}
}
})
</script>

View File

@@ -2,115 +2,47 @@
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
Quasar App
{{ listaStore.pb.authStore.model.name }} -
{{ lista }}
</q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
<div>
<q-btn @click="listaStore.logout()">Cerrar</q-btn>
</div>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
show-if-above
bordered
>
<q-list>
<q-item-label
header
>
Essential Links
</q-item-label>
<EssentialLink
v-for="link in essentialLinks"
:key="link.title"
v-bind="link"
/>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
import { defineComponent, ref } from 'vue'
import EssentialLink from 'components/EssentialLink.vue'
<script setup>
import { ref, computed } from "vue";
import { useListaStore } from "../stores/lista.js";
import { useRouter } from "vue-router";
const linksList = [
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev'
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework'
},
{
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev'
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev'
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev'
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev'
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev'
}
]
const $router = useRouter();
const listaStore = useListaStore();
export default defineComponent({
name: 'MainLayout',
const nombreLista = ref("");
const cargarLista = async () => {
const result = await listaStore.pb
.collection("lista")
.getOne($router.currentRoute.value.params.id, { $autoCancel: false });
nombreLista.value = result.nombre;
};
components: {
EssentialLink
},
setup () {
const leftDrawerOpen = ref(false)
return {
essentialLinks: linksList,
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
}
})
const lista = computed(() => {
if ($router.currentRoute.value.path == "/") {
return "Listas";
} else if ($router.currentRoute.value.path == "/perfil") {
return "Perfil";
} else if ($router.currentRoute.value.path.includes("/lista/")) {
cargarLista();
return nombreLista.value;
} else return "";
});
</script>

View File

@@ -1,11 +1,206 @@
<template>
<q-page class="flex flex-center">
<h3>Página inicial</h3>
</q-page>
</template>
<div class="row q-mt-md q-mx-md">
<div class="col">
<q-btn
round
color="deep-orange"
icon="keyboard_return"
@click="
cargarDatos();
$router.push('/');
"
/>
</div>
<div class="col-12 flex flex-center">
<q-avatar size="96px">
<q-img :src="fuenteImagen" />
</q-avatar>
</div>
</div>
<div class="row">
<div class="col-2"></div>
<div class="col">
<q-form class="q-gutter-md" @click.once="recibeDatos()">
<q-file
outlined
v-model="imagen"
label="Avatar"
accept=".jpg, image/*"
max-total-size="5120000"
@rejected="noValidos"
class="q-py-md"
/>
<q-input
outlined
v-model="name"
label="Name"
:rules="[
(val) => (val != null && val.length >= 3) || 'Mínimo 3 caracteres',
]"
/>
<q-input
outlined
v-model="username"
label="Username"
:rules="[
(val) => (val != null && val.length >= 3) || 'Mínimo 3 caracteres',
(val) => comprobarUsername || 'Ya existe en la BD',
(val) => /^[A-Z0-9]+$/i.test(val) || 'Sólo letras o números',
]"
/>
<q-input
outlined
v-model="listaStore.pb.authStore.model.email"
label="email"
disable
/>
</q-form>
<div class="row q-mt-md q-mx-auto">
<q-btn
class="col-6"
flat
color="primary"
label="Cancelar"
@click="cargarDatos()"
/>
<q-btn
class="col-6"
flat
:disable="btnSaveDisable()"
color="secondary"
label="Guardar"
@click="alertSave = true"
/>
</div>
</div>
<div class="col-2"></div>
</div>
<q-dialog v-model="alertSave" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="warning" color="warning" text-color="white" />
<span class="q-ml-sm">Está seguro de guardar los datos?</span>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
label="No"
color="negative"
v-close-popup
@click="cargarDatos()"
/>
<q-btn
flat
label="Guardar"
color="accent"
v-close-popup
@click="updateUser()"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
import { ref } from "vue";
import { ref, computed, onMounted } from "vue";
import { useListaStore } from "../stores/lista.js";
import { useQuasar } from "quasar";
const $q = useQuasar();
const listaStore = useListaStore();
const imagen = ref(null);
const fuenteImagen = ref(null);
const username = ref(listaStore.pb.authStore.model.username);
const name = ref(listaStore.pb.authStore.model.name);
const alertSave = ref(false);
const usuarios = ref([]);
const updateUser = async () => {
const formData = new FormData();
if (username.value.trim().length > 0) {
formData.append("username", username.value.trim());
}
if (name.value.trim().length > 0) {
formData.append("name", name.value.trim());
}
if (imagen.value != null) {
formData.append("avatar", imagen.value);
}
await listaStore.pb
.collection("users")
.update(listaStore.pb.authStore.model.id, formData)
.then((r) => {
cargarDatos();
});
recibeDatos();
};
const cargarDatos = () => {
refrescarImagen();
imagen.value = null;
username.value = listaStore.pb.authStore.model.username.trim();
name.value = listaStore.pb.authStore.model.name.trim();
};
const recibeDatos = () => {
listaStore.getUsers().then(function (item) {
usuarios.value = item;
});
};
const comprobarUsername = computed(() => {
let filtro = usuarios.value?.filter(
(user) => user.username.toLowerCase() == username.value.trim().toLowerCase()
);
if (filtro.length == 0) {
return true;
} else if (
filtro.length == 1 &&
filtro[0].username == listaStore.pb.authStore.model.username
) {
return true;
}
return false;
});
const btnSaveDisable = () => {
if (
username.value == null ||
username.value.length < 3 ||
!comprobarUsername.value ||
!/^[A-Z0-9]+$/i.test(username.value) ||
name.value == null ||
name.value.length < 3
) {
return true;
}
return false;
};
const refrescarImagen = () => {
if (listaStore.pb.authStore.model.avatar.length > 0) {
fuenteImagen.value =
listaStore.pb.getFileUrl(
listaStore.pb.authStore.model,
listaStore.pb.authStore.model.avatar
) + "?thumb=100x100";
} else {
fuenteImagen.value = `https://cdn.quasar.dev/img/avatar.png`;
}
};
const noValidos = () => {
$q.dialog({
type: "negative",
message: `Sólo se permiten archivos de imágenes de 5 MB como máximo`,
});
};
onMounted(() => {
refrescarImagen();
});
</script>

131
src/pages/ListasPage.vue Normal file
View File

@@ -0,0 +1,131 @@
<template>
<div
class="flex flex-center bg-teal-7 q-ma-md cursor-pointer rounded-borders"
@click="irPerfil()"
>
<h3 style="font-weight: 615">
{{ listaStore.pb.authStore.model.username }}
</h3>
</div>
<div class="flex flex-center">
<q-btn
outline
rounded
color="primary"
label="Nueva Lista"
@click="crearLista()"
/>
</div>
<div class="flex flex-center q-mt-xl">
<div
class="full-width text-center q-px-md"
v-for="lista in listas"
:key="lista.id"
>
<p class="bg-cyan-7 q-pa-md text-weight-bold text-h6 rounded-borders">
<router-link :to="`/lista/${lista.id}`">{{ lista.nombre }}</router-link
><q-icon
class="float-right cursor-pointer q-mt-xs"
name="delete"
@click="eliminarLista(lista)"
/>
<q-badge
color="cyan-5"
text-color="black"
:label="lista.items.length"
class="float-right q-ma-sm"
/>
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useListaStore } from "../stores/lista.js";
import { useRouter } from "vue-router";
import { useQuasar } from "quasar";
const $router = useRouter();
const listaStore = useListaStore();
const $q = useQuasar();
const listas = ref(null);
const listListas = async () => {
listas.value = await listaStore.pb.collection("lista").getFullList({
sort: "-created",
});
};
const irPerfil = () => {
$router.push("perfil");
};
const crearLista = () => {
$q.dialog({
title: "Nueva lista",
message: "Nombre de la lista",
prompt: {
model: "",
isValid: (val) => val.length > 2,
type: "text", // optional
},
cancel: true,
persistent: true,
})
.onOk((nombreLista) => {
console.log(">>>> OK, received", nombreLista);
const data = {
nombre: nombreLista.trim(),
usuarios: [listaStore.pb.authStore.model.id],
items: [],
};
listaStore.pb
.collection("lista")
.create(data)
.then((r) => {
console.log("creada");
listListas();
})
.catch((e) => {
console.log(e);
});
})
.onCancel((e) => {
console.log(e);
});
};
const eliminarLista = (l) => {
console.log(l.id);
$q.dialog({
title: l.nombre,
message: "Está seguro de querer eliminar " + l.nombre + "?",
cancel: true,
persistent: true,
}).onOk(async () => {
if (l.usuarios.length == 1) {
await listaStore.pb.collection("lista").delete(l.id);
} else {
const resultado = l.usuarios.filter(
(user) => user != listaStore.pb.authStore.model.id
);
l.usuarios = resultado;
await listaStore.pb.collection("lista").update(l.id, l);
}
listListas();
});
};
onMounted(() => {
listListas();
});
</script>
<style scoped>
a:link,
a:visited,
a:active {
text-decoration: none;
color: black;
}
</style>

View File

@@ -1,7 +1,137 @@
<template>
<div class="flex flex-center">
<h1>Huevos login</h1>
<div
class="bg-light-green window-height window-width row justify-center items-center"
>
<div class="column">
<div class="row">
<h5 class="text-h5 text-white q-my-md">Mis listas</h5>
</div>
<div class="row">
<q-card square bordered class="q-pa-lg shadow-1">
<q-card-section>
<q-form class="q-gutter-md" @click.once="recibeDatos()">
<q-input
square
filled
clearable
v-model="email"
type="email"
label="email"
:rules="[
(val) =>
(val != null && val.length >= 7) || 'Mínimo 7 caracteres',
() => comprobarEmailVerified || msgError,
]"
/>
<q-input
square
filled
clearable
v-model="password"
type="password"
label="password"
:rules="[
(val) =>
(val != null && val.trim().length >= 8) ||
'Mínimo 8 caracteres',
]"
/>
</q-form>
</q-card-section>
<q-card-actions class="q-px-md">
<q-btn
unelevated
color="light-green-7"
size="lg"
class="full-width"
label="Login"
@click="login()"
:disable="btnLoginDisable()"
/>
</q-card-actions>
<q-card-section class="text-center q-pa-none">
<p class="text-grey-2">
<router-link to="/register"
>No está registrado? Crear una cuenta</router-link
>
</p>
</q-card-section>
</q-card>
</div>
</div>
</div>
</template>
<script></script>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useListaStore } from "../stores/lista.js";
import { useRouter } from "vue-router";
import { useQuasar } from "quasar";
const $q = useQuasar();
const listaStore = useListaStore();
const $router = useRouter();
const email = ref("");
const password = ref("");
const usuarios = ref([]);
const msgError = ref("");
const comprobarEmailVerified = computed(() => {
const existe = usuarios.value?.filter((item) => item.email == email.value);
const verificado = existe.filter((item) => item.verified == true);
if (existe.length == 1 && verificado.length == 0) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
msgError.value = "Falta verificar el correo";
return false;
} else if (existe.length == 0 && usuarios.value?.length > 0) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
msgError.value = "El email no existe";
return false;
}
return true;
});
const recibeDatos = () => {
listaStore.getUsers().then((item) => {
usuarios.value = item;
});
};
const login = () => {
listaStore
.login(email.value, password.value)
.then((r) => {
$router.push("/");
})
.catch((e) => {
$q.dialog({
title: "Aviso",
message: "El correo o la contraseña son erroneos",
});
});
};
const btnLoginDisable = () => {
if (
!comprobarEmailVerified.value ||
password.value == null ||
password.value.trim().length < 8
) {
return true;
}
return false;
};
onMounted(() => {
recibeDatos();
});
</script>
<style scoped>
a:link,
a:visited,
a:active {
text-decoration: none;
color: gray;
}
</style>

View File

@@ -1,7 +1,200 @@
<template>
<div class="flex flex-center">
<h1>Registro huevos</h1>
<div
class="bg-light-green window-height window-width row justify-center items-center"
>
<div class="column">
<div class="row">
<h5 class="text-h5 text-white q-my-md">Mis listas</h5>
</div>
<div class="row">
<q-card square bordered class="q-pa-lg shadow-1">
<q-card-section>
<q-form class="q-gutter-md" @click.once="recibeDatos()">
<q-input
square
filled
clearable
v-model="username"
type="text"
label="username"
:rules="[
(val) =>
(val != null && val.length >= 3) || 'Mínimo 3 caracteres',
(val) => comprobarUsername || 'Ya existe en la BD',
(val) => /^[A-Z0-9]+$/i.test(val) || 'Sólo letras o números',
]"
/>
<q-input
square
filled
clearable
v-model="nombre"
type="text"
label="nombre completo"
:rules="[
(val) =>
(val != null && val.length >= 3) || 'Mínimo 3 caracteres',
]"
/>
<q-input
square
filled
clearable
v-model="email"
type="email"
label="email"
:rules="[
(val) =>
(val != null && val.length >= 7) || 'Mínimo 7 caracteres',
(val) =>
comprobarEmail ||
'No es un email válido o ya existe en la BD',
]"
/>
<q-input
square
filled
clearable
v-model="password"
type="password"
label="password"
:rules="[
(val) =>
(val != null && val.trim().length >= 8) ||
'Mínimo 8 caracteres',
]"
/>
<q-input
square
filled
clearable
v-model="confirmpassword"
type="password"
label="repite password"
error-message="No coinciden"
:error="comprobarConfirmPassword"
/>
</q-form>
</q-card-section>
<q-card-actions class="q-px-md">
<q-btn
:disable="btnRegisterDisable()"
unelevated
color="light-green-7"
size="lg"
class="full-width"
label="Register"
@click="registrar()"
/>
</q-card-actions>
<q-card-section class="text-center q-pa-none">
<p class="text-grey-6">
<router-link to="/login"
>Está registrado? Página de login</router-link
>
</p>
</q-card-section>
</q-card>
</div>
</div>
</div>
</template>
<script></script>
<script setup>
import { ref, computed } from "vue";
import { useListaStore } from "../stores/lista.js";
import { useQuasar } from "quasar";
import { useRouter } from "vue-router";
const listaStore = useListaStore();
const $q = useQuasar();
const $router = useRouter();
const email = ref("");
const password = ref("");
const username = ref("");
const confirmpassword = ref("");
const nombre = ref("");
const usuarios = ref([]);
const recibeDatos = () => {
listaStore.getUsers().then(function (item) {
usuarios.value = item;
});
};
const comprobarUsername = computed(() => {
let filtro = usuarios.value?.filter(
(user) => user.username.toLowerCase() == username.value.trim().toLowerCase()
);
if (filtro.length == 0) {
return true;
}
return false;
});
const comprobarEmail = computed(() => {
const emailPattern =
/^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
let filtro = usuarios.value?.filter(
(user) => user.email == email.value.trim()
);
if (emailPattern.test(email.value) && filtro.length == 0) {
return true;
}
return false;
});
const comprobarConfirmPassword = computed(
() => password.value != confirmpassword.value
);
const btnRegisterDisable = () => {
if (
username.value == null ||
username.value.length < 3 ||
!comprobarUsername.value ||
!/^[A-Z0-9]+$/i.test(username.value) ||
nombre.value == null ||
nombre.value.length < 3 ||
email.value == null ||
email.value.length < 7 ||
!comprobarEmail.value ||
password.value == null ||
password.value.trim().length < 8 ||
confirmpassword.value == null ||
comprobarConfirmPassword.value
) {
return true;
}
return false;
};
const registrar = () => {
const data = {
username: username.value,
email: email.value,
emailVisibility: true,
password: password.value,
passwordConfirm: confirmpassword.value,
name: nombre.value,
};
listaStore.register(data).then((r) => {
$q.dialog({
title: "Aviso",
message: "Se le ha enviado un correo electrónico que debe verificar",
}).onOk(() => {
$router.push("/");
});
});
};
</script>
<style scoped>
a:link,
a:visited,
a:active {
text-decoration: none;
color: gray;
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<div class="row">
<div class="col q-ma-md">
<q-btn
round
color="deep-orange"
icon="keyboard_return"
@click="$router.push('/')"
/>
</div>
<div class="col-10">
<div class="q-ma-md rounded-borders">
<q-input
outlined
v-model="producto"
label="Item"
@keyup.enter="cargarProducto()"
:rules="[
(val) =>
(val != null && val.length >= 3) ||
(val != null && val.length == 0) ||
'Mínimo 3 caracteres',
]"
/>
</div>
</div>
</div>
<div class="flex flex-center q-mt-xl">
<div
v-for="item in items"
:key="item.id"
class="full-width text-center q-px-md"
>
<p
class="bg-cyan-7 q-pa-md q-mx-md text-weight-bold text-h6 rounded-borders"
>
{{ item.nombre
}}<q-icon
class="float-right cursor-pointer q-mt-xs"
name="delete"
@click="eliminarItem(item)"
/>
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useListaStore } from "../../stores/lista.js";
import { useRouter } from "vue-router";
const listaStore = useListaStore();
const $router = useRouter();
const lista = ref(null);
const items = ref(null);
const producto = ref("");
const cargarProducto = async () => {
if (producto.value.length >= 3) {
// Busca en todos, si no está crea y añade
const res = await listaStore.pb.collection("items").getFullList();
const busqueda = res.filter(
(element) => element.nombre == producto.value.trim()
);
if (busqueda.length == 0) {
const data = {
nombre: producto.value.trim(),
cantidad: 1,
};
const item = await listaStore.pb.collection("items").create(data);
lista.value.items.push(item.id);
await listaStore.pb
.collection("lista")
.update(lista.value.id, lista.value);
} else {
// Si está, comprueba que no esté en la lista y lo añade
if (lista.value.items.indexOf(busqueda[0].id) == -1) {
lista.value.items.push(busqueda[0].id);
await listaStore.pb
.collection("lista")
.update(lista.value.id, lista.value);
}
}
const arrayIdItem = lista.value.items;
const result = await listaStore.pb.collection("items").getFullList();
items.value = result.filter(
(element) => arrayIdItem.indexOf(element.id) != -1
);
producto.value = "";
}
};
const eliminarItem = async (i) => {
//Lo saca de la lista
const orden = lista.value.items.indexOf(i.id);
lista.value.items.splice(orden, 1);
await listaStore.pb.collection("lista").update(lista.value.id, lista.value);
const arrayIdItem = lista.value.items;
const result = await listaStore.pb.collection("items").getFullList();
items.value = result.filter(
(element) => arrayIdItem.indexOf(element.id) != -1
);
};
onMounted(async () => {
lista.value = await listaStore.pb
.collection("lista")
.getOne($router.currentRoute.value.params.id);
const arrayIdItem = lista.value.items;
const result = await listaStore.pb.collection("items").getFullList();
items.value = result.filter(
(element) => arrayIdItem.indexOf(element.id) != -1
);
});
</script>
<style scoped>
a:link,
a:visited,
a:active {
text-decoration: none;
color: gray;
}
</style>

View File

@@ -1,6 +1,12 @@
import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
import { route } from "quasar/wrappers";
import {
createRouter,
createMemoryHistory,
createWebHistory,
createWebHashHistory,
} from "vue-router";
import routes from "./routes";
import { useListaStore } from "../stores/lista.js";
/*
* If not building with SSR mode, you can
@@ -14,7 +20,9 @@ import routes from './routes'
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
: process.env.VUE_ROUTER_MODE === "history"
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
@@ -23,8 +31,21 @@ export default route(function (/* { store, ssrContext } */) {
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE)
})
history: createHistory(process.env.VUE_ROUTER_BASE),
});
return Router
})
Router.beforeEach(async (to, from, next) => {
const listaStore = useListaStore();
if (to.meta.auth) {
//Si es una ruta protegida
if (listaStore.pb.authStore.isValid) {
return next(); //Si es protegida y el token es válido
}
return next("/login"); //Si es protegida y el token no es válido
}
next();
});
return Router;
});

View File

@@ -5,9 +5,19 @@ const routes = [
children: [
{
path: "",
component: () => import("pages/ListasPage.vue"),
meta: { auth: true },
},
{
path: "perfil",
component: () => import("pages/IndexPage.vue"),
meta: { auth: true },
},
{
path: 'lista/:id',
component: () => import("pages/listas/indexPage.vue"),
meta: { auth: true },
},
],
},
{

View File

@@ -5,43 +5,44 @@ import PocketBase from "pocketbase";
export const useListaStore = defineStore("lista", () => {
const pb = new PocketBase("https://pocketbase.clonbg.es");
const authData = ref("");
function login() {
authData.value = pb
async function login(email, password) {
return await pb.collection("users").authWithPassword(email, password);
}
async function refresh() {
return await pb.collection("users").authRefresh();
}
async function logout() {
pb.authStore.clear();
this.router.push("/login");
}
async function getUsers() {
return await pb.collection("users").getFullList({
sort: "-created",
});
}
async function register(data) {
return await pb
.collection("users")
.authWithPassword("clonbg", "m4nu3lm4nu3l")
.create(data)
.then((r) => {
console.log("logueado");
pb.collection("users").requestVerification(data.email);
})
.catch((e) => {
console.log("error");
console.log(e);
});
}
return { pb, authData, login };
return {
pb,
authData,
login,
refresh,
logout,
getUsers,
register,
};
});
/*
--- Pinia
export const useListaStore = defineStore("lista", () => {
const counter = ref(3);
function increment() {
counter.value++;
}
function doubleCount() {
return counter.value * 2;
}
return { counter, increment, doubleCount };
});
--- vue
listaStore.counter = 9;
console.log(listaStore.counter);
console.log(listaStore.doubleCount());
console.log(listaStore.counter);
listaStore.increment();
console.log(listaStore.counter);
*/