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