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

# Códigos

# Vite

# 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 (opens new window)
  • 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 (opens new window)
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í (opens new window)
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

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;
    }
  }
}
Last Updated: 26/3/2022, 13:18:07