Pinia + Vite + Firebase 9
En esta sección conoceremos como trabajar con Vite, Pinia y Firebase 9, en nuevo estándar 2022 para Vue.js 🙌
⭐ Videos Premium ⭐
Esta sección es parte del curso en Udemy. Si quieres acceder a ella, puedes comprar el curso en Udemy: Vue.js + Firebase by bluuweb cupón de descuento aplicado 😊.
Códigos
Vite
- Vite web oficial: Vite se define como una herramienta de frontend que te ayudará a crear tus proyectos de forma agnóstica (sin atarte a ningún framework concreto) y que su desarrollo y construcción final sea lo más sencilla posible. Está desarrollada por Evan You, el creador de Vue. Actualmente, Vite soporta tanto proyectos vanilla (sin utilizar frameworks), como proyectos utilizando Vue, React, Preact o Lit-element (tanto en versión Javascript, como Typescript). Fuente
- Templates
- Comunidad DEV
# npm 6.x
npm create vite@latest my-vue-app --template vue
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
# pnpm
pnpm create vite my-vue-app -- --template vue
Install Vue Router
npm install vue-router@4
router.js
import { createRouter, createWebHistory } from "vue-router";
import Home from "./components/Home.vue";
import About from "./components/About.vue";
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
const history = createWebHistory();
const router = createRouter({
history,
routes,
});
export default router;
main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
createApp(App).use(router).mount("#app");
App.vue
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/login">Login</router-link> |
<router-link to="/register">Register</router-link> |
</nav>
<router-view />
</template>
Pinia
- Pinia web oficial
- Pinia es una biblioteca de tiendas para Vue, le permite compartir un estado entre componentes/páginas.
- Aunque Pinia es lo suficientemente bueno para reemplazar a Vuex, reemplazar a Vuex no era su objetivo. Pero luego se volvió tan bueno que el equipo central de Vue.js decidió convertirlo en Vuex 5.
- vuex vs pinia
npm install pinia
main.js
import { createPinia } from "pinia";
app.use(createPinia());
STATE
stores/user.js
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
userData: "bluuweb",
}),
});
Home.vue
<template>
<h1>Home {{ userStore.userData }}</h1>
</template>
<script setup>
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
</script>
Login.vue
<template>
<h1>Login</h1>
<h2>{{ pasarMayuscula }}</h2>
</template>
<script setup>
import { computed } from "vue";
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
const pasarMayuscula = computed(() => userStore.userData.toUpperCase());
</script>
GETTER
- Los captadores son solo propiedades computadas detrás de escena, por lo que no es posible pasarles ningún parámetro. Sin embargo, puede devolver una función del getter para aceptar cualquier argumento: más info aquí
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
userData: "bluuweb",
}),
getters: {
userMayuscula(state) {
return state.userData.toUpperCase();
},
},
});
Login.vue
<template>
<h1>Login</h1>
<h2>{{ pasarMayuscula }}</h2>
<h2>{{ userStore.userMayuscula }}</h2>
</template>
<script setup>
import { computed } from "vue";
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
const pasarMayuscula = computed(() => userStore.userData.toUpperCase());
</script>
ACTIONS
- Las acciones son el equivalente de los métodos en los componentes. Se pueden definir con la actionspropiedad en
defineStore()
y son perfectos para definir la lógica empresarial:
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
userData: "bluuweb",
}),
getters: {
userMayuscula(state) {
return state.userData.toUpperCase();
},
},
actions: {
registerUser(name) {
this.userData = name;
},
},
});
Register.vue
<template>
<h1>Register</h1>
<button @click="userStore.registerUser('Ignacio')">Acceder</button>
</template>
<script setup>
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
</script>
Firebase 9
npm install firebase
firebaseConfig.js
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth, onAuthStateChanged } from "firebase/auth";
const firebaseConfig = {
apiKey: "xxx",
authDomain: "xxx",
projectId: "xxx",
storageBucket: "xxx",
messagingSenderId: "xxx",
appId: "xxx",
};
initializeApp(firebaseConfig);
const db = getFirestore();
const auth = getAuth();
export { db, auth };
Register
import { defineStore } from "pinia";
import { auth } from "../firebaseConfig";
import { createUserWithEmailAndPassword } from "firebase/auth";
import router from "../router";
export const useUserStore = defineStore("user", {
state: () => ({
userData: {},
loadingUser: false,
loading: false,
}),
actions: {
async registerUser(email, password) {
this.loadingUser = true;
try {
const { user } = await createUserWithEmailAndPassword(
auth,
email,
password
);
this.userData = { email: user.email, uid: user.uid };
router.push("/");
} catch (error) {
console.log(error);
this.userData = {};
} finally {
this.loadingUser = false;
}
},
},
});
Register.vue
<template>
<div>
<h1>Register</h1>
<form @submit.prevent="handleSubmit">
<input
type="email"
placeholder="email"
v-model.trim="email"
/>
<input
type="password"
placeholder="password"
v-model.trim="password"
/>
<button
type="submit"
:disabled="userStore.loadingUser"
>
Crear cuenta
</button>
</form>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
const email = ref("bluuweb1@test.com");
const password = ref("123123");
const handleSubmit = () => {
if (!email.value || password.value.length < 6) {
alert("ingresa los campos");
}
userStore.registerUser(email.value, password.value);
};
</script>
Login
async login(email, password) {
this.loadingUser = true;
try {
const { user } = await signInWithEmailAndPassword(
email,
password
);
this.userData = { email: user.email, uid: user.uid };
router.push("/");
} catch (error) {
console.log(error);
this.userData = {};
} finally {
this.loadingUser = false;
}
},
Login.vue
<template>
<div>
<h1>Login</h1>
<form @submit.prevent="handleSubmit">
<input
type="email"
placeholder="email"
v-model.trim="email"
/>
<input
type="password"
placeholder="password"
v-model.trim="password"
/>
<button
type="submit"
:disabled="userStore.loadingUser"
>
Acceder
</button>
</form>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
const email = ref("bluuweb1@test.com");
const password = ref("123123");
const handleSubmit = () => {
if (!email.value || password.value.length < 6) {
alert("ingresa los campos");
}
userStore.loginUser(email.value, password.value);
};
</script>
SignOut
async signOutUser() {
this.loading = true;
try {
await signOut(auth);
} catch (error) {
console.log(error);
} finally {
this.userData = {};
this.loading = false;
router.push("/login");
}
},
App.vue
<button @click="useStore.signOutUser">Logout</button>
Ruta
- onAuthStateChanged
- stackoverflow promise auth
- unsubscribe to onauthstatechanged
- api Auth.onAuthStateChanged() v9
store/user.js (actions)
currentUser() {
return new Promise((resolve, reject) => {
const unsubcribe = onAuthStateChanged(
auth,
(user) => {
if (user) {
this.userData = {
email: user.email,
uid: user.uid,
};
}
resolve(user);
},
(e) => reject(e)
);
// Según la documentación, la función onAuthStateChanged() devuelve
// La función de cancelación de suscripción para el observador
unsubcribe();
});
},
router.js
import { createRouter, createWebHistory } from "vue-router";
import { useUserStore } from "./stores/user";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
import Register from "./views/Register.vue";
const requireAuth = async (to, from, next) => {
const userStore = useUserStore();
userStore.loading = true;
const user = await userStore.currentUser();
if (user) {
next();
} else {
next("/login");
}
userStore.loading = false;
};
const routes = [
{ path: "/", component: Home, beforeEnter: requireAuth },
{ path: "/login", component: Login },
{ path: "/register", component: Register },
];
const router = createRouter({
routes,
history: createWebHistory(),
});
export default router;
app.vue
<template>
<div v-if="userStore.loading">loading...</div>
<div v-else>
<h1>App</h1>
<nav>
<router-link
to="/"
v-if="userStore.userData"
>Home</router-link
>
|
<router-link
to="/login"
v-if="!userStore.userData"
>Login</router-link
>
|
<router-link
to="/register"
v-if="!userStore.userData"
>Register</router-link
>
|
<button
@click="userStore.signOutUser"
v-if="userStore.userData"
>
Logout
</button>
</nav>
<router-view></router-view>
</div>
</template>
<script setup>
import { useUserStore } from "./stores/user";
const userStore = useUserStore();
</script>
Verificar cuenta correo
stores/user.js
async registerUser(email, password) {
this.loadingUser = true;
try {
await createUserWithEmailAndPassword(auth, email, password);
await sendEmailVerification(auth.currentUser);
router.push("/login");
} catch (error) {
console.log(error);
} finally {
this.loadingUser = false;
}
},
Register.vue
const handleSubmit = async () => {
if (!email.value || password.value.length < 6) {
alert("ingresa los campos");
}
try {
await userStore.registerUser(email.value, password.value);
alert("Verifica email");
} catch (error) {
console.log(error);
}
};
router.js
const requireAuth = async (to, from, next) => {
const userStore = useUserStore();
userStore.loading = true;
const user = await userStore.currentUser();
console.log(user);
if (user && user.emailVerified) {
next();
} else {
next("/login");
}
userStore.loading = false;
};
Firestore
firebaseConfig.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore/lite";
const firebaseConfig = {
apiKey: "AIzaSyBHSBW7EIKq8XvlyfLt3_AQJfoGo4P-w10",
authDomain: "vite-udemy.firebaseapp.com",
projectId: "vite-udemy",
storageBucket: "vite-udemy.appspot.com",
messagingSenderId: "472497203702",
appId: "1:472497203702:web:022b5d5fc22b4e522c3fd7",
};
initializeApp(firebaseConfig);
const auth = getAuth();
const db = getFirestore();
export { auth, db };
Agregar datos manualmente
urls: [
id1: {
name: 'https://bluuweb.org',
short: 'aDgdGd',
user: 'pQycjKGmIKQ2wL4P1jvkAPhH4gh2'
},
id2: {
name: 'https://firebase.com',
short: 'aDgdGd',
user: 'pQycjKGmIKQ2wL4P1jvkAPhH4gh2'
}
]
Leer doc
database.js
import { collection, getDocs, query, where } from "firebase/firestore/lite";
import { defineStore } from "pinia";
import { auth, db } from "../firebaseConfig";
export const useDatabaseStore = defineStore("database", {
state: () => ({
documents: [],
loading: false,
loadingDoc: false,
}),
actions: {
async getUrls() {
if (this.documents.length !== 0) {
return;
}
this.loading = true;
this.documents = [];
const q = query(
collection(db, "urls"),
where("user", "==", auth.currentUser.uid)
);
try {
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
this.documents.push({
id: doc.id,
...doc.data(),
});
});
} catch (error) {
console.log(error);
} finally {
this.loading = false;
}
},
},
});
Reset store
useUserStore
import { useDatabaseStore } from "./database";
async signOutUser() {
this.loadingUser = true;
const databaseStore = useDatabaseStore();
try {
await signOut(auth);
router.push("/login");
} catch (error) {
console.log(error);
} finally {
this.loadingUser = false;
this.userData = null;
databaseStore.$reset();
}
},
currentUser() {
return new Promise((resolve, reject) => {
const unsubcribe = onAuthStateChanged(
auth,
(user) => {
const databaseStore = useDatabaseStore();
if (user) {
this.userData = {
email: user.email,
uid: user.uid,
};
} else {
this.userData = null;
databaseStore.$reset();
}
resolve(user);
},
(e) => reject(e)
);
unsubcribe();
});
},
Agregar doc
async addUrl(name) {
this.loadingDoc = true;
try {
const docObjeto = {
name: name,
short: nanoid(5),
user: auth.currentUser.uid
};
const q = query(collection(db, 'urls'))
const docRef = await addDoc(q, docObjeto);
this.documents.push({ id: docRef.id, ...docObjeto });
} catch (error) {
console.log(error);
} finally {
this.loadingDoc = false;
}
},
Borrar doc
async deleteUrl(id) {
this.loadingDoc = true;
try {
const docRef = doc(db, "urls", id);
const docSnap = await getDoc(docRef);
if(!docSnap.exists()){
throw new Error('no existe el doc')
}
if (docSnap.data().user === auth.currentUser.uid) {
await deleteDoc(docRef);
this.documents = this.documents.filter(
(item) => item.id !== id
);
} else {
throw new Error('no eres el autor')
}
} catch (error) {
console.log(error.message);
} finally {
this.loadingDoc = false;
}
},
<template>
<div>
<h1>Home</h1>
<p>Bienvenido: {{ userStore.userData.uid }}</p>
<form @submit.prevent="handleSubmit">
<input
type="text"
placeholder="url"
v-model.trimp="url"
/>
<button
type="submit"
:disabled="databaseStore.loadingDoc"
>
Agregar
</button>
</form>
<ul v-if="!databaseStore.loading">
<li
v-for="item of databaseStore.documents"
:key="item.id"
>
{{ item.id }} <br />
{{ item.name }} <br />
{{ item.short }}
<div>
<button
@click="databaseStore.deleteUrl(item.id)"
:disabled="databaseStore.loadingDoc"
>
Eliminar
</button>
<button @click="router.push(`/editar/${item.id}`)">Editar</button>
</div>
</li>
</ul>
<div v-else>loading...</div>
</div>
</template>
<script setup>
import { onBeforeMount, ref } from "vue";
import { useRouter } from "vue-router";
import { useDatabaseStore } from "../stores/database";
import { useUserStore } from "../stores/user";
const databaseStore = useDatabaseStore();
const userStore = useUserStore();
const router = useRouter();
const url = ref("");
const handleSubmit = async () => {
await databaseStore.addUrl(url.value);
console.log("agregado");
};
onBeforeMount(async () => {
await databaseStore.getUrls();
});
</script>
Leer único doc
async leerUrl(id) {
this.loadingDoc = true;
try {
const docRef = doc(db, "urls", id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) {
throw new Error("no existe el doc");
}
if (docSnap.data().user === auth.currentUser.uid) {
return docSnap.data().name;
} else {
throw new Error("no eres el autor");
}
} catch (error) {
console.log(error.message);
} finally {
this.loadingDoc = false;
}
},
router.js
const routes = [
{ path: "/", component: Home, beforeEnter: requireAuth },
{ path: "/editar/:id", component: Editar, beforeEnter: requireAuth },
{ path: "/login", component: Login },
{ path: "/register", component: Register },
];
<template>
<div>
<h1>Editar</h1>
<p v-if="databaseStore.loadingDoc">Loading doc...</p>
<form
@submit.prevent="handleSubmit"
v-else
>
<input
type="text"
placeholder="url"
v-model.trimp="url"
/>
<button
type="submit"
:disabled="databaseStore.loadingDoc"
>
Editar
</button>
</form>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { useDatabaseStore } from "../stores/database";
const route = useRoute();
const databaseStore = useDatabaseStore();
const url = ref("");
onMounted(async () => {
url.value = await databaseStore.leerUrl(route.params.id);
});
const handleSubmit = async () => {
await databaseStore.updateUrl(route.params.id, url.value);
};
</script>
Update doc
async updateUrl(id, name) {
this.loadingDoc = true;
try {
const docRef = doc(db, "urls", id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) {
throw new Error("no existe el doc");
}
if (docSnap.data().user === auth.currentUser.uid) {
await updateDoc(docRef, {
name: name,
});
this.documents = this.documents.map((item) =>
item.id === id ? { ...item, name: name } : item
);
} else {
throw new Error("no eres el autor");
}
} catch (error) {
console.log(error.message);
} finally {
this.loadingDoc = false;
}
},
Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /urls/{id} {
allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.user;
allow create: if request.auth != null;
}
}
}
Deploy
npm install -g firebase-tools
firebase login
firebase init
firebase deploy
¿ejecución de scripts está deshabilitada en este sistema?
Ejecutar windows + R –> gpedit.msc. Ir a Plantillas administrativas> Componentes de Windows> Windows PowerShell> Seleccionar Activar la ejecución de scripts, click derecho, editar. Seleccionar Habilitada y Permitir todos los scripts, Aplicar.
Ant Design Vue
npm install ant-design-vue@next --save
npm i unplugin-vue-components
vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [AntDesignVueResolver()],
}),
],
});
<a-button type="primary" size="large">Boton</a-button>
Layout
Login.vue
<template>
<a-layout>
<a-layout-header v-if="!userStore.loadingSession">
<a-menu
mode="horizontal"
theme="dark"
:style="{ lineHeight: '64px' }"
v-model:selectedKeys="selectedKeys"
>
<a-menu-item
v-if="userStore.userData"
key="home"
>
<router-link to="/">Home</router-link>
</a-menu-item>
<a-menu-item
v-if="!userStore.userData"
key="login"
>
<router-link to="/login">Login</router-link>
</a-menu-item>
<a-menu-item
v-if="!userStore.userData"
key="register"
>
<router-link to="/register">Register</router-link>
</a-menu-item>
<a-menu-item
@click="userStore.logoutUser"
v-if="userStore.userData"
key="logout"
>
Logout
</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout-content style="padding: 0 50px">
<div
:style="{
background: '#fff',
padding: '24px',
minHeight: '280px',
}"
>
<div v-if="userStore.loadingSession">loading user...</div>
<router-view></router-view>
</div>
</a-layout-content>
</a-layout>
</template>
<script setup>
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useUserStore } from "./stores/user";
const userStore = useUserStore();
const route = useRoute();
const selectedKeys = ref([]);
watch(
() => route.name,
() => {
// console.log(route.name)
selectedKeys.value = [route.name];
}
);
</script>
Grid
<template>
<a-row>
<a-col
:xs="{ span: 24 }"
:sm="{ span: 18, offset: 3 }"
:lg="{ span: 12, offset: 6 }"
>
<a-form
:model="formState"
@finish="onFinish"
@finishFailed="onFinishFailed"
name="basic"
layout="vertical"
autocomplete="off"
>
<a-form-item
label="Email"
name="email"
:rules="[
{
required: true,
type: 'email',
message: 'Por favor escriba un email válido',
},
]"
>
<a-input v-model:value="formState.email"></a-input>
</a-form-item>
<a-form-item
label="Password"
name="password"
:rules="[
{
required: true,
min: 6,
message: 'Por favor escriba una contraseña de 6 carácteres',
},
]"
>
<a-input-password
v-model:value="formState.password"
></a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
>Acceder</a-button
>
</a-form-item>
</a-form>
</a-col>
</a-row>
</template>
<script setup>
import { reactive } from "vue";
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
const formState = reactive({
password: "",
email: "bluuweb1@test.com",
});
const onFinish = async (values) => {
console.log("Success:", values);
await userStore.loginUser(formState.email, formState.password);
};
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
};
</script>
Register.vue
<template>
<a-row>
<a-col
:xs="{ span: 24 }"
:sm="{ span: 18, offset: 3 }"
:lg="{ span: 12, offset: 6 }"
>
<a-form
:model="formState"
@finishFailed="onFinishFailed"
@finish="onFinish"
name="basicTwo"
layout="vertical"
autocomplete="off"
>
<a-form-item
label="Email"
name="email"
:rules="[
{
required: true,
type: 'email',
message: 'Por favor escriba un email válido',
},
]"
>
<a-input v-model:value="formState.email"></a-input>
</a-form-item>
<a-form-item
label="Password"
name="password"
:rules="[
{
required: true,
min: 6,
message: 'Por favor escriba una contraseña de 6 carácteres',
},
]"
>
<a-input-password
v-model:value="formState.password"
></a-input-password>
</a-form-item>
<a-form-item
label="Repita Password"
name="repassword"
:rules="{ validator: validateRePass }"
>
<a-input-password
v-model:value="formState.repassword"
></a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
>Acceder</a-button
>
</a-form-item>
</a-form>
</a-col>
</a-row>
</template>
<script setup>
import { reactive } from "vue";
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
const formState = reactive({
password: "",
repassword: "",
email: "bluuweb1@test.com",
});
const validateRePass = async (_rule, value) => {
if (value === "") {
return Promise.reject("Por favor repita contraseña");
}
if (value !== formState.password) {
return Promise.reject("No coinciden las contraseñas");
}
Promise.resolve();
};
const onFinish = async (values) => {
console.log("Success:", values);
await userStore.registerUser(values.email, values.password);
};
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
};
</script>
Errores Fire Auth
async loginUser(email, password) {
this.loadingUser = true;
try {
const { user } = await signInWithEmailAndPassword(
auth,
email,
password
);
this.userData = { email: user.email, uid: user.uid };
router.push("/");
} catch (error) {
// console.log(error.code);
return error.code;
} finally {
this.loadingUser = false;
}
},
Login.vue
<script setup>
import { reactive } from "vue";
import { useUserStore } from "../stores/user";
import { message } from "ant-design-vue";
import "ant-design-vue/es/message/style/css";
const userStore = useUserStore();
const formState = reactive({
password: "",
email: "bluuweb1@test.com",
});
const onFinish = async (values) => {
console.log("Success:", values);
const res = await userStore.loginUser(formState.email, formState.password);
if (res === "auth/wrong-password") {
message.error("credenciales no válidas");
}
};
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
};
</script>
async registerUser(email, password) {
this.loadingUser = true;
try {
await createUserWithEmailAndPassword(auth, email, password);
await sendEmailVerification(auth.currentUser);
router.push("/login");
} catch (error) {
// console.log(error);
return error.code;
} finally {
this.loadingUser = false;
}
},
Register.vue
const onFinish = async (values) => {
console.log("Success:", values);
const res = await userStore.registerUser(values.email, values.password);
if (!res) {
return message.success("Revisa tu correo electrónico para continuar");
}
switch (res) {
case "auth/email-already-in-use":
message.error("Correo ya registrado");
break;
}
};
AddForm.vue
components/AddForm.vue
<template>
<a-form
:model="formState"
@finish="onFinish"
name="basicAdd"
layout="vertical"
autocomplete="off"
>
<a-form-item
label="Ingrese URL"
name="url"
:rules="[
{
required: true,
whitespace: true,
pattern: regExpUrl,
message: 'Ingresa una URL válida',
},
]"
>
<a-input v-model:value="formState.url"></a-input>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="databaseStore.loadingURL"
>
Agregar
</a-button>
</a-form-item>
</a-form>
</template>
<script setup>
import { reactive } from "vue";
import { regExpUrl } from "../utils/regExpUrl";
import { useDatabaseStore } from "../stores/database";
import { message } from "ant-design-vue";
const databaseStore = useDatabaseStore();
const formState = reactive({
url: "",
});
const onFinish = async (values) => {
// console.log("success");
const res = await databaseStore.addUrl(formState.url);
formState.url = "";
if (!res) {
message.success("URL agregada con éxito");
}
};
</script>
database.js
async addUrl(name) {
this.loadingURL = true;
try {
const objetoDoc = {
name: name,
short: nanoid(6),
user: auth.currentUser.uid,
};
const docRef = await addDoc(collection(db, "urls"), objetoDoc);
this.documents.push({
...objetoDoc,
id: docRef.id,
});
} catch (error) {
console.log(error.code);
} finally {
this.loadingURL = false;
}
},
utils/regExpUrl.js
const regExpUrl =
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
export { regExpUrl };
Card
<template>
<div>
<h1>Home</h1>
<add-form></add-form>
<a-spin v-if="databaseStore.loadingDoc" />
<a-space
direction="vertical"
style="width: 100%"
>
<a-card
v-for="item of databaseStore.documents"
:key="item.id"
:title="item.short"
>
<template #extra>
<a-space>
<a-button @click="router.push(`/editar/${item.id}`)"
>Editar</a-button
>
<a-popconfirm
title="¿Estás seguro?"
ok-text="Yes"
cancel-text="No"
@confirm="confirm(item.id)"
@cancel="cancel"
>
<a-button danger>Eliminar</a-button>
</a-popconfirm>
</a-space>
</template>
<p>{{ item.name }}</p>
</a-card>
</a-space>
</div>
</template>
<script setup>
import { useDatabaseStore } from "../stores/database";
import { useRouter } from "vue-router";
import { message } from "ant-design-vue";
const databaseStore = useDatabaseStore();
const router = useRouter();
const confirm = (id) => {
console.log(id);
databaseStore.deleteUrl(id);
message.success("Eliminado");
};
const cancel = (e) => {
console.log(e);
message.error("No se eliminó");
};
databaseStore.getUrls();
</script>
View Editar
<template>
<div>
<h1>Editar id: route.params</h1>
<a-form
:model="formState"
@finish="onFinish"
name="basicAdd"
layout="vertical"
autocomplete="off"
>
<a-form-item
label="Ingrese URL"
name="url"
:rules="[
{
required: true,
whitespace: true,
pattern: regExpUrl,
message: 'Ingresa una URL válida',
},
]"
>
<a-input v-model:value="formState.url"></a-input>
</a-form-item>
<a-form-item>
<a-space>
<a-button
type="primary"
html-type="submit"
:loading="databaseStore.loadingURL"
>
Editar
</a-button>
<a-button
danger
@click="router.push('/')"
>
Volver
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</template>
<script setup>
import { onMounted, reactive } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useDatabaseStore } from "../stores/database";
import { regExpUrl } from "../utils/regExpUrl";
import { message } from "ant-design-vue";
const databaseStore = useDatabaseStore();
const formState = reactive({
url: "",
});
const route = useRoute();
const router = useRouter();
const onFinish = async () => {
const res = await databaseStore.updateUrl(route.params.id, formState.url);
formState.url = "";
if (!res) {
return message.success("URL modificada con éxito");
}
message.error(res);
};
onMounted(async () => {
formState.url = await databaseStore.leerUrl(route.params.id);
});
</script>
App.vue
.container {
background-color: rgb(255, 255, 255);
padding: 24px;
min-height: calc(100vh - 64px);
}
.text-center {
text-align: center;
}
Perfil User
Reglas Firestore
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /urls/{id} {
allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.user;
allow create: if request.auth != null;
}
match /users/{id} {
allow read, update, delete: if request.auth != null && request.auth.uid == id;
allow create: if request.auth != null;
}
}
}
Reglas Storage
Refactorización updateUser:
- Lógica en una sola action.
- Cambio de directorio donde se almacenan las imágenes.
- Se agregó loading.
async updateUser(displayName, imagen) {
this.loadingUser = true;
try {
await updateProfile(auth.currentUser, {
displayName,
});
if (imagen) {
const storageRef = ref(
storage,
`perfiles/${this.userData.uid}`
);
await uploadBytes(storageRef, imagen.originFileObj);
const photoURL = await getDownloadURL(storageRef);
await updateProfile(auth.currentUser, {
photoURL,
});
}
this.setUser(auth.currentUser);
} catch (error) {
console.log(error);
return error.code;
} finally {
this.loadingUser = false;
}
},
Perfil.vue
const onFinish = async () => {
const error = await userStore.updateUser(
userStore.userData.displayName,
fileList.value[0]
);
if (!error) {
return message.success("Se actualizó tu información.");
}
message.error(error.code + " Ocurrió un error al actualizar el perfil");
};
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
function restrictFileSize(sizeInMB) {
return request.resource.size < sizeInMB * 1024 * 1024;
}
function isAllowedFile() {
return request.resource.contentType.matches('image/jpeg')
|| request.resource.contentType.matches('image/png')
}
match /perfiles/{uid} {
allow read, delete: if request.auth != null && request.auth.uid == uid;
allow create: if request.auth != null && restrictFileSize(1) && isAllowedFile();
allow update: if request.auth != null && request.auth.uid == uid && restrictFileSize(1) && isAllowedFile();
}
}
}
Redirect
database.js
async searchURL(id) {
try {
const docRef = doc(db, "urls", id);
const docSpan = await getDoc(docRef);
if (!docSpan.exists()) {
return false;
}
window.location.href = docSpan.data().name;
return docSpan.data().name;
} catch (error) {
console.log(error);
return false;
} finally {
}
},
async addUrl(name) {
this.loadingURL = true;
try {
const objetoDoc = {
name: name,
short: nanoid(6),
user: auth.currentUser.uid,
};
await setDoc(doc(db, "urls", objetoDoc.short), objetoDoc);
this.documents.push({
...objetoDoc,
id: objetoDoc.short,
});
} catch (error) {
console.log(error.code);
} finally {
this.loadingURL = false;
}
},
NotFound.vue
<template>
<h1>404</h1>
</template>
router.js
const redireccion = async (to, from, next) => {
const userStore = useUserStore();
const databaseStore = useDatabaseStore();
userStore.loadingSession = true;
const name = await databaseStore.searchURL(to.params.pathMatch[0]);
if (!name) {
next();
userStore.loadingSession = false;
} else {
userStore.loadingSession = true;
next();
}
};
const routes = [
{
name: "redireccion",
path: "/:pathMatch(.*)*",
component: NotFound,
beforeEnter: redireccion,
},
];
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /urls/{id} {
allow read: if true;
allow update, delete: if request.auth != null && request.auth.uid == resource.data.user;
allow create: if request.auth != null;
}
match /users/{id} {
allow read, update, delete: if request.auth != null && request.auth.uid == id;
allow create: if request.auth != null;
}
}
}