Skip to content

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
sh
# 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

sh
npm install vue-router@4

router.js

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

js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

createApp(App).use(router).mount("#app");

App.vue

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
sh
npm install pinia

main.js

js
import { createPinia } from "pinia";

app.use(createPinia());

STATE

stores/user.js

js
import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
  state: () => ({
    userData: "bluuweb",
  }),
});

Home.vue

vue
<template>
  <h1>Home {{ userStore.userData }}</h1>
</template>

<script setup>
import { useUserStore } from "../stores/user";
const userStore = useUserStore();
</script>

Login.vue

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í
js
import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
  state: () => ({
    userData: "bluuweb",
  }),
  getters: {
    userMayuscula(state) {
      return state.userData.toUpperCase();
    },
  },
});

Login.vue

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:
js
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

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

sh
npm install firebase

firebaseConfig.js

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

js
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

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

js
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

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

js
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

vue
<button @click="useStore.signOutUser">Logout</button>

Ruta

store/user.js (actions)

js
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

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

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

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

js
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

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

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

js
urls: [
    id1: {
        name: 'https://bluuweb.org',
        short: 'aDgdGd',
        user: 'pQycjKGmIKQ2wL4P1jvkAPhH4gh2'
    },
    id2: {
        name: 'https://firebase.com',
        short: 'aDgdGd',
        user: 'pQycjKGmIKQ2wL4P1jvkAPhH4gh2'
    }
]

Leer doc

database.js

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

js
import { useDatabaseStore } from "./database";
js
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

js
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

js
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;
    }
},
vue
<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

js
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

js
const routes = [
  { path: "/", component: Home, beforeEnter: requireAuth },
  { path: "/editar/:id", component: Editar, beforeEnter: requireAuth },
  { path: "/login", component: Login },
  { path: "/register", component: Register },
];
vue
<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

js
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

js
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

sh
npm install ant-design-vue@next --save
sh
npm i unplugin-vue-components

vite.config.js

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()],
    }),
  ],
});
vue
<a-button type="primary" size="large">Boton</a-button>

Layout

Login.vue

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

vue
<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

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

js
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

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>
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);
        return error.code;
    } finally {
        this.loadingUser = false;
    }
},

Register.vue

js
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

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

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

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

vue
<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

vue
<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

css
.container {
  background-color: rgb(255, 255, 255);
  padding: 24px;
  min-height: calc(100vh - 64px);
}
.text-center {
  text-align: center;
}

Perfil User

Reglas Firestore

js
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.
js
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

js
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");
};
js
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

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

vue
<template>
  <h1>404</h1>
</template>

router.js

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,
  },
];
js
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;
    }
  }
}