Skip to content

Commit 54c6b38

Browse files
authored
Avatar preview widget (vas3k#248)
* add prettier config file * add vue component for avatar input preview
1 parent 5e0dc80 commit 54c6b38

File tree

4 files changed

+112
-3
lines changed

4 files changed

+112
-3
lines changed

.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"tabWidth": 4,
3+
"useTabs": true
4+
}

frontend/html/users/edit/profile.html

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
<label for="{{ form.avatar.id_for_label }}" class="form-label">
6363
Обновить аватар
6464
</label>
65+
<user-avatar-input
66+
input-id="{{ form.avatar.id_for_label }}"
67+
current-avatar="{{ user.get_avatar }}"
68+
></user-avatar-input>
6569
{{ form.avatar }}
6670
{% if form.avatar.errors %}<span class="form-row-errors">{{ form.avatar.errors }}</span>{% endif %}
6771
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<template>
2+
<label
3+
class="user-avatar-input"
4+
:for="inputId"
5+
>
6+
<div
7+
class="avatar profile-user-avatar"
8+
:style="avatarStyle"
9+
/>
10+
</label>
11+
</template>
12+
<script>
13+
const FILE_READER_STATE = {
14+
EMPTY: 0,
15+
LOADING: 1,
16+
DONE: 2,
17+
};
18+
19+
export default {
20+
name: "UserAvatarInput",
21+
props: {
22+
// To allow "click-to-activate" on avatar
23+
inputId: {
24+
type: String,
25+
required: true,
26+
},
27+
// For preview
28+
currentAvatar: {
29+
type: String,
30+
required: false,
31+
},
32+
},
33+
data() {
34+
return {
35+
inputElement: null,
36+
imageData: null,
37+
fileReader: null,
38+
error: null,
39+
};
40+
},
41+
methods: {
42+
subscribeToInput(ele) {
43+
ele.addEventListener("change", (e) => {
44+
let file = e.target.files[0];
45+
if (file) {
46+
this.handleFile(file);
47+
} else {
48+
this.imageData = null;
49+
this.fileReader = null;
50+
this.error = null;
51+
}
52+
});
53+
},
54+
async handleFile(file) {
55+
if (this.fileReader) {
56+
this.fileReader.abort();
57+
// reset state
58+
this.imageData = null;
59+
this.error = null;
60+
this.progress = 0;
61+
}
62+
let fileReader = new FileReader();
63+
fileReader.readAsDataURL(file);
64+
65+
fileReader.addEventListener("loadend", (ev) => {
66+
this.imageData = fileReader.result;
67+
});
68+
fileReader.addEventListener("error", (ev) => {
69+
this.error = fileReader.error;
70+
});
71+
},
72+
init() {
73+
// Find related input in DOM
74+
this.inputElement = document.querySelector("#" + this.inputId);
75+
76+
// Assign listeners
77+
if (this.inputElement) {
78+
this.subscribeToInput(this.inputElement);
79+
}
80+
},
81+
},
82+
computed: {
83+
avatarStyle() {
84+
if (!this.imageData && !this.currentAvatar) {
85+
return null;
86+
}
87+
return `background-image: url(${this.imageData || this.currentAvatar})`;
88+
},
89+
},
90+
mounted() {
91+
// Vue recreates elements, so using mounted hook instead of 'created'.
92+
this.init();
93+
},
94+
};
95+
</script>
96+
<style scoped>
97+
.user-avatar-input {
98+
display: inline-block;
99+
}
100+
</style>

frontend/static/js/main.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ import CommentUpvote from "./components/CommentUpvote.vue";
99
import UserTag from "./components/UserTag.vue";
1010
import PeopleMap from "./components/PeopleMap.vue";
1111
import UserExpertiseWindow from "./components/UserExpertiseWindow.vue";
12+
import UserAvatarInput from "./components/UserAvatarInput.vue";
1213
import ClubApi from "./common/api.service";
13-
1414
Vue.component("post-upvote", PostUpvote);
1515
Vue.component("post-subscription", PostSubscription);
1616
Vue.component("comment-upvote", CommentUpvote);
1717
Vue.component("user-expertise-window", UserExpertiseWindow);
1818
Vue.component("user-tag", UserTag);
1919
Vue.component("people-map", PeopleMap);
20+
Vue.component("user-avatar-input", UserAvatarInput);
2021

2122
// Since our pages have user-generated content, any fool can insert "{{" on the page and break it.
2223
// We have no other choice but to completely turn off template matching and leave it on only for components.
23-
const noDelimiter = {replace: function(){}};
24+
const noDelimiter = { replace: function () {} };
2425

2526
new Vue({
2627
el: "#app",
@@ -60,7 +61,7 @@ new Vue({
6061

6162
// Define helper function
6263
function appendMarkdownTextareaValue(textarea, value) {
63-
textarea.focus(); // on mobile
64+
textarea.focus(); // on mobile
6465
textarea.value = value;
6566
const codeMirrorEditor = textarea.nextElementSibling.CodeMirror;
6667
codeMirrorEditor.setValue(codeMirrorEditor.getValue() + value);

0 commit comments

Comments
 (0)