mirror of
https://github.com/janishutz/libreevent.git
synced 2025-11-25 21:34:24 +00:00
root account setup email and pw checker + other
This commit is contained in:
@@ -74,7 +74,7 @@
|
||||
This method deletes a notification and, in case the notification is being displayed, hides it.
|
||||
*/
|
||||
try {
|
||||
delete notifications[ id ];
|
||||
delete this.notifications[ id ];
|
||||
delete this.queue[ this.queue.findIndex( id ) ];
|
||||
} catch ( error ) {
|
||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<title>No page title :: libreevent</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
||||
<script defer src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -141,6 +141,15 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-radius: 500px;
|
||||
border-style: solid;
|
||||
background-color: #b4d9ff;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
|
||||
236
src/webapp/setup/src/components/notifications.vue
Normal file
236
src/webapp/setup/src/components/notifications.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div id="notifications" @click="handleNotifications();">
|
||||
<div class="message-box" :class="[ location, size ]">
|
||||
<div class="message-container" :class="messageType">
|
||||
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
|
||||
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
|
||||
<p class="message">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'notificationsAPI',
|
||||
props: {
|
||||
location: {
|
||||
type: String,
|
||||
'default': 'topleft',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
'default': 'default',
|
||||
}
|
||||
// Size options: small, default (default option), big, bigger, huge
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
notifications: {},
|
||||
queue: [],
|
||||
message: '',
|
||||
messageType: 'hide',
|
||||
notificationDisplayTime: 0,
|
||||
notificationPriority: 'normal',
|
||||
currentlyDisplayedNotificationID: 0,
|
||||
currentID: { 'critical': 0, 'medium': 1000, 'low': 100000 },
|
||||
displayTimeCurrentNotification: 0,
|
||||
notificationScheduler: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createNotification( message, showDuration, messageType, priority ) {
|
||||
/*
|
||||
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
|
||||
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
|
||||
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
|
||||
*/
|
||||
let id = 0;
|
||||
|
||||
if ( priority === 'critical' ) {
|
||||
this.currentID[ 'critical' ] += 1;
|
||||
id = this.currentID[ 'critical' ];
|
||||
} else if ( priority === 'normal' ) {
|
||||
this.currentID[ 'medium' ] += 1;
|
||||
id = this.currentID[ 'medium' ];
|
||||
} else if ( priority === 'low' ) {
|
||||
this.currentID[ 'low' ] += 1;
|
||||
id = this.currentID[ 'low' ];
|
||||
}
|
||||
this.notifications[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id };
|
||||
this.queue.push( id );
|
||||
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
|
||||
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
|
||||
this.handleNotifications();
|
||||
}
|
||||
return id;
|
||||
},
|
||||
cancelNotification ( id ) {
|
||||
/*
|
||||
This method deletes a notification and, in case the notification is being displayed, hides it.
|
||||
*/
|
||||
try {
|
||||
delete this.notifications[ id ];
|
||||
delete this.queue[ this.queue.findIndex( id ) ];
|
||||
} catch ( error ) {
|
||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||
}
|
||||
if ( this.currentlyDisplayedNotificationID == id ) {
|
||||
this.handleNotifications();
|
||||
}
|
||||
},
|
||||
handleNotifications () {
|
||||
/*
|
||||
This methods should NOT be called in any other component than this one!
|
||||
*/
|
||||
this.displayTimeCurrentNotification = 0;
|
||||
this.notificationDisplayTime = 0;
|
||||
this.message = '';
|
||||
this.queue.sort();
|
||||
if ( this.queue.length > 0 ) {
|
||||
this.message = this.notifications[ this.queue[ 0 ] ][ 'message' ];
|
||||
this.messageType = this.notifications[ this.queue[ 0 ] ][ 'messageType' ];
|
||||
this.priority = this.notifications[ this.queue[ 0 ] ][ 'priority' ];
|
||||
this.currentlyDisplayedNotificationID = this.notifications[ this.queue[ 0 ] ][ 'id' ];
|
||||
this.notificationDisplayTime = this.notifications[ this.queue[ 0 ] ][ 'showDuration' ];
|
||||
this.queue.reverse();
|
||||
this.queue.pop();
|
||||
} else {
|
||||
this.messageType = 'hide';
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.notificationScheduler = setInterval( () => {
|
||||
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
|
||||
this.handleNotifications();
|
||||
} else {
|
||||
this.displayTimeCurrentNotification += 0.5;
|
||||
}
|
||||
}, 500 );
|
||||
},
|
||||
unmounted ( ) {
|
||||
clearInterval( this.notificationScheduler );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-box {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
color: white;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.default {
|
||||
height: 10vh;
|
||||
width: 15vw;
|
||||
}
|
||||
|
||||
.small {
|
||||
height: 7vh;
|
||||
width: 11vw;
|
||||
}
|
||||
|
||||
.big {
|
||||
height: 12vh;
|
||||
width: 17vw;
|
||||
}
|
||||
|
||||
.bigger {
|
||||
height: 15vh;
|
||||
width: 20vw;
|
||||
}
|
||||
|
||||
.huge {
|
||||
height: 20vh;
|
||||
width: 25vw;
|
||||
}
|
||||
|
||||
.topleft {
|
||||
top: 3vh;
|
||||
left: 0.5vw;
|
||||
}
|
||||
|
||||
.topright {
|
||||
top: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.bottomright {
|
||||
bottom: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.bottomleft {
|
||||
top: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: all 0.5s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.types {
|
||||
color: white;
|
||||
border-radius: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: 5%;
|
||||
padding: 1.5%;
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-right: 5%;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.ok {
|
||||
background-color: rgb(1, 71, 1);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgb(114, 1, 1);
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: rgb(44, 112, 151);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgb(0, 0, 99);
|
||||
}
|
||||
|
||||
.progress-spinner {
|
||||
animation: spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate( 0deg );
|
||||
}
|
||||
to {
|
||||
transform: rotate( 720deg );
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,9 +15,14 @@
|
||||
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#basic-setup" target="_blank">here</a></p>
|
||||
<h2>Database</h2>
|
||||
<p>A database is a piece of software that specializes in storing data. libreevent can use most SQL based databases as well as a custom JSON-based database. You are strongly encouraged to use a SQL based database, as they perform significantly better. Read more
|
||||
<a href="https://libreevent.janishutz.com/docs/setup/installation#database">here</a>
|
||||
<a href="https://libreevent.janishutz.com/docs/setup/installation#database" target="_blank">here</a>
|
||||
</p>
|
||||
<form>
|
||||
<label for="dbType">Database type</label><br>
|
||||
<select name="dbType" id="dbType" v-model="formData.dbType">
|
||||
<option value="mysql">SQL-Database</option>
|
||||
<option value="json">JSON-Database</option>
|
||||
</select>
|
||||
<form v-if="formData.dbType === 'mysql'">
|
||||
<label for="host">Database host name</label><br>
|
||||
<input type="url" name="host" id="host"><br>
|
||||
<label for="database">Database name</label><br>
|
||||
@@ -30,7 +35,12 @@
|
||||
<input type="number" name="port" id="port" min="1" value="3306" max="65535"><br>
|
||||
</form>
|
||||
<h2>Email</h2>
|
||||
<p>An email address is required for libreevent to send out mails to users automatically, including their ticket and, in case Two-Factor-Authentication is enabled,
|
||||
a Two-Factor-Authentication email.</p>
|
||||
|
||||
<h3>Account</h3>
|
||||
<p>Here you have to enter the connection details for an email account. Most webhosting plans come with email addresses, so you might as well create a new one.
|
||||
Note that you can customize how the sender of the mail appears down below in the display section.</p>
|
||||
<form>
|
||||
<label for="host">SMTP Server</label><br>
|
||||
<input type="url" name="host" id="host"><br>
|
||||
@@ -42,6 +52,10 @@
|
||||
<input type="password" name="pass" id="pass"><br>
|
||||
</form>
|
||||
<h3>Display</h3>
|
||||
<p>Here you can adjust how the email sender appears to the customer. This also means, that the email address shown below might receive a response if
|
||||
a customer does not possess the ability to read, which might happen from time to time. All mails contain the information that one should not respond
|
||||
to them.
|
||||
</p>
|
||||
<form>
|
||||
<label for="display">Display name (what is shown to user in from field)</label><br>
|
||||
<input type="url" name="display" id="display"><br>
|
||||
@@ -53,6 +67,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#dbType {
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { useBackendStore } from '@/stores/backendStore.js';
|
||||
import { mapStores } from 'pinia';
|
||||
@@ -60,7 +80,10 @@
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
formData: {}
|
||||
formData: {
|
||||
'dbType': 'mysql',
|
||||
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="home">
|
||||
<img alt="Libreevent logo" src="../assets/logo.png">
|
||||
<h1>Welcome to libreǝvent!</h1>
|
||||
<p>Let's start the setup by entering the setup key below! You may define a setup key in the setupkey.txt file of libreevent. See <a href="https://libreevent.janishutz.com/docs/setup/installation" target="_blank">here</a> for more instructions</p>
|
||||
<p>Let's start the setup by entering the setup key below! You may define a setup key in the <i>setupkey.txt</i> file of libreevent. See <a href="https://libreevent.janishutz.com/docs/setup/installation" target="_blank">here</a> for more instructions</p>
|
||||
<form>
|
||||
<label for="key">Your setup key</label><br>
|
||||
<input type="text" v-model="formData[ 'key' ]" required name="key" id="key">
|
||||
@@ -48,4 +48,8 @@
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -13,8 +13,11 @@
|
||||
<img src="@/assets/logo.png" alt="libreevent logo" style="height: 30vh;">
|
||||
<h1>Setup complete!</h1>
|
||||
<p>Congratulations on finishing the setup of libreǝvent!</p>
|
||||
<p>Please restart the node.js application to have it load the actual user interface for libreevent and finish setup. You may then log in at <a :href="windowURL" target="_blank">{{ windowURL }}</a>. You may open that link now and then come back to that page once libreevent is restarted.</p>
|
||||
<p>In the admin panel, there are a few things you still need to change. You may find a list of all things <a href="https://libreevent.janishutz.com/docs/setup/afterSetup" target="_blank">here</a></p>
|
||||
<p>Please restart the node.js application to have it load the actual user interface for libreevent and finish setup.
|
||||
You may then log in at <a :href="windowURL" target="_blank">{{ windowURL }}</a>.
|
||||
You may open that link now and then come back to that page once libreevent is restarted.</p>
|
||||
<p>In the admin panel, there are a few things you still need to change. You may find a list of all things
|
||||
<a href="https://libreevent.janishutz.com/docs/setup/afterSetup" target="_blank">here</a></p>
|
||||
<div class="list-wrapper">
|
||||
<ul>
|
||||
<li>Choose a payment gateway and set it up</li>
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<h1>Root account</h1>
|
||||
<p>The root account is the most powerful account. Therefore, it should only be used if really necessary and should have a strong password. It also always requires Two Factor Authentication for added security. You may log into the root account by typing 'root' into the Email/Username field on the admin login screen.</p>
|
||||
<p>The root account is the most powerful account. Therefore, it should only be used if really necessary and should have a strong password.
|
||||
It also always requires Two Factor Authentication for added security.
|
||||
You may log into the root account by typing 'root' into the Email-Address field on the admin login screen.
|
||||
Therefore, the email used for the root account may also be used for an additional admin account.</p>
|
||||
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#root-account" target="_blank">here</a></p>
|
||||
<p>Password requirements:</p>
|
||||
<p>By default (when the toggle "Enforce password requirements" below is enabled), libreevent forces you to follow the password requirements listed below.
|
||||
You may turn off those password requirements and use any password, but we strongly advice against this.</p>
|
||||
<ul style="list-style: none;">
|
||||
<li>At least 15 characters long</li>
|
||||
<li>At least 2 special characters</li>
|
||||
@@ -22,36 +26,223 @@
|
||||
</ul>
|
||||
<form>
|
||||
<label for="mail">Email address for 2FA</label><br>
|
||||
<input type="email" name="mail" id="mail"><br>
|
||||
<input type="email" name="mail" id="mail" v-model="formData.mail" @keyup="emailLiveChecker()"><br>
|
||||
<p v-if="emailStatus" class="email-status">{{ emailStatus }}</p>
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" name="password" id="password"><br>
|
||||
<input type="password" name="password" id="password" v-model="formData.password"><br>
|
||||
<label for="password2">Confirm password</label><br>
|
||||
<input type="password" name="password2" id="password2">
|
||||
<input type="password" name="password2" id="password2" v-model="formData.password2"><br>
|
||||
<label for="pwCheck">Enforce password requirements (leaving this turned on is strongly recommended)</label><br>
|
||||
<label class="switch">
|
||||
<input type="checkbox" v-model="passwordCheck">
|
||||
<span class="slider round"></span>
|
||||
</label><br>
|
||||
</form>
|
||||
<button @click="submit()" class="button">Continue</button>
|
||||
</div>
|
||||
<notifications ref="notification" location="topright" size="bigger"></notifications>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useBackendStore } from '@/stores/backendStore.js';
|
||||
import { mapStores } from 'pinia';
|
||||
import notifications from '../components/notifications.vue';
|
||||
|
||||
const lookup = [ '@', '!', '.', ',', '?', '%', '&', '-', '_', ':', ';', '*', '§', '<', '>', '{', '}', '[', ']', '(', ')', '/', '#' ];
|
||||
// TODO: Also add this to user signup
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
formData: {}
|
||||
formData: {},
|
||||
passwordCheck: true,
|
||||
emailStatus: '',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
notifications,
|
||||
},
|
||||
computed: {
|
||||
...mapStores( useBackendStore )
|
||||
},
|
||||
methods: {
|
||||
emailLiveChecker () {
|
||||
setTimeout( () => {
|
||||
if ( this.checkEmail() ) {
|
||||
this.emailStatus = '';
|
||||
} else {
|
||||
this.emailStatus = 'Invalid email address';
|
||||
}
|
||||
}, 100 );
|
||||
},
|
||||
checkEmail () {
|
||||
const mail = this.formData.mail ?? '';
|
||||
let stat = { 'atPos': 0, 'topLevelPos': 0 };
|
||||
for ( let l in mail ) {
|
||||
if ( stat[ 'atPos' ] > 0 ) {
|
||||
if ( mail[ l ] === '@' ) {
|
||||
return false;
|
||||
} else if ( mail[ l ] === '.' ) {
|
||||
if ( stat[ 'topLevelPos' ] > 0 ) {
|
||||
if ( l > stat[ 'topLevelPos' ] + 2 ) {
|
||||
stat[ 'topLevelPos' ] = parseInt( l );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if ( l > stat[ 'atPos' ] + 2 ) {
|
||||
stat[ 'topLevelPos' ] = parseInt( l );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if ( !( /[a-z]/.test( mail[ l ] ) || /[A-Z]/.test( mail[ l ] ) || /[1-9]/.test( mail[ l ] ) ) ) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if ( mail[ l ] === '@' ) {
|
||||
if ( l > 2 ) {
|
||||
stat[ 'atPos' ] = parseInt( l );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if ( !( /[a-z]/.test( mail[ l ] ) || /[A-Z]/.test( mail[ l ] ) || /[1-9]/.test( mail[ l ] ) || mail[ l ] === '.' ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if ( mail.length > stat[ 'topLevelPos' ] + 2 && stat[ 'topLevelPos' ] > 0 && stat[ 'atPos' ] > 0 ) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
submit () {
|
||||
// TODO: Require confirming email before proceeding
|
||||
// TODO: Maybe require confirming email before proceeding
|
||||
if ( this.formData.mail && this.formData.password && this.formData.password2 ) {
|
||||
if ( this.checkEmail() ) {
|
||||
if ( this.formData.password == this.formData.password2 ) {
|
||||
if ( this.passwordCheck ) {
|
||||
let requirementsCount = { 'special': 0, 'numbers': 0, 'lower': 0, 'upper': 0, 'incorrect': '' };
|
||||
const pw = this.formData.password;
|
||||
for ( let l in pw ) {
|
||||
console.log( pw[ l ] );
|
||||
if ( /[a-z]/.test( pw[ l ] ) ) {
|
||||
requirementsCount[ 'lower' ] += 1;
|
||||
} else if ( /[A-Z]/.test( pw[ l ] ) ) {
|
||||
requirementsCount[ 'upper' ] += 1;
|
||||
} else if ( lookup.includes( pw[ l ] ) ) {
|
||||
requirementsCount[ 'special' ] += 1;
|
||||
} else if ( !isNaN( pw[ l ] * 1 ) ) {
|
||||
requirementsCount[ 'number' ] += 1;
|
||||
} else {
|
||||
console.log( 'incorrect letter' );
|
||||
requirementsCount[ 'incorrect' ] = pw[ l ];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( requirementsCount[ 'incorrect' ] ) {
|
||||
this.$refs.notification.createNotification( `Character "${ requirementsCount[ 'incorrect' ] }" cannot be used for passwords`, 5, 'error', 'normal' );
|
||||
} else {
|
||||
if ( pw.length > 14 ) {
|
||||
if ( requirementsCount[ 'lower' ] > 1 && requirementsCount[ 'upper' ] > 1 && requirementsCount[ 'special' ] > 1 && requirementsCount[ 'numbers' ] > 1 ) {
|
||||
this.proceed();
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'Your password does not fulfill the requirements', 5, 'error', 'normal' );
|
||||
}
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'Your password is not long enough', 5, 'error', 'normal' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.proceed();
|
||||
}
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'Passwords do not match', 10, 'error', 'normal' );
|
||||
}
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'The email address you entered is not an email address', 10, 'error', 'normal' );
|
||||
}
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'One or more fields missing!', 10, 'error', 'normal' );
|
||||
}
|
||||
},
|
||||
proceed () {
|
||||
this.backendStore.addVisitedSetupPages( 'page', true );
|
||||
this.$router.push( 'page' );
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.email-status {
|
||||
margin-top: -10px;
|
||||
color: red;
|
||||
font-style: italic;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_switch */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user