diff --git a/app/javascript/controllers/password_controller.js b/app/javascript/controllers/password_controller.js new file mode 100644 index 00000000..151c360c --- /dev/null +++ b/app/javascript/controllers/password_controller.js @@ -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]"]') + } +} \ No newline at end of file diff --git a/app/views/layouts/auth.html.erb b/app/views/layouts/auth.html.erb index f6fd8f2e..cb71025d 100644 --- a/app/views/layouts/auth.html.erb +++ b/app/views/layouts/auth.html.erb @@ -12,14 +12,11 @@ <%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %> - <% if controller_name == "sessions" %> -

- <%= tag.span t(".no_account"), class: "text-secondary" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-primary hover:underline transition" %> -

- <% elsif controller_name == "registrations" %> -

- <%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:underline transition" %> -

+ <%# switch component to switch from signin to signup, let it show active tab depending on page %> + <% if controller_name == "sessions" || controller_name == "registrations" %> +
+ <%= render "layouts/shared/switch", active: controller_name %> +
<% end %> diff --git a/app/views/layouts/shared/_switch.html.erb b/app/views/layouts/shared/_switch.html.erb new file mode 100644 index 00000000..1f6ab643 --- /dev/null +++ b/app/views/layouts/shared/_switch.html.erb @@ -0,0 +1,12 @@ +<%# Switch component to toggle between sign-in and sign-up tabs %> +
+ <%= 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 %> +
\ No newline at end of file diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb index 0449c4e2..ec00ac8e 100644 --- a/app/views/registrations/new.html.erb +++ b/app/views/registrations/new.html.erb @@ -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 %> + +
+ <%= form.password_field :password, + autocomplete: "new-password", + required: "required", + label: true, + maxlength: 72, + data: { action: "input->password#validateCriteria" } %> + +
+ <%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %> +
+ + +
+
+
+
+
+
+ + +
+
+ <%= 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" } %> + Minimum 8 characters +
+
+ <%= 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" } %> + Upper and lowercase letters +
+
+ <%= 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" } %> + A number (0-9) +
+
+ <%= 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" } %> + A special character (!, @, #, $, %, etc) +
+
+
+ <% if invite_code_required? && !@invitation %> <%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %> <% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 2f5228e4..432ddfb8 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -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| %> diff --git a/config/locales/views/layout/en.yml b/config/locales/views/layout/en.yml index b5484b6a..6c1eafab 100644 --- a/config/locales/views/layout/en.yml +++ b/config/locales/views/layout/en.yml @@ -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: diff --git a/config/locales/views/registrations/en.yml b/config/locales/views/registrations/en.yml index d79c10ff..d567ce1d 100644 --- a/config/locales/views/registrations/en.yml +++ b/config/locales/views/registrations/en.yml @@ -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! diff --git a/config/locales/views/sessions/en.yml b/config/locales/views/sessions/en.yml index 8c0a610d..2398a57a 100644 --- a/config/locales/views/sessions/en.yml +++ b/config/locales/views/sessions/en.yml @@ -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