feat: add sign up page

This commit is contained in:
Sergey Krylov 2025-02-11 07:32:47 +03:00
parent 84806bdc47
commit 240e2f97fb
14 changed files with 1232 additions and 38 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22.13.1

View File

@ -2,17 +2,27 @@
"name": "firebase-auth-vue",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "rsbuild build",
"dev": "rsbuild dev --open",
"dev": "rsbuild dev",
"preview": "rsbuild preview"
},
"dependencies": {
"vue": "3.5.13"
"@primevue/themes": "4.2.5",
"axios": "1.7.9",
"firebase": "11.3.0",
"pinia": "2.3.1",
"primeflex": "3.3.1",
"primeicons": "7.0.0",
"primevue": "4.2.5",
"vue": "3.5.13",
"vue-router": "4.5.0"
},
"devDependencies": {
"@rsbuild/core": "1.2.5",
"@rsbuild/plugin-vue": "1.0.5",
"@types/node": "22.13.1",
"typescript": "5.7.3"
}
}

999
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,17 @@ import { pluginVue } from '@rsbuild/plugin-vue';
export default defineConfig({
plugins: [pluginVue()],
server: {
open: {
}
}
html: {
favicon: 'https://www.gstatic.com/devrel-devsite/prod/v9d8458b4d932582248eb0dcdffdc6a8fe4b6994060b14993d6b364721b7e870f/firebase/images/favicon.png',
},
source: {
define: {
'process.env.FIREBASE_API_KEY': JSON.stringify(process.env.FIREBASE_API_KEY),
'process.env.FIREBASE_AUTH_DOMAIN': JSON.stringify(process.env.FIREBASE_AUTH_DOMAIN),
'process.env.FIREBASE_PROJECT_ID': JSON.stringify(process.env.FIREBASE_PROJECT_ID),
'process.env.FIREBASE_STORAGE_BUCKET': JSON.stringify(process.env.FIREBASE_STORAGE_BUCKET),
'process.env.FIREBASE_MESSAGING_SENDER_ID': JSON.stringify(process.env.FIREBASE_MESSAGING_SENDER_ID),
'process.env.FIREBASE_APP_ID': JSON.stringify(process.env.FIREBASE_APP_ID),
},
},
});

View File

@ -1,28 +1,13 @@
<template>
<div class="content">
<h1>Rsbuild with VueJS</h1>
<p>Start building amazing things with Rsbuild.</p>
<div class="container">
<router-view></router-view>
</div>
</template>
<style scoped>
.content {
display: flex;
min-height: 100vh;
line-height: 1.1;
text-align: center;
flex-direction: column;
justify-content: center;
}
.content h1 {
font-size: 3.6rem;
font-weight: 700;
}
.content p {
font-size: 1.2rem;
font-weight: 400;
opacity: 0.5;
}
.container {
max-width: 700px;
margin: 0 auto;
font-family: Arial, sans-serif;
}
</style>

14
src/components/Loader.vue Normal file
View File

@ -0,0 +1,14 @@
<script setup>
import ProgressSpinner from 'primevue/progressspinner';
</script>
<template>
<ProgressSpinner
style="
width: 50px;
height: 50px"
strokeWidth="8"
fill="var(--surface-ground)"
animationDuration=".5s"
aria-label="Custom ProgressSpinner" />
</template>

12
src/firebase/index.ts Normal file
View File

@ -0,0 +1,12 @@
import { initializeApp } from "firebase/app";
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
export const firebaseApp = initializeApp(firebaseConfig);

View File

@ -1,6 +0,0 @@
body {
margin: 0;
color: #fff;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
background-image: linear-gradient(to bottom, #020917, #101725);
}

View File

@ -1,5 +1,23 @@
import { createApp } from 'vue';
import App from './App.vue';
import './index.css';
import router from './router';
import pinia from './store';
import './firebase';
import Aura from '@primevue/themes/lara';
import PrimeVue from 'primevue/config';
import 'primeicons/primeicons.css'
import 'primeflex/primeflex.css'
createApp(App)
.use(router)
.use(pinia)
.use(PrimeVue, {
theme: {
preset: Aura,
options: {
darkModeSelector: false
}
}
})
.mount('#root');
createApp(App).mount('#root');

46
src/pages/SignUp.vue Normal file
View File

@ -0,0 +1,46 @@
<script setup>
import { Button, InputText, Message } from 'primevue';
import { ref } from 'vue';
import { useAuthStore } from '@/store/auth';
import Loader from '@/components/Loader.vue';
const authStore = useAuthStore()
const email = ref('')
const password = ref('')
const signup = async () => {
await authStore.signUp({ email: email.value, password: password.value })
}
</script>
<template>
<h2>Sign Up</h2>
<form class="flex flex-column gap-3" @submit.prevent="signup">
<transition-group name="p-message" tag="div" class="flex flex-col">
<Message v-if="authStore.error" closable icon="pi pi-times-circle" severity="error">{{authStore.error}}</Message>
</transition-group>
<div class="p-inputgroup flex align-items-center gap-4">
<span class="p-inputgroup-addon">
<i class="pi pi-user"></i>
</span>
<InputText type="email" class="flex-1" v-model="email" placeholder="Your Email" />
</div>
<div class="p-inputgroup flex align-items-center gap-4">
<span class="p-inputgroup-addon">
<i class="pi pi-at"></i>
</span>
<InputText type="password" class="flex-1" v-model="password" placeholder="Password" />
</div>
<div class="flex flex-column gap-3">
<Loader v-if="authStore.loading" />
<Button v-else label="Signup" @click="signup"/>
<span>Are you already registered? <router-link to="/signin">Sign in</router-link></span>
</div>
</form>
</template>

15
src/router/index.ts Normal file
View File

@ -0,0 +1,15 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/sign-up',
name: 'SignUp',
component: () => import('../pages/SignUp.vue')
}
]
})
export default router;

84
src/store/auth.ts Normal file
View File

@ -0,0 +1,84 @@
import axios, { AxiosError } from 'axios';
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAuthStore = defineStore('auth', () => {
const userInfo = ref({
token: '',
email: '',
userId: '',
refreshToken: '',
expiresIn: '',
})
const loading = ref(false);
const error = ref('');
const signUp = async (payload: {email: string; password: string}) => {
error.value = '';
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${process.env.FIREBASE_API_KEY}`
type AuthResponse = {
kind: string
idToken: string
email: string;
refreshToken: string;
expiresIn: string;
localId: string;
}
try {
loading.value = true;
const { data } = await axios.post<AuthResponse>(url, {
...payload,
returnSecureToken: true
})
userInfo.value = {
email: data.email,
expiresIn: data.expiresIn,
refreshToken: data.refreshToken,
userId: data.localId,
token: data.idToken,
}
} catch (e) {
if (e instanceof AxiosError) {
switch (e?.response?.data?.error?.message) {
case 'EMAIL_EXISTS':
error.value = 'Email already exists';
break;
case 'OPERATION_NOT_ALLOWED':
error.value = 'Operation not allowed';
break;
case 'TOO_MANY_ATTEMPTS_TRY_LATER':
error.value = 'Too many attempts';
break;
case 'EMAIL_NOT_FOUND':
error.value = 'Email not found';
break;
case 'INVALID_PASSWORD':
error.value = 'Invalid password';
break;
case 'WEAK_PASSWORD':
error.value = 'Password is weak';
break;
case 'USER_DISABLED':
error.value = 'User disabled';
break;
default:
error.value = 'Something went wrong';
return;
}
}
error.value = 'Something went wrong';
} finally {
loading.value = false;
}
};
return {
signUp,
userInfo,
error,
loading,
}
})

5
src/store/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { createPinia } from 'pinia';
const pinia = createPinia()
export default pinia

View File

@ -18,7 +18,10 @@
/* type checking */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
"noUnusedParameters": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}