feat: add sign up page
This commit is contained in:
parent
84806bdc47
commit
240e2f97fb
14
package.json
14
package.json
@ -2,17 +2,27 @@
|
|||||||
"name": "firebase-auth-vue",
|
"name": "firebase-auth-vue",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rsbuild build",
|
"build": "rsbuild build",
|
||||||
"dev": "rsbuild dev --open",
|
"dev": "rsbuild dev",
|
||||||
"preview": "rsbuild preview"
|
"preview": "rsbuild preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@rsbuild/core": "1.2.5",
|
"@rsbuild/core": "1.2.5",
|
||||||
"@rsbuild/plugin-vue": "1.0.5",
|
"@rsbuild/plugin-vue": "1.0.5",
|
||||||
|
"@types/node": "22.13.1",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
999
pnpm-lock.yaml
generated
999
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,9 +3,17 @@ import { pluginVue } from '@rsbuild/plugin-vue';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [pluginVue()],
|
plugins: [pluginVue()],
|
||||||
server: {
|
html: {
|
||||||
open: {
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
29
src/App.vue
29
src/App.vue
@ -1,28 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="container">
|
||||||
<h1>Rsbuild with VueJS</h1>
|
<router-view></router-view>
|
||||||
<p>Start building amazing things with Rsbuild.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content {
|
.container {
|
||||||
display: flex;
|
max-width: 700px;
|
||||||
min-height: 100vh;
|
margin: 0 auto;
|
||||||
line-height: 1.1;
|
font-family: Arial, sans-serif;
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
14
src/components/Loader.vue
Normal file
14
src/components/Loader.vue
Normal 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
12
src/firebase/index.ts
Normal 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);
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
22
src/index.ts
22
src/index.ts
@ -1,5 +1,23 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.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
46
src/pages/SignUp.vue
Normal 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
15
src/router/index.ts
Normal 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
84
src/store/auth.ts
Normal 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
5
src/store/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
export default pinia
|
||||||
@ -18,7 +18,10 @@
|
|||||||
/* type checking */
|
/* type checking */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true
|
"noUnusedParameters": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user