Skip to content

Commit caf9603

Browse files
authored
Merge pull request #7212 from escattone/new-edit-avatar-2755
new edit avatar page
2 parents 8c5d17a + 49ce4b6 commit caf9603

4 files changed

Lines changed: 324 additions & 15 deletions

File tree

kitsune/groups/forms.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ class GroupAvatarForm(forms.ModelForm):
2626
def __init__(self, *args, **kwargs):
2727
self.request = kwargs.pop("request", None)
2828
super().__init__(*args, **kwargs)
29-
self.fields["avatar"].help_text = _("Your avatar will be resized to {size}x{size}").format(
30-
size=settings.AVATAR_SIZE
31-
)
29+
self.fields["avatar"].help_text = _(
30+
"Upload an image file (JPG or PNG). Maximum file size: {size}MB"
31+
).format(size=settings.MAX_AVATAR_FILE_SIZE // (1024 * 1024))
3232

3333
class Meta:
3434
model = GroupProfile
Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,84 @@
11
{% extends "groups/base.html" %}
22
{% set title = _('Change {group} group avatar')|f(group=profile.group.name) %}
33

4+
{# Build breadcrumb trail #}
5+
{% set crumbs = [(url('groups.list'), _('Groups')), (url('groups.profile', profile.slug), profile.group.name), (None, _('Change Avatar'))] %}
6+
47
{% block content %}
5-
<article id="change-avatar" class="main">
6-
<h1>{{ title }}</h1>
7-
<form method="post" action="" enctype="multipart/form-data">
8-
{% csrf_token %}
9-
<ul>
10-
{{ form.as_ul()|safe }}
11-
</ul>
12-
<div class="submit">
13-
<input type="submit" value="{{ pgettext('avatar', 'Upload') }}" />
14-
<a href="{{ url('groups.profile', profile.slug) }}">{{ _('Cancel') }}</a>
15-
</div>
16-
</form>
8+
<article id="change-avatar">
9+
<div class="avatar-edit-container">
10+
<header class="avatar-edit-header">
11+
<h1 class="sumo-page-heading">{{ title }}</h1>
12+
<p class="avatar-edit-description">{{ _('Upload a new avatar for your group. Supported formats: JPG, PNG') }}</p>
13+
</header>
14+
15+
<form method="post" action="" enctype="multipart/form-data" id="avatar-form">
16+
{% csrf_token %}
17+
18+
<div class="avatar-edit-content">
19+
<div class="avatar-preview-section">
20+
<h2 class="section-title">{{ _('Preview') }}</h2>
21+
<div class="avatar-preview-wrapper">
22+
<img id="avatar-preview"
23+
src="{% if profile.avatar %}{{ profile.avatar.url }}{% else %}{{ webpack_static('sumo/img/default-FFA-avatar.png') }}{% endif %}"
24+
alt="{{ _('Avatar preview') }}"
25+
class="avatar-preview-image" />
26+
<div id="avatar-overlay" class="avatar-overlay hidden">
27+
<span>{{ _('Preview') }}</span>
28+
</div>
29+
</div>
30+
<p class="avatar-preview-help">{{ _('This is how your avatar will appear') }}</p>
31+
</div>
32+
33+
<div class="avatar-upload-section">
34+
<h2 class="section-title">{{ _('Upload New Avatar') }}</h2>
35+
36+
{% if form.errors %}
37+
<div class="form-errors">
38+
{% for field in form %}
39+
{% for error in field.errors %}
40+
<p class="error-message">{{ error }}</p>
41+
{% endfor %}
42+
{% endfor %}
43+
{% for error in form.non_field_errors() %}
44+
<p class="error-message">{{ error }}</p>
45+
{% endfor %}
46+
</div>
47+
{% endif %}
48+
49+
<div class="form-field">
50+
<div class="file-input-wrapper">
51+
{{ form.avatar }}
52+
<label for="id_avatar" class="file-input-label sumo-button secondary-button">
53+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
54+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
55+
<polyline points="17 8 12 3 7 8"></polyline>
56+
<line x1="12" y1="3" x2="12" y2="15"></line>
57+
</svg>
58+
{{ _('Choose File') }}
59+
</label>
60+
<span id="file-name" class="file-name">{{ _('No file chosen') }}</span>
61+
</div>
62+
63+
{% if form.avatar.help_text %}
64+
<p class="form-help-text">{{ form.avatar.help_text }}</p>
65+
{% endif %}
66+
</div>
67+
68+
<div class="form-actions">
69+
<button type="submit" class="sumo-button primary-button">
70+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
71+
<polyline points="20 6 9 17 4 12"></polyline>
72+
</svg>
73+
{{ pgettext('avatar', 'Upload') }}
74+
</button>
75+
<a href="{{ url('groups.profile', profile.slug) }}" class="sumo-button secondary-button">
76+
{{ _('Cancel') }}
77+
</a>
78+
</div>
79+
</div>
80+
</div>
81+
</form>
82+
</div>
1783
</article>
1884
{% endblock %}

kitsune/sumo/static/sumo/js/groups.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,44 @@ import "sumo/js/libs/jquery.lazyload";
1818

1919
// Initialize lazy loading for images in group information
2020
$("img.lazy").lazyload();
21+
22+
// Initialize avatar preview functionality
23+
initAvatarPreview();
24+
}
25+
26+
function initAvatarPreview() {
27+
const fileInput = document.getElementById('id_avatar');
28+
const fileName = document.getElementById('file-name');
29+
const preview = document.getElementById('avatar-preview');
30+
const overlay = document.getElementById('avatar-overlay');
31+
32+
if (fileInput && fileName && preview) {
33+
fileInput.addEventListener('change', function(e) {
34+
const file = e.target.files[0];
35+
36+
if (file) {
37+
// Update file name display
38+
fileName.textContent = file.name;
39+
40+
// Show preview
41+
const reader = new FileReader();
42+
reader.onload = function(e) {
43+
preview.src = e.target.result;
44+
if (overlay) {
45+
overlay.classList.remove('hidden');
46+
47+
// Hide overlay after animation
48+
setTimeout(function() {
49+
overlay.classList.add('hidden');
50+
}, 1500);
51+
}
52+
};
53+
reader.readAsDataURL(file);
54+
} else {
55+
fileName.textContent = gettext('No file chosen');
56+
}
57+
});
58+
}
2159
}
2260

2361
function initGroupsTree() {

kitsune/sumo/static/sumo/scss/components/_groups.scss

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,211 @@
543543
}
544544
}
545545

546+
// Avatar edit page
547+
#change-avatar {
548+
.avatar-edit-container {
549+
max-width: 900px;
550+
margin: 0 auto;
551+
}
552+
553+
.avatar-edit-header {
554+
text-align: center;
555+
margin-bottom: p.$spacing-2xl;
556+
padding-bottom: p.$spacing-lg;
557+
border-bottom: 2px solid var(--color-marketing-gray-03);
558+
559+
.sumo-page-heading {
560+
margin-bottom: p.$spacing-sm;
561+
}
562+
563+
.avatar-edit-description {
564+
@include c.text-body-md;
565+
color: var(--color-marketing-gray-07);
566+
margin: 0;
567+
}
568+
}
569+
570+
.avatar-edit-content {
571+
display: grid;
572+
grid-template-columns: 1fr;
573+
gap: p.$spacing-2xl;
574+
575+
@media #{p.$mq-md} {
576+
grid-template-columns: 1fr 1.5fr;
577+
}
578+
}
579+
580+
.avatar-preview-section {
581+
.section-title {
582+
@include c.text-body-lg;
583+
font-weight: 700;
584+
margin-bottom: p.$spacing-lg;
585+
color: var(--color-ink-09);
586+
text-align: center;
587+
}
588+
589+
.avatar-preview-wrapper {
590+
position: relative;
591+
width: 200px;
592+
height: 200px;
593+
margin: 0 auto p.$spacing-md;
594+
border-radius: 50%;
595+
overflow: hidden;
596+
border: 4px solid var(--color-marketing-gray-03);
597+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
598+
}
599+
600+
.avatar-preview-image {
601+
width: 100%;
602+
height: 100%;
603+
object-fit: cover;
604+
transition: transform 0.3s ease;
605+
}
606+
607+
.avatar-overlay {
608+
position: absolute;
609+
top: 0;
610+
left: 0;
611+
right: 0;
612+
bottom: 0;
613+
background: rgba(0, 0, 0, 0.7);
614+
display: flex;
615+
align-items: center;
616+
justify-content: center;
617+
color: white;
618+
font-weight: 700;
619+
font-size: 18px;
620+
opacity: 1;
621+
transition: opacity 0.5s ease;
622+
623+
&.hidden {
624+
opacity: 0;
625+
pointer-events: none;
626+
}
627+
}
628+
629+
.avatar-preview-help {
630+
@include c.text-body-sm;
631+
text-align: center;
632+
color: var(--color-marketing-gray-06);
633+
margin: 0;
634+
}
635+
}
636+
637+
.avatar-upload-section {
638+
text-align: center;
639+
640+
.section-title {
641+
@include c.text-body-lg;
642+
font-weight: 700;
643+
margin-bottom: p.$spacing-lg;
644+
color: var(--color-ink-09);
645+
}
646+
647+
.form-errors {
648+
background: var(--color-red-01);
649+
border-left: 4px solid var(--color-red-60);
650+
padding: p.$spacing-md;
651+
margin-bottom: p.$spacing-lg;
652+
border-radius: p.$border-radius-sm;
653+
654+
.error-message {
655+
color: var(--color-red-70);
656+
margin: 0;
657+
@include c.text-body-sm;
658+
659+
& + .error-message {
660+
margin-top: p.$spacing-xs;
661+
}
662+
}
663+
}
664+
665+
.form-field {
666+
margin-bottom: p.$spacing-xl;
667+
}
668+
669+
.form-label {
670+
display: block;
671+
@include c.text-body-md;
672+
font-weight: 600;
673+
margin-bottom: p.$spacing-sm;
674+
color: var(--color-ink-09);
675+
676+
.required {
677+
color: var(--color-red-60);
678+
margin-left: 2px;
679+
}
680+
}
681+
682+
.file-input-wrapper {
683+
position: relative;
684+
685+
input[type="file"] {
686+
position: absolute;
687+
left: -9999px;
688+
opacity: 0;
689+
}
690+
691+
// Hide the widget's built-in image preview since we have our own
692+
.val-wrap img {
693+
display: none;
694+
}
695+
696+
.file-input-label {
697+
display: inline-flex;
698+
align-items: center;
699+
gap: p.$spacing-sm;
700+
cursor: pointer;
701+
margin-bottom: p.$spacing-sm;
702+
703+
svg {
704+
flex-shrink: 0;
705+
}
706+
}
707+
708+
.file-name {
709+
display: block;
710+
@include c.text-body-sm;
711+
color: var(--color-marketing-gray-07);
712+
padding: p.$spacing-sm p.$spacing-md;
713+
background: var(--color-marketing-gray-01);
714+
border-radius: p.$border-radius-sm;
715+
border: 1px solid var(--color-marketing-gray-03);
716+
}
717+
}
718+
719+
.form-help-text {
720+
@include c.text-body-sm;
721+
color: var(--color-marketing-gray-06);
722+
margin-top: p.$spacing-sm;
723+
margin-bottom: 0;
724+
}
725+
726+
.form-actions {
727+
display: flex;
728+
gap: p.$spacing-md;
729+
align-items: center;
730+
justify-content: center;
731+
flex-wrap: wrap;
732+
733+
button,
734+
a {
735+
white-space: nowrap;
736+
}
737+
738+
button[type="submit"] {
739+
display: inline-flex;
740+
align-items: center;
741+
gap: p.$spacing-xs;
742+
743+
svg {
744+
flex-shrink: 0;
745+
}
746+
}
747+
}
748+
}
749+
}
750+
546751
// Groups list page
547752
#group-list {
548753
.group-tree {

0 commit comments

Comments
 (0)