WIP: Responsive design #2084
65
app/javascript/controllers/password_controller.js
Normal file
65
app/javascript/controllers/password_controller.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Controls the password validation requirements UI
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"lengthIconX", "lengthIconCheck", "caseIconX", "caseIconCheck",
|
||||
"numberIconX", "numberIconCheck", "specialIconX", "specialIconCheck",
|
||||
"lengthBar", "caseBar", "numberBar", "specialBar"
|
||||
]
|
||||
|
||||
connect() {
|
||||
// Initialize if there's already a password value
|
||||
if (this.passwordField && this.passwordField.value) {
|
||||
this.validateCriteria()
|
||||
}
|
||||
}
|
||||
|
||||
validateCriteria() {
|
||||
const password = this.passwordField.value
|
||||
|
||||
// Validate minimum length (8 characters)
|
||||
const hasLength = password.length >= 8
|
||||
this.updateCriterionIcons(this.lengthIconXTarget, this.lengthIconCheckTarget, hasLength)
|
||||
this.updateBar(this.lengthBarTarget, hasLength)
|
||||
|
||||
// Validate upper and lowercase letters
|
||||
const hasCase = /[a-z]/.test(password) && /[A-Z]/.test(password)
|
||||
this.updateCriterionIcons(this.caseIconXTarget, this.caseIconCheckTarget, hasCase)
|
||||
this.updateBar(this.caseBarTarget, hasCase)
|
||||
|
||||
// Validate numbers
|
||||
const hasNumber = /[0-9]/.test(password)
|
||||
this.updateCriterionIcons(this.numberIconXTarget, this.numberIconCheckTarget, hasNumber)
|
||||
this.updateBar(this.numberBarTarget, hasNumber)
|
||||
|
||||
// Validate special characters
|
||||
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
|
||||
this.updateCriterionIcons(this.specialIconXTarget, this.specialIconCheckTarget, hasSpecial)
|
||||
this.updateBar(this.specialBarTarget, hasSpecial)
|
||||
}
|
||||
|
||||
updateCriterionIcons(xIcon, checkIcon, isValid) {
|
||||
if (isValid) {
|
||||
xIcon.classList.add("hidden")
|
||||
checkIcon.classList.remove("hidden")
|
||||
} else {
|
||||
xIcon.classList.remove("hidden")
|
||||
checkIcon.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
updateBar(barElement, isValid) {
|
||||
if (isValid) {
|
||||
barElement.classList.remove("bg-gray-200")
|
||||
barElement.classList.add("bg-green-500")
|
||||
} else {
|
||||
barElement.classList.remove("bg-green-500")
|
||||
barElement.classList.add("bg-gray-200")
|
||||
}
|
||||
}
|
||||
|
||||
get passwordField() {
|
||||
return this.element.querySelector('input[name="user[password]"]')
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,11 @@
|
||||
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
|
||||
</h2>
|
||||
|
||||
<% if controller_name == "sessions" %>
|
||||
<p class="text-sm text-center">
|
||||
<%= tag.span t(".no_account"), class: "text-secondary" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-primary hover:underline transition" %>
|
||||
</p>
|
||||
<% elsif controller_name == "registrations" %>
|
||||
<p class="text-sm text-center text-gray-600">
|
||||
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:underline transition" %>
|
||||
</p>
|
||||
<%# switch component to switch from signin to signup, let it show active tab depending on page %>
|
||||
<% if controller_name == "sessions" || controller_name == "registrations" %>
|
||||
<div class="flex justify-center mt-4">
|
||||
<%= render "layouts/shared/switch", active: controller_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
12
app/views/layouts/shared/_switch.html.erb
Normal file
12
app/views/layouts/shared/_switch.html.erb
Normal file
@@ -0,0 +1,12 @@
|
||||
<%# Switch component to toggle between sign-in and sign-up tabs %>
|
||||
<div class="flex rounded-md shadow-sm bg-gray-100 p-1 w-fit mx-auto">
|
||||
<%= link_to new_session_path,
|
||||
class: "px-4 py-1 text-sm font-medium rounded-md #{active == 'sessions' ? 'bg-white text-primary shadow-sm' : 'text-gray-600 hover:text-gray-800'}" do %>
|
||||
<%= t("layouts.auth.sign_in") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_registration_path,
|
||||
class: "px-4 py-1 text-sm font-medium rounded-md #{active == 'registrations' ? 'bg-white text-primary shadow-sm' : 'text-gray-600 hover:text-gray-800'}" do %>
|
||||
<%= t("layouts.auth.sign_up") %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -32,8 +32,52 @@
|
||||
placeholder: "you@example.com",
|
||||
label: true,
|
||||
disabled: @invitation.present? %>
|
||||
<%= form.password_field :password, autocomplete: "new-password", required: "required", label: true, maxlength: 72 %>
|
||||
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
|
||||
|
||||
<div data-controller="password">
|
||||
<%= form.password_field :password,
|
||||
autocomplete: "new-password",
|
||||
required: "required",
|
||||
label: true,
|
||||
maxlength: 72,
|
||||
data: { action: "input->password#validateCriteria" } %>
|
||||
|
||||
<div class="mt-4">
|
||||
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal lines with combined width of the password field -->
|
||||
<div class="grid grid-cols-4 gap-2 mt-2 mb-4">
|
||||
<div class="h-1 bg-gray-200 rounded" data-password-target="lengthBar"></div>
|
||||
<div class="h-1 bg-gray-200 rounded" data-password-target="caseBar"></div>
|
||||
<div class="h-1 bg-gray-200 rounded" data-password-target="numberBar"></div>
|
||||
<div class="h-1 bg-gray-200 rounded" data-password-target="specialBar"></div>
|
||||
</div>
|
||||
|
||||
<!-- Password requirement criteria, vertically stacked -->
|
||||
<div class="space-y-2 mb-4">
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-red-400 transition-colors", data: { password_target: "lengthIconX" } %>
|
||||
<%= lucide_icon "check", class: "w-4 h-4 text-green-500 transition-colors hidden", data: { password_target: "lengthIconCheck" } %>
|
||||
<span>Minimum 8 characters</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-red-400 transition-colors", data: { password_target: "caseIconX" } %>
|
||||
<%= lucide_icon "check", class: "w-4 h-4 text-green-500 transition-colors hidden", data: { password_target: "caseIconCheck" } %>
|
||||
<span>Upper and lowercase letters</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-red-400 transition-colors", data: { password_target: "numberIconX" } %>
|
||||
<%= lucide_icon "check", class: "w-4 h-4 text-green-500 transition-colors hidden", data: { password_target: "numberIconCheck" } %>
|
||||
<span>A number (0-9)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-red-400 transition-colors", data: { password_target: "specialIconX" } %>
|
||||
<%= lucide_icon "check", class: "w-4 h-4 text-green-500 transition-colors hidden", data: { password_target: "specialIconCheck" } %>
|
||||
<span>A special character (!, @, #, $, %, etc)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if invite_code_required? && !@invitation %>
|
||||
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%
|
||||
header_title t(".title")
|
||||
header_title t(".title")
|
||||
%>
|
||||
|
||||
<%= styled_form_with url: sessions_path, class: "space-y-4", data: { turbo: false } do |form| %>
|
||||
|
||||
@@ -5,7 +5,7 @@ en:
|
||||
existing_account: Already have an account?
|
||||
no_account: New to Maybe?
|
||||
sign_in: Sign in
|
||||
sign_up: Create account
|
||||
sign_up: Sign up
|
||||
your_account: Your account
|
||||
shared:
|
||||
footer:
|
||||
|
||||
@@ -18,7 +18,7 @@ en:
|
||||
role_admin: administrator
|
||||
role_member: member
|
||||
submit: Create account
|
||||
title: Create your account
|
||||
title: Sign Up to Maybe
|
||||
welcome_body: To get started, you must sign up for a new account. You will
|
||||
then be able to configure additional settings within the app.
|
||||
welcome_title: Welcome to Self Hosted Maybe!
|
||||
|
||||
@@ -10,5 +10,5 @@ en:
|
||||
email_placeholder: you@example.com
|
||||
forgot_password: Forgot your password?
|
||||
password: Password
|
||||
submit: Log in
|
||||
title: Sign in to your account
|
||||
submit: Continue
|
||||
title: Sign in to Maybe
|
||||
|
||||
Reference in New Issue
Block a user