WIP: Responsive design #2084

Closed
Myestery wants to merge 1 commits from main into main
8 changed files with 133 additions and 15 deletions

View 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]"]')
}
}

View File

@@ -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>

View 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>

View File

@@ -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 %>

View File

@@ -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| %>

View File

@@ -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:

View File

@@ -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!

View File

@@ -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