Browse Source

feat: redesigned reset password page, added validation

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 years ago
parent
commit
65669714e4

+ 19 - 0
frontend/src/App.vue

@@ -392,6 +392,7 @@ button.delete:focus {
 			background-color: darken($green, 5%) !important;
 		}
 	}
+
 	&.is-primary {
 		background-color: $primary-color !important;
 
@@ -400,6 +401,7 @@ button.delete:focus {
 			background-color: darken($primary-color, 5%) !important;
 		}
 	}
+
 	&.is-danger {
 		background-color: $red !important;
 
@@ -408,6 +410,7 @@ button.delete:focus {
 			background-color: darken($red, 5%) !important;
 		}
 	}
+
 	&.is-info {
 		background-color: $blue !important;
 
@@ -418,6 +421,22 @@ button.delete:focus {
 	}
 }
 
+.input-with-button {
+	.control {
+		margin-right: 0px !important;
+	}
+
+	input {
+		height: 36px;
+		border-radius: 3px 0 3px 0;
+	}
+
+	.button {
+		height: 36px;
+		border-radius: 0 3px 3px 0;
+	}
+}
+
 .page-title {
 	margin: 0 0 50px 0;
 }

+ 1 - 1
frontend/src/components/layout/MainHeader.vue

@@ -158,7 +158,7 @@ export default {
 
 		img {
 			max-height: 38px;
-			color: $musareBlue;
+			color: $musare-blue;
 		}
 	}
 

+ 8 - 8
frontend/src/components/modals/EditSong.vue

@@ -1511,7 +1511,7 @@ export default {
 	.genre-helper-header {
 		cursor: move;
 		z-index: 100000001;
-		background-color: $musareBlue;
+		background-color: $musare-blue;
 		padding: 10px;
 		display: block;
 		height: 10px;
@@ -1610,7 +1610,7 @@ export default {
 					}
 
 					.player-play-pause {
-						color: $musareBlue;
+						color: $musare-blue;
 					}
 
 					.player-stop {
@@ -1683,7 +1683,7 @@ export default {
 		}
 
 		.add-button {
-			background-color: $musareBlue !important;
+			background-color: $musare-blue !important;
 			width: 32px;
 
 			i {
@@ -1745,7 +1745,7 @@ export default {
 					font-size: 15px;
 					align-self: center;
 					margin-left: 5px;
-					color: $musareBlue;
+					color: $musare-blue;
 					cursor: pointer;
 					-webkit-user-select: none;
 					-moz-user-select: none;
@@ -1760,7 +1760,7 @@ export default {
 		}
 
 		.list-item-circle {
-			background-color: $musareBlue;
+			background-color: $musare-blue;
 			width: 16px;
 			height: 16px;
 			border-radius: 8px;
@@ -1775,7 +1775,7 @@ export default {
 			user-select: none;
 
 			i {
-				color: $musareBlue;
+				color: $musare-blue;
 				font-size: 14px;
 				margin-left: 1px;
 			}
@@ -1940,13 +1940,13 @@ export default {
 
 		.api-result {
 			background-color: white;
-			border: 0.5px solid $musareBlue;
+			border: 0.5px solid $musare-blue;
 			border-radius: 5px;
 			margin-bottom: 16px;
 		}
 
 		button {
-			background-color: $musareBlue !important;
+			background-color: $musare-blue !important;
 
 			&:focus,
 			&:hover {

+ 4 - 4
frontend/src/components/modals/EditStation.vue

@@ -1063,7 +1063,7 @@ export default {
 			width: 32px;
 
 			&.blue {
-				background-color: $musareBlue !important;
+				background-color: $musare-blue !important;
 			}
 
 			&.red {
@@ -1097,10 +1097,10 @@ export default {
 		user-select: none;
 
 		&.blue {
-			background-color: $musareBlue;
+			background-color: $musare-blue;
 
 			i {
-				color: $musareBlue;
+				color: $musare-blue;
 			}
 		}
 
@@ -1218,7 +1218,7 @@ export default {
 		}
 
 		&.blue {
-			background-color: $musareBlue;
+			background-color: $musare-blue;
 		}
 
 		&.orange {

+ 1 - 1
frontend/src/pages/Home/index.vue

@@ -471,7 +471,7 @@ html {
 				}
 
 				.blue-icon {
-					color: $musareBlue;
+					color: $musare-blue;
 				}
 			}
 		}

+ 2 - 2
frontend/src/pages/Profile.vue

@@ -645,7 +645,7 @@ export default {
 			outline: none;
 			border: none;
 			box-shadow: none;
-			color: $musareBlue;
+			color: $musare-blue;
 			font-size: 22px;
 			line-height: 26px;
 			padding: 7px 0 7px 12px;
@@ -657,7 +657,7 @@ export default {
 
 			&.active {
 				color: $white;
-				background-color: $musareBlue;
+				background-color: $musare-blue;
 			}
 		}
 	}

+ 389 - 58
frontend/src/pages/ResetPassword.vue

@@ -3,65 +3,202 @@
 		<metadata title="Reset password" />
 		<main-header />
 		<div class="container">
-			<!--Implement Validation-->
-			<h1>Step {{ step }}</h1>
-
-			<label v-if="step === 1" class="label">Email Address</label>
-			<div v-if="step === 1" class="control is-grouped">
-				<p class="control is-expanded has-icon has-icon-right">
-					<input
-						v-model="email"
-						class="input"
-						type="email"
-						placeholder="The email address associated with your account"
-					/>
-				</p>
-				<p class="control">
-					<button class="button is-success" @click="submitEmail()">
-						Request
-					</button>
-					<button
+			<div class="content-wrapper">
+				<h1 id="title">Reset your password</h1>
+
+				<div id="steps">
+					<p class="step" :class="{ selected: step === 1 }">1</p>
+					<span class="divider"></span>
+					<p class="step" :class="{ selected: step === 2 }">2</p>
+					<span class="divider"></span>
+					<p class="step" :class="{ selected: step === 3 }">3</p>
+				</div>
+
+				<transition name="steps-fade" mode="out-in">
+					<!-- Step 1 -- Enter email address -->
+					<div
+						class="content-box"
 						v-if="step === 1"
-						class="button is-default skip-step"
-						@click="step = 2"
+						v-bind:key="step"
 					>
-						Skip this step
-					</button>
-				</p>
-			</div>
+						<h2 class="content-box-title">
+							Enter your email address
+						</h2>
+						<p class="content-box-description">
+							We will send a code to your email address to verify
+							your identity.
+						</p>
 
-			<label v-if="step === 2" class="label">Reset Code</label>
-			<div v-if="step === 2" class="control is-grouped">
-				<p class="control is-expanded has-icon has-icon-right">
-					<input
-						v-model="code"
-						class="input"
-						type="text"
-						placeholder="The reset code that was sent to your account's email address"
-					/>
-				</p>
-				<p class="control">
-					<button class="button is-success" v-on:click="verifyCode()">
-						Verify reset code
-					</button>
-				</p>
-			</div>
+						<p class="content-box-optional-helper">
+							<a href="#" @click="step = 2"
+								>Already have a code?</a
+							>
+						</p>
+
+						<div class="content-box-inputs">
+							<div class="control is-grouped input-with-button">
+								<p class="control is-expanded">
+									<input
+										class="input"
+										type="email"
+										placeholder="Enter email address here..."
+										autofocus
+										v-model="email"
+										@keyup.enter="submitEmail()"
+										@blur="onInputBlur('email')"
+									/>
+								</p>
+								<p class="control">
+									<a
+										class="button is-info"
+										href="#"
+										@click="submitEmail()"
+										><i
+											class="material-icons icon-with-button"
+											>mail</i
+										>Request</a
+									>
+								</p>
+							</div>
+							<p
+								class="help"
+								v-if="validation.email.entered"
+								:class="
+									validation.email.valid
+										? 'is-success'
+										: 'is-danger'
+								"
+							>
+								{{ validation.email.message }}
+							</p>
+						</div>
+					</div>
+
+					<!-- Step 2 -- Enter code -->
+					<div
+						class="content-box"
+						v-if="step === 2"
+						v-bind:key="step"
+					>
+						<h2 class="content-box-title">
+							Enter the code sent to your email
+						</h2>
+						<p class="content-box-description">
+							A code has been sent to <strong>email</strong>.
+						</p>
+
+						<p class="content-box-optional-helper">
+							<a
+								href="#"
+								@click="email ? submitEmail() : (step = 1)"
+								>Request another code</a
+							>
+						</p>
 
-			<label v-if="step === 3" class="label">Change password</label>
-			<div v-if="step === 3" class="control is-grouped">
-				<p class="control is-expanded has-icon has-icon-right">
-					<input
-						v-model="newPassword"
-						class="input"
-						type="password"
-						placeholder="New password"
-					/>
-				</p>
-				<p class="control">
-					<button class="button is-success" @click="changePassword()">
-						Change password
-					</button>
-				</p>
+						<div class="content-box-inputs">
+							<div class="control is-grouped input-with-button">
+								<p class="control is-expanded">
+									<input
+										class="input"
+										type="text"
+										placeholder="Enter code here..."
+										autofocus
+										v-model="code"
+										@keyup.enter="verifyCode()"
+									/>
+								</p>
+								<p class="control">
+									<a
+										class="button is-info"
+										href="#"
+										@click="verifyCode()"
+										><i
+											class="material-icons icon-with-button"
+											>vpn_key</i
+										>Verify</a
+									>
+								</p>
+							</div>
+						</div>
+					</div>
+
+					<!-- Step 3 -- Set new password -->
+					<div
+						class="content-box"
+						v-if="step === 3"
+						v-bind:key="step"
+					>
+						<h2 class="content-box-title">
+							Set a new password
+						</h2>
+						<p class="content-box-description">
+							Create a new password for your account.
+						</p>
+
+						<div class="content-box-inputs">
+							<p class="control is-expanded">
+								<label for="new-password">New password</label>
+								<input
+									class="input"
+									id="new-password"
+									type="password"
+									placeholder="Enter password here..."
+									v-model="newPassword"
+									@blur="onInputBlur('newPassword')"
+								/>
+							</p>
+							<p
+								class="help"
+								v-if="validation.newPassword.entered"
+								:class="
+									validation.newPassword.valid
+										? 'is-success'
+										: 'is-danger'
+								"
+							>
+								{{ validation.newPassword.message }}
+							</p>
+
+							<p
+								id="new-password-again-input"
+								class="control is-expanded"
+							>
+								<label for="new-password-again"
+									>New password again</label
+								>
+								<input
+									class="input"
+									id="new-password-again"
+									type="password"
+									placeholder="Enter password here..."
+									v-model="newPasswordAgain"
+									@keyup.enter="changePassword()"
+									@blur="onInputBlur('newPasswordAgain')"
+								/>
+							</p>
+							<p
+								class="help"
+								v-if="validation.newPasswordAgain.entered"
+								:class="
+									validation.newPasswordAgain.valid
+										? 'is-success'
+										: 'is-danger'
+								"
+							>
+								{{ validation.newPasswordAgain.message }}
+							</p>
+
+							<a
+								id="change-password-button"
+								class="button is-success"
+								href="#"
+								@click="changePassword()"
+							>
+								Change password</a
+							>
+						</div>
+					</div>
+				</transition>
 			</div>
 		</div>
 		<main-footer />
@@ -75,6 +212,7 @@ import MainHeader from "../components/layout/MainHeader.vue";
 import MainFooter from "../components/layout/MainFooter.vue";
 
 import io from "../io";
+import validation from "../validation";
 
 export default {
 	components: { MainHeader, MainFooter },
@@ -83,7 +221,25 @@ export default {
 			email: "",
 			code: "",
 			newPassword: "",
-			step: 1
+			newPasswordAgain: "",
+			step: 1,
+			validation: {
+				email: {
+					entered: false,
+					valid: false,
+					message: "Please enter a valid email address."
+				},
+				newPassword: {
+					entered: false,
+					valid: false,
+					message: "Please enter a valid password."
+				},
+				newPasswordAgain: {
+					entered: false,
+					valid: false,
+					message: "This password must match."
+				}
+			}
 		};
 	},
 	mounted() {
@@ -91,8 +247,65 @@ export default {
 			this.socket = socket;
 		});
 	},
+	watch: {
+		email(value) {
+			if (
+				value.indexOf("@") !== value.lastIndexOf("@") ||
+				!validation.regex.emailSimple.test(value)
+			) {
+				this.validation.email.message =
+					"Please enter a valid email address.";
+				this.validation.email.valid = false;
+			} else {
+				this.validation.email.message = "Everything looks great!";
+				this.validation.email.valid = true;
+			}
+		},
+		newPassword(value) {
+			this.checkPasswordMatch(value, this.newPasswordAgain);
+
+			if (!validation.isLength(value, 6, 200)) {
+				this.validation.newPassword.message =
+					"Password must have between 6 and 200 characters.";
+				this.validation.newPassword.valid = false;
+			} else if (!validation.regex.password.test(value)) {
+				this.validation.newPassword.message =
+					"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.";
+				this.validation.newPassword.valid = false;
+			} else {
+				this.validation.newPassword.message = "Everything looks great!";
+				this.validation.newPassword.valid = true;
+			}
+		},
+		newPasswordAgain(value) {
+			this.checkPasswordMatch(this.newPassword, value);
+		}
+	},
 	methods: {
+		checkPasswordMatch(newPassword, newPasswordAgain) {
+			if (newPasswordAgain !== newPassword) {
+				this.validation.newPasswordAgain.message =
+					"This password must match.";
+				this.validation.newPasswordAgain.valid = false;
+			} else {
+				this.validation.newPasswordAgain.message =
+					"Everything looks great!";
+				this.validation.newPasswordAgain.valid = true;
+			}
+		},
+		onInputBlur(inputName) {
+			this.validation[inputName].entered = true;
+		},
 		submitEmail() {
+			if (
+				this.email.indexOf("@") !== this.email.lastIndexOf("@") ||
+				!validation.regex.emailSimple.test(this.email)
+			)
+				return new Toast({
+					content: "Invalid email format.",
+					timeout: 8000
+				});
+
 			if (!this.email)
 				return new Toast({
 					content: "Email cannot be empty",
@@ -105,6 +318,7 @@ export default {
 					new Toast({ content: res.message, timeout: 8000 });
 					if (res.status === "success") {
 						this.step = 2;
+						this.code = ""; // in case: already have a code -> request another code
 					}
 				}
 			);
@@ -115,6 +329,7 @@ export default {
 					content: "Code cannot be empty",
 					timeout: 8000
 				});
+
 			return this.socket.emit(
 				"users.verifyPasswordResetCode",
 				this.code,
@@ -127,11 +342,21 @@ export default {
 			);
 		},
 		changePassword() {
-			if (!this.newPassword)
+			if (
+				this.validation.newPassword.valid &&
+				!this.validation.newPasswordAgain.valid
+			)
 				return new Toast({
-					content: "Password cannot be empty",
+					content: "Please ensure the passwords match.",
 					timeout: 8000
 				});
+
+			if (!this.validation.newPassword.valid)
+				return new Toast({
+					content: "Please enter a valid password.",
+					timeout: 8000
+				});
+
 			return this.socket.emit(
 				"users.changePasswordWithResetCode",
 				this.code,
@@ -161,8 +386,114 @@ export default {
 	}
 }
 
+h1,
+h2,
+p {
+	margin: 0;
+}
+
+.help {
+	margin-bottom: 5px;
+}
+
 .container {
 	padding: 25px;
+
+	#title {
+		color: #000;
+		font-size: 42px;
+		text-align: center;
+	}
+
+	#steps {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		height: 50px;
+		margin-top: 36px;
+
+		.step {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			border-radius: 100%;
+			border: 1px solid $dark-grey;
+			width: 50px;
+			height: 50px;
+			background-color: #fff;
+			font-size: 30px;
+			cursor: pointer;
+
+			&.selected {
+				background-color: $musare-blue;
+				color: #fff;
+				border: 0;
+			}
+		}
+
+		.divider {
+			display: flex;
+			justify-content: center;
+			width: 180px;
+			height: 1px;
+			background-color: $dark-grey;
+		}
+	}
+
+	.content-box {
+		margin-top: 90px;
+		border-radius: 3px;
+		background-color: #fff;
+		border: 1px solid $dark-grey;
+		width: 580px;
+		padding: 40px;
+
+		.content-box-title {
+			font-size: 25px;
+			color: #000;
+		}
+
+		.content-box-description {
+			font-size: 14px;
+			color: $dark-grey;
+		}
+
+		.content-box-optional-helper {
+			margin-top: 15px;
+			color: $musare-blue;
+			text-decoration: underline;
+			font-size: 16px;
+		}
+
+		.content-box-inputs {
+			margin-top: 35px;
+
+			.input-with-button {
+				.button {
+					width: 105px;
+				}
+			}
+
+			label {
+				font-size: 11px;
+			}
+
+			#change-password-button {
+				margin-top: 36px;
+				width: 175px;
+			}
+		}
+	}
+}
+
+.steps-fade-enter-active,
+.steps-fade-leave-active {
+	transition: all 0.3s ease;
+}
+
+.steps-fade-enter,
+.steps-fade-leave-to {
+	opacity: 0;
 }
 
 .skip-step {

+ 6 - 6
frontend/src/pages/Settings.vue

@@ -77,7 +77,7 @@
 			</div>
 			<div class="content account-tab" v-if="activeTab === 'account'">
 				<p class="control is-expanded">
-					<label for="name">Username</label>
+					<label for="username">Username</label>
 					<input
 						class="input"
 						id="username"
@@ -97,7 +97,7 @@
 					{{ validation.username.message }}
 				</p>
 				<p class="control is-expanded">
-					<label for="location">Email</label>
+					<label for="email">Email</label>
 					<input
 						class="input"
 						id="email"
@@ -677,7 +677,7 @@ export default {
 			outline: none;
 			border: none;
 			box-shadow: none;
-			color: $musareBlue;
+			color: $musare-blue;
 			font-size: 22px;
 			line-height: 26px;
 			padding: 7px 0 7px 12px;
@@ -690,7 +690,7 @@ export default {
 
 			&.active {
 				color: $white;
-				background-color: $musareBlue;
+				background-color: $musare-blue;
 			}
 		}
 	}
@@ -756,7 +756,7 @@ export default {
 				left: 2px;
 				top: 2px;
 				border-radius: 3px;
-				background-color: $musareBlue;
+				background-color: $musare-blue;
 				position: absolute;
 			}
 		}
@@ -769,7 +769,7 @@ export default {
 	align-items: flex-start;
 
 	.select:after {
-		border-color: $musareBlue;
+		border-color: $musare-blue;
 	}
 }
 

+ 0 - 16
frontend/src/pages/Station/AddSongToQueue.vue

@@ -414,22 +414,6 @@ tr td {
 	}
 }
 
-.input-with-button {
-	.control {
-		margin-right: 0px !important;
-	}
-
-	input {
-		height: 36px;
-		border-radius: 3px 0 3px 0;
-	}
-
-	.button {
-		height: 36px;
-		border-radius: 0 3px 3px 0;
-	}
-}
-
 .vertical-padding {
 	padding: 20px;
 }

+ 1 - 1
frontend/src/pages/Station/SongsList.vue

@@ -187,7 +187,7 @@ export default {
 }
 
 .media.is-playing {
-	background-color: $musareBlue;
+	background-color: $musare-blue;
 	color: white;
 }
 

+ 1 - 1
frontend/src/pages/Station/StationHeader.vue

@@ -286,7 +286,7 @@ export default {
 
 		img {
 			max-height: 38px;
-			color: $musareBlue;
+			color: $musare-blue;
 		}
 	}
 }

+ 2 - 2
frontend/src/styles/colors.scss

@@ -1,4 +1,4 @@
-$musareBlue: hsl(199, 98%, 48%);
+$musare-blue: hsl(199, 98%, 48%);
 $teal: hsl(171, 100%, 41%);
 $purple: hsl(302, 56%, 36%);
 $light-purple: hsl(263, 49%, 70%);
@@ -19,5 +19,5 @@ $dark-grey: hsl(0, 0%, 30%);
 $dark-grey-2: hsl(0, 0%, 20%);
 $dark-grey-3: hsl(0, 0%, 10%);
 
-$primary-color: $musareBlue;
+$primary-color: $musare-blue;
 $night-mode-secondary: #222;