Skip to content

Commit 6a562f2

Browse files
committed
feat: create user authentication and profile views
- User profile and edit views with Bootstrap styling - Devise authentication forms (login, registration, password reset) - Home page with role-based content display - PWA manifest and service worker files - Responsive design with form validation and error handling
1 parent 04cc1d6 commit 6a562f2

19 files changed

+910
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<h2>Resend confirmation instructions</h2>
2+
3+
<%= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4+
<%= f.error_notification %>
5+
<%= f.full_error :confirmation_token %>
6+
7+
<div class="form-inputs">
8+
<%= f.input :email,
9+
required: true,
10+
autofocus: true,
11+
value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
12+
input_html: { autocomplete: "email" } %>
13+
</div>
14+
15+
<div class="form-actions">
16+
<%= f.button :submit, "Resend confirmation instructions" %>
17+
</div>
18+
<% end %>
19+
20+
<%= render "devise/shared/links" %>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<p>Welcome <%= @email %>!</p>
2+
3+
<p>You can confirm your account email through the link below:</p>
4+
5+
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<p>Hello <%= @email %>!</p>
2+
3+
<% if @resource.try(:unconfirmed_email?) %>
4+
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
5+
<% else %>
6+
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
7+
<% end %>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<p>Hello <%= @resource.email %>!</p>
2+
3+
<p>We're contacting you to notify you that your password has been changed.</p>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<p>Hello <%= @resource.email %>!</p>
2+
3+
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
4+
5+
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
6+
7+
<p>If you didn't request this, please ignore this email.</p>
8+
<p>Your password won't change until you access the link above and create a new one.</p>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<p>Hello <%= @resource.email %>!</p>
2+
3+
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
4+
5+
<p>Click the link below to unlock your account:</p>
6+
7+
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<h2>Change your password</h2>
2+
3+
<%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
4+
<%= f.error_notification %>
5+
6+
<%= f.input :reset_password_token, as: :hidden %>
7+
<%= f.full_error :reset_password_token %>
8+
9+
<div class="form-inputs">
10+
<%= f.input :password,
11+
label: "New password",
12+
required: true,
13+
autofocus: true,
14+
hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length),
15+
input_html: { autocomplete: "new-password" } %>
16+
<%= f.input :password_confirmation,
17+
label: "Confirm your new password",
18+
required: true,
19+
input_html: { autocomplete: "new-password" } %>
20+
</div>
21+
22+
<div class="form-actions">
23+
<%= f.button :submit, "Change my password" %>
24+
</div>
25+
<% end %>
26+
27+
<%= render "devise/shared/links" %>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<h2>Forgot your password?</h2>
2+
3+
<%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
4+
<%= f.error_notification %>
5+
6+
<div class="form-inputs">
7+
<%= f.input :email,
8+
required: true,
9+
autofocus: true,
10+
input_html: { autocomplete: "email" } %>
11+
</div>
12+
13+
<div class="form-actions">
14+
<%= f.button :submit, "Send me reset password instructions" %>
15+
</div>
16+
<% end %>
17+
18+
<%= render "devise/shared/links" %>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<h2>Edit <%= resource_name.to_s.humanize %></h2>
2+
3+
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
4+
<%= f.error_notification %>
5+
6+
<div class="form-inputs">
7+
<%= f.input :email, required: true, autofocus: true %>
8+
9+
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
10+
<p>Currently waiting confirmation for: <%= resource.unconfirmed_email %></p>
11+
<% end %>
12+
13+
<%= f.input :password,
14+
hint: "leave it blank if you don't want to change it",
15+
required: false,
16+
input_html: { autocomplete: "new-password" } %>
17+
<%= f.input :password_confirmation,
18+
required: false,
19+
input_html: { autocomplete: "new-password" } %>
20+
<%= f.input :current_password,
21+
hint: "we need your current password to confirm your changes",
22+
required: true,
23+
input_html: { autocomplete: "current-password" } %>
24+
</div>
25+
26+
<div class="form-actions">
27+
<%= f.button :submit, "Update" %>
28+
</div>
29+
<% end %>
30+
31+
<h3>Cancel my account</h3>
32+
33+
<div>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div>
34+
35+
<%= link_to "Back", :back %>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<% content_for :title, "Create Account" %>
2+
3+
<div class="auth-container">
4+
<div class="container">
5+
<div class="row justify-content-center">
6+
<div class="col-md-6 col-lg-5">
7+
<div class="auth-card">
8+
<div class="auth-header">
9+
<div class="auth-icon">
10+
<i class="bi bi-person-plus"></i>
11+
</div>
12+
<h2 class="auth-title">Create Account</h2>
13+
<p class="auth-subtitle">Join our platform today</p>
14+
</div>
15+
16+
<%= form_with model: resource,
17+
as: resource_name,
18+
url: registration_path(resource_name),
19+
local: true,
20+
html: {
21+
class: "auth-form needs-validation",
22+
novalidate: true,
23+
multipart: true
24+
} do |f| %>
25+
26+
<% if resource.errors.any? %>
27+
<div class="alert alert-danger">
28+
<h6><i class="bi bi-exclamation-triangle me-2"></i>Please fix the following errors:</h6>
29+
<ul class="mb-0">
30+
<% resource.errors.full_messages.each do |message| %>
31+
<li><%= message %></li>
32+
<% end %>
33+
</ul>
34+
</div>
35+
<% end %>
36+
37+
<div class="form-group">
38+
<%= f.label :full_name, class: "form-label" %>
39+
<%= f.text_field :full_name,
40+
class: "form-control #{'is-invalid' if resource.errors[:full_name].present?}",
41+
placeholder: "Enter your full name",
42+
autofocus: true,
43+
required: true %>
44+
<% if resource.errors[:full_name].present? %>
45+
<div class="invalid-feedback">
46+
<%= resource.errors[:full_name].first %>
47+
</div>
48+
<% else %>
49+
<div class="invalid-feedback">
50+
Please enter your full name.
51+
</div>
52+
<% end %>
53+
</div>
54+
55+
<div class="form-group">
56+
<%= f.label :email, class: "form-label" %>
57+
<%= f.email_field :email,
58+
class: "form-control #{'is-invalid' if resource.errors[:email].present?}",
59+
placeholder: "Enter your email address",
60+
autocomplete: "email",
61+
required: true %>
62+
<% if resource.errors[:email].present? %>
63+
<div class="invalid-feedback">
64+
<%= resource.errors[:email].first %>
65+
</div>
66+
<% else %>
67+
<div class="invalid-feedback">
68+
Please enter a valid email address.
69+
</div>
70+
<% end %>
71+
</div>
72+
73+
<div class="form-group">
74+
<%= f.label :password, class: "form-label" %>
75+
<%= f.password_field :password,
76+
class: "form-control #{'is-invalid' if resource.errors[:password].present?}",
77+
placeholder: "Create a password",
78+
autocomplete: "new-password",
79+
required: true,
80+
minlength: (@minimum_password_length || 6) %>
81+
<% if resource.errors[:password].present? %>
82+
<div class="invalid-feedback">
83+
<%= resource.errors[:password].first %>
84+
</div>
85+
<% else %>
86+
<div class="form-text">
87+
<% if @minimum_password_length %>
88+
Minimum <%= @minimum_password_length %> characters required.
89+
<% else %>
90+
Choose a strong password.
91+
<% end %>
92+
</div>
93+
<% end %>
94+
</div>
95+
96+
<div class="form-group">
97+
<%= f.label :password_confirmation, "Confirm Password", class: "form-label" %>
98+
<%= f.password_field :password_confirmation,
99+
class: "form-control #{'is-invalid' if resource.errors[:password_confirmation].present?}",
100+
placeholder: "Confirm your password",
101+
autocomplete: "new-password",
102+
required: true %>
103+
<% if resource.errors[:password_confirmation].present? %>
104+
<div class="invalid-feedback">
105+
<%= resource.errors[:password_confirmation].first %>
106+
</div>
107+
<% else %>
108+
<div class="invalid-feedback">
109+
Please confirm your password.
110+
</div>
111+
<% end %>
112+
</div>
113+
114+
<div class="form-group">
115+
<%= f.label :avatar, "Profile Picture (Optional)", class: "form-label" %>
116+
<%= f.file_field :avatar,
117+
class: "form-control #{'is-invalid' if resource.errors[:avatar].present?}",
118+
accept: "image/*" %>
119+
<% if resource.errors[:avatar].present? %>
120+
<div class="invalid-feedback">
121+
<%= resource.errors[:avatar].first %>
122+
</div>
123+
<% else %>
124+
<div class="form-text">
125+
Upload a profile picture (JPG, PNG, or GIF). Max size: 5MB.
126+
</div>
127+
<% end %>
128+
</div>
129+
130+
<div class="d-grid">
131+
<%= f.submit "Create Account", class: "btn btn-primary btn-lg" %>
132+
</div>
133+
<% end %>
134+
135+
<div class="auth-divider">
136+
<span>or</span>
137+
</div>
138+
139+
<div class="auth-links">
140+
<%= render "devise/shared/links" %>
141+
</div>
142+
</div>
143+
</div>
144+
</div>
145+
</div>
146+
</div>
147+
148+
<script>
149+
document.addEventListener('DOMContentLoaded', function() {
150+
// Form validation
151+
const form = document.querySelector('.auth-form');
152+
const passwordField = document.querySelector('#user_password');
153+
const confirmField = document.querySelector('#user_password_confirmation');
154+
155+
if (form) {
156+
form.addEventListener('submit', function(e) {
157+
if (!form.checkValidity()) {
158+
e.preventDefault();
159+
e.stopPropagation();
160+
}
161+
form.classList.add('was-validated');
162+
});
163+
}
164+
165+
// Password confirmation validation
166+
if (passwordField && confirmField) {
167+
function validatePasswordMatch() {
168+
if (passwordField.value !== '' && confirmField.value !== '') {
169+
if (passwordField.value !== confirmField.value) {
170+
confirmField.setCustomValidity('Passwords do not match');
171+
confirmField.classList.add('is-invalid');
172+
} else {
173+
confirmField.setCustomValidity('');
174+
confirmField.classList.remove('is-invalid');
175+
}
176+
}
177+
}
178+
179+
passwordField.addEventListener('input', validatePasswordMatch);
180+
confirmField.addEventListener('input', validatePasswordMatch);
181+
}
182+
});
183+
</script>

0 commit comments

Comments
 (0)