diff --git a/flask_setup/routes.py b/flask_setup/routes.py index 8d179fab..5295923c 100644 --- a/flask_setup/routes.py +++ b/flask_setup/routes.py @@ -100,6 +100,20 @@ def _register_auth_routes(app: Flask, adapters: dict) -> None: endpoint="auth.remove_profile_photo", ) + app.add_url_rule( + "/profile/email", + view_func=acc.update_email, + methods=["POST"], + endpoint="auth.update_email", + ) + + app.add_url_rule( + "/profile/password", + view_func=acc.update_password, + methods=["POST"], + endpoint="auth.update_password", + ) + app.add_url_rule( "/admin/users", view_func=acc.list_all_users, @@ -107,6 +121,20 @@ def _register_auth_routes(app: Flask, adapters: dict) -> None: endpoint="auth.list_all_users", ) + app.add_url_rule( + "/account/delete", + view_func=acc.delete_account, + methods=["POST"], + endpoint="auth.delete_account", + ) + + app.add_url_rule( + "/admin/users//role", + view_func=acc.change_role, + methods=["POST"], + endpoint="auth.change_role", + ) + def _register_file_routes(app: Flask, adapters: dict) -> None: fad = adapters["file_adapter"] diff --git a/frontend/static/css/article.css b/frontend/static/css/article.css index 37f06767..e7473232 100644 --- a/frontend/static/css/article.css +++ b/frontend/static/css/article.css @@ -101,7 +101,7 @@ margin-right: var(--spacing-xs); } -.meta-author:hover { +a.meta-author:hover { color: var(--primary); text-decoration: underline; } @@ -295,47 +295,6 @@ input[type=number] { appearance: textfield; } -/* Shared dialog (modal) styles */ -.dialog { - border: 1px solid var(--outline-variant); - border-radius: var(--radius-lg); - background: var(--surface-container-high); - color: var(--on-surface); - padding: var(--spacing-xl); - width: 90%; - max-width: 25rem; - box-shadow: 0 20px 40px color-mix(in srgb, var(--on-surface) 10%, transparent); - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: var(--z-modal); -} - -.dialog-sm { - max-width: 20rem; -} - -.dialog::backdrop { - background: color-mix(in srgb, var(--surface-container-lowest) 80%, transparent); - backdrop-filter: blur(8px); -} - -.dialog .modal-footer { - margin-top: var(--spacing-xl); -} - -.modal-header { - margin-bottom: var(--spacing-md); -} - -.modal-title { - font-family: var(--font-primary); - font-size: var(--font-headline-md); - font-weight: 700; - margin: 0; -} - .modal-body { margin-bottom: var(--spacing-xl); } @@ -561,10 +520,6 @@ input[type=number] { display: block; } -.comment-depth-3 > .comment-content > .comment-replies { - display: block; -} - .comment-actions { display: flex; gap: var(--spacing-sm); @@ -592,7 +547,7 @@ input[type=number] { text-decoration: none; } -.comment-author:hover { +a.comment-author:hover { color: var(--primary); text-decoration: underline; } @@ -675,10 +630,6 @@ input[type=number] { margin-top: var(--spacing-sm); } -.comment-depth-3 .comment-replies > .comment-depth-3 > .comment-avatar { - display: none; -} - @media (max-width: 30em) { .comment-avatar { width: 2rem; diff --git a/frontend/static/css/base.css b/frontend/static/css/base.css index 060474ec..bf147118 100644 --- a/frontend/static/css/base.css +++ b/frontend/static/css/base.css @@ -509,6 +509,53 @@ button:hover, filter: brightness(1.15); } +#confirm-message { + text-align: justify; +} + +.dialog { + border: 1px solid var(--outline-variant); + border-radius: var(--radius-lg); + background: var(--surface-container-high); + color: var(--on-surface); + padding: var(--spacing-xl); + width: 90%; + max-width: 25rem; + box-shadow: 0 20px 40px color-mix(in srgb, var(--on-surface) 10%, transparent); + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: var(--z-modal); +} + +.dialog-sm { + max-width: 20rem; +} + +.dialog::backdrop { + background: color-mix(in srgb, var(--surface-container-lowest) 80%, transparent); + backdrop-filter: blur(8px); +} + +.dialog .modal-footer { + display: flex; + justify-content: flex-end; + gap: var(--spacing-md); + margin-top: var(--spacing-xl); +} + +.modal-header { + margin-bottom: var(--spacing-md); +} + +.modal-title { + font-family: var(--font-primary); + font-size: var(--font-headline-md); + font-weight: 700; + margin: 0; +} + button:focus-visible, .btn:focus-visible, a:focus-visible { diff --git a/frontend/static/css/profile.css b/frontend/static/css/profile.css index 626e1d54..de4d23c5 100644 --- a/frontend/static/css/profile.css +++ b/frontend/static/css/profile.css @@ -94,15 +94,89 @@ margin-top: var(--spacing-lg); } +.profile-actions .btn-secondary, +.profile-actions .btn-danger { + padding: var(--spacing-xs) var(--spacing-md); + font-size: var(--font-body-base); +} + + +.info-right-group { + display: flex; + align-items: center; + gap: var(--spacing-sm); + flex-wrap: wrap; + justify-content: flex-end; +} + +.email-edit-btn { + background: none; + border: none; + color: var(--primary); + cursor: pointer; + font-size: var(--font-body-xs); + font-family: var(--font-mono); + text-transform: uppercase; + padding: 0; +} + +.email-edit-btn:hover { + background: color-mix(in srgb, var(--primary) 10%, transparent); + border-radius: var(--radius-xs); + filter: brightness(1.1); +} + @media (max-width: 22.5em) { + .profile-card { + padding: var(--spacing-md); + } + + .info-row { + gap: var(--spacing-xs); + } + + .info-right-group { + flex-wrap: nowrap; + min-width: 0; + } + + .info-value { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + max-width: 50%; + } + + .email-edit-btn { + flex-shrink: 0; + } + .profile-actions { flex-direction: column; } - .profile-actions .btn-secondary { + .profile-actions .btn-secondary, + .profile-actions .btn-danger { width: 100%; } } +.modal-body { + margin-bottom: var(--spacing-xl); +} + +.modal-input-group { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.modal-input-group label { + font-size: var(--font-body-base); + color: var(--on-surface-variant); + font-family: var(--font-mono); +} + .back-link { display: flex; align-items: center; @@ -130,3 +204,72 @@ .back-link span:last-child { transform: translateY(1.5px); } + +.password-wrapper { + position: relative; +} + +.password-toggle { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + color: var(--on-surface-variant); + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-xs); + line-height: 1; +} + +.password-toggle .icon { + font-size: 1.5rem; +} + +.password-toggle:hover { + color: var(--on-surface); +} + +.password-toggle:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +.role-row { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-sm); +} + +.role-inline-form { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); +} + +.role-inline-form select { + font-family: var(--font-mono); + font-size: var(--font-body-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: var(--spacing-xs) var(--spacing-sm); + border: 1px solid var(--outline-variant); + border-radius: var(--radius-default); + background: var(--surface); + color: var(--on-surface); + cursor: pointer; +} + +.role-inline-form select:hover { + border-color: var(--outline); +} + +.role-inline-form .btn-sm { + font-size: var(--font-body-sm); + padding: var(--spacing-xs) var(--spacing-sm); +} diff --git a/frontend/static/css/user-list.css b/frontend/static/css/user-list.css index 38ad49a3..79208dba 100644 --- a/frontend/static/css/user-list.css +++ b/frontend/static/css/user-list.css @@ -97,3 +97,54 @@ .user-card .back-link span:last-child { transform: translateY(1.5px); } + +.user-table .btn-danger { + font-size: var(--font-body-sm); + padding: var(--spacing-xs) var(--spacing-sm); + white-space: nowrap; +} + +.user-table-form { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + margin: 0; +} + +.user-table td:last-child { + white-space: nowrap; +} + +.user-table-actions { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.user-table-na { + color: var(--on-surface-variant); + font-size: var(--font-body-sm); +} + +.user-table-form select { + font-family: var(--font-mono); + font-size: var(--font-body-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: var(--spacing-xs) var(--spacing-sm); + border: 1px solid var(--outline-variant); + border-radius: var(--radius-default); + background: var(--surface); + color: var(--on-surface); + cursor: pointer; +} + +.user-table-form select:hover { + border-color: var(--outline); +} + +.user-table-form .btn-sm { + font-size: var(--font-body-sm); + padding: var(--spacing-xs) var(--spacing-sm); +} diff --git a/frontend/static/scripts/profile-email.js b/frontend/static/scripts/profile-email.js new file mode 100644 index 00000000..379ad3e6 --- /dev/null +++ b/frontend/static/scripts/profile-email.js @@ -0,0 +1,20 @@ +'use strict'; +(() => { + const MODAL_ID = 'email-modal'; + const OPEN_BTN_ID = 'open-email-modal'; + const CANCEL_BTN_ID = 'email-modal-cancel'; + + document.addEventListener('DOMContentLoaded', () => { + const dialog = document.getElementById(MODAL_ID); + const openBtn = document.getElementById(OPEN_BTN_ID); + const cancelBtn = document.getElementById(CANCEL_BTN_ID); + if (!dialog || !openBtn || !cancelBtn) return; + + openBtn.addEventListener('click', () => dialog.showModal()); + cancelBtn.addEventListener('click', () => dialog.close()); + + dialog.addEventListener('click', (e) => { + if (e.target === dialog) dialog.close(); + }); + }); +})(); diff --git a/frontend/static/scripts/profile-password.js b/frontend/static/scripts/profile-password.js new file mode 100644 index 00000000..8544c3a4 --- /dev/null +++ b/frontend/static/scripts/profile-password.js @@ -0,0 +1,20 @@ +'use strict'; +(() => { + const MODAL_ID = 'password-modal'; + const OPEN_BTN_ID = 'open-password-modal'; + const CANCEL_BTN_ID = 'password-modal-cancel'; + + document.addEventListener('DOMContentLoaded', () => { + const dialog = document.getElementById(MODAL_ID); + const openBtn = document.getElementById(OPEN_BTN_ID); + const cancelBtn = document.getElementById(CANCEL_BTN_ID); + if (!dialog || !openBtn || !cancelBtn) return; + + openBtn.addEventListener('click', () => dialog.showModal()); + cancelBtn.addEventListener('click', () => dialog.close()); + + dialog.addEventListener('click', (e) => { + if (e.target === dialog) dialog.close(); + }); + }); +})(); diff --git a/frontend/templates/article_detail.html b/frontend/templates/article_detail.html index dcee9ed6..68865223 100644 --- a/frontend/templates/article_detail.html +++ b/frontend/templates/article_detail.html @@ -48,18 +48,22 @@

{{ article.article_title }}

+ {% if article.author_username == 'Anonymous' %} + {{ article.author_username }} + {% else %} {% if article.author_avatar_file_id %} {{ article.author_username }} {% endif %} {{ article.author_username }} + {% endif %}
- {% if current_user and current_user.account_id == article.article_author_id %} + {% if current_user and (current_user.account_id == article.article_author_id or current_user.account_role == 'admin') %} Edit Delete + {% if is_own_profile %} + + +
+ + +
+
+ + + +
+ + +
+
+ {% endif %} {% if is_own_profile %}
{% if current_user.account_role == 'admin' %} Manage Users {% endif %} +
+ {% if current_user.account_role != 'admin' %} +
+ + +
+ {% endif %} +
+ {% endif %} + + {% if current_user and current_user.account_role == 'admin' and not is_own_profile and user.account_role != 'admin' %} +
+
+ + + +
{% endif %} @@ -77,4 +160,7 @@

{{ user.account_username }}

{% block extra_js %} + + + {% endblock %} diff --git a/frontend/templates/registration.html b/frontend/templates/registration.html index e30a3c47..e2ddaace 100644 --- a/frontend/templates/registration.html +++ b/frontend/templates/registration.html @@ -18,23 +18,23 @@

Join DevJournal

- +
- +
- +
@@ -44,7 +44,7 @@

Join DevJournal