Gave profile page a facelift in perperation for email/password update pop-ups.

This commit is contained in:
rainbow napkin 2024-12-31 06:50:34 -05:00
parent a51152a89d
commit c32f3df3f3
16 changed files with 316 additions and 157 deletions

View file

@ -27,6 +27,7 @@ module.exports.post = async function(req, res){
const data = matchedData(req); const data = matchedData(req);
var tempResult = []; var tempResult = [];
//This is gross and quite honestly password changes should be their own endpoint anyways
//if we're not chaning the password //if we're not chaning the password
if(data.passChange == null){ if(data.passChange == null){
//go through validation errors //go through validation errors
@ -48,6 +49,7 @@ module.exports.post = async function(req, res){
const userDB = await userModel.findOne(user); const userDB = await userModel.findOne(user);
const update = {}; const update = {};
if(userDB){ if(userDB){
if(data.img){ if(data.img){
userDB.img = data.img; userDB.img = data.img;
@ -64,6 +66,15 @@ module.exports.post = async function(req, res){
update.signature = data.signature; update.signature = data.signature;
} }
if(data.pronouns != null){
userDB.pronouns = data.pronouns;
if(data.pronouns == ''){
update.pronouns = "Unset/Hidden";
}else{
update.pronouns = data.pronouns;
}
}
if(data.passChange){ if(data.passChange){
//kill active session to prevent connect-mongo from freaking out //kill active session to prevent connect-mongo from freaking out
accountUtils.killSession(req.session); accountUtils.killSession(req.session);

View file

@ -46,6 +46,7 @@ router.post('/register', accountValidator.user(),
router.post('/update', accountValidator.img(), router.post('/update', accountValidator.img(),
accountValidator.bio(), accountValidator.bio(),
accountValidator.signature(), accountValidator.signature(),
accountValidator.pronouns(),
accountValidator.pass('passChange.oldPass'), accountValidator.pass('passChange.oldPass'),
accountValidator.securePass('passChange.newPass'), accountValidator.securePass('passChange.newPass'),
accountValidator.pass('passChange.confirmPass'), updateController.post); accountValidator.pass('passChange.confirmPass'), updateController.post);

View file

@ -435,6 +435,7 @@ userSchema.methods.getProfile = function(){
tokeCount: this.getTokeCount(), tokeCount: this.getTokeCount(),
img: this.img, img: this.img,
signature: this.signature, signature: this.signature,
pronouns: this.pronouns,
bio: this.bio bio: this.bio
}; };

View file

@ -32,6 +32,8 @@ module.exports = {
img: (field = 'img') => body(field).optional().isURL({require_tld: false, require_host: false}).trim(), img: (field = 'img') => body(field).optional().isURL({require_tld: false, require_host: false}).trim(),
pronouns: (field = 'pronouns') => body(field).optional().escape().trim().isLength({min: 0, max: 15}),
signature: (field = 'signature') => body(field).optional().escape().trim().isLength({min: 1, max: 150}), signature: (field = 'signature') => body(field).optional().escape().trim().isLength({min: 1, max: 150}),
bio: (field = 'bio') => body(field).optional().escape().trim().isLength({min: 1, max: 1000}), bio: (field = 'bio') => body(field).optional().escape().trim().isLength({min: 1, max: 1000}),

View file

@ -13,9 +13,12 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %> along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<span class="profile-item" id="profile-bio"> <span class="profile-bio-span">
<p class="profile-item profile-item-label" id="profile-bio-label">Bio: <span class="profile-content" id="profile-bio-content"><%- profile.bio %></span></p> <h4 id="profile-bio-label" class="profile-item-label">Bio:</h4>
<% if(selfProfile){ %> <% if(selfProfile){ %>
<p class="profile-item-edit">(<a class="profile-item-edit" id="profile-bio-edit" href="javascript:">edit</a>)</p> <p class="profile-item interactive" id="profile-bio-content"><%- profile.bio %></p>
<% } %> <textarea class="profile-item-prompt" id="profile-bio-prompt"></textarea>
<% }else{ %>
<p class="profile-item" id="profile-bio-content"><%- profile.bio %></p>
<% } %>
</span> </span>

View file

@ -13,4 +13,6 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %> along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<p class="profile-item" id="profile-creation-date">Joined: <%- profile.date.toUTCString(); %></p> <span class="profile-item">
<p class="profile-item" id="profile-creation-date" title="<%- profile.date.toUTCString() %>">Joined: <%- profile.date.toLocaleDateString(); %></p>
</span>

View file

@ -13,7 +13,9 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %> along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<img class="profile-item" id="profile-img" src="<%- profile.img %>"> <div class="profile-item" id="profile-img">
<% if(selfProfile){ %> <img class="profile-item" id="profile-img-content" src="<%- profile.img %>">
<p class="profile-item-edit">(<a class="profile-item-edit" id="profile-img-edit" href="javascript:">edit</a>)</p> <% if(selfProfile){ %>
<% } %> <input class="profile-item-prompt" id="profile-img-prompt">
<% } %>
</div>

View file

@ -0,0 +1,33 @@
<%# Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<%# If pronons are unset and the user is viewing their own profile %>
<% if((profile.pronouns == null || profile.pronouns == "") && selfProfile){ %>
<span class="profile-item profile-item-oneliner">
<p class="profile-item profile-item-oneliner" id="profile-pronouns">Pronouns: <span class="profile-content interactive" id="profile-pronouns-content">Unset/Hidden</span></p>
<input class="profile-item-prompt" id="profile-pronouns-prompt">
</span>
<%# If pronons are set regardless of who's profile %>
<% }else if(profile.pronouns != null && profile.pronouns != ""){ %>
<span class="profile-item profile-item-oneliner">
<% if(selfProfile){ %>
<p class="profile-item profile-item-oneliner" id="profile-pronouns">Pronouns: <span class="profile-content interactive" id="profile-pronouns-content"><%- profile.pronouns %></span></p>
<input class="profile-item-prompt" id="profile-pronouns-prompt">
<% }else{ %>
<p class="profile-item profile-item-oneliner" id="profile-pronouns">Pronouns: <span class="profile-content" id="profile-pronouns-content"><%- profile.pronouns %></span></p>
<% } %>
</span>
<% } %>

View file

@ -13,15 +13,11 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %> along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<div class="account-settings" id="account-settings-div"> <div class="account-settings dynamic-container" id="account-settings-div">
<h3 class="account-settings" id="account-settings-label">Account Settings</h3> <h3 class="account-settings" id="account-settings-label">Account Settings</h3>
<span class="account-settings-password-reset" id="account-settings-password-reset-div"> <span class="account-settings" id="account-settings-buttons">
<h4 class="account-settings-password-reset" id="account-settings-password-reset-label">Password Reset:</h4> <button href="javascript:" class="account-settings positive-button" id="account-settings-update-email-button">Update Email</button>
<input class="account-settings-password-reset" id="account-settings-password-reset-old" placeholder="Current Password" type="password"> <button href="javascript:" class="account-settings positive-button" id="account-settings-change-password-button">Change Password</button>
<input class="account-settings-password-reset" id="account-settings-password-reset-new" placeholder="New Password" type="password">
<input class="account-settings-password-reset" id="account-settings-password-reset-confirm" placeholder="Confirm New Password" type="password">
</span>
<span class="account-settings" id="account-settings-delete">
<button href="javascript:" class="account-settings danger-button" id="account-settings-delete-button">Delete Account</button> <button href="javascript:" class="account-settings danger-button" id="account-settings-delete-button">Delete Account</button>
</span> </span>
</div> </div>

View file

@ -13,9 +13,11 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %> along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<span class="profile-item" id="profile-signature"> <span class="profile-item">
<p class="profile-item profile-item-label" id="profile-signature-label">Signature: <span class="profile-content" id="profile-signature-content"><%- profile.signature %></span></p>
<% if(selfProfile){ %> <% if(selfProfile){ %>
<p class="profile-item-edit">(<a class="profile-item-edit" id="profile-signature-edit" href="javascript:">edit</a>)</p> <p class="profile-item profile-item-oneliner" id="profile-signature">Signature: <span class="profile-content interactive" id="profile-signature-content"><%- profile.signature %></span></p>
<input class="profile-item-prompt" id="profile-signature-prompt">
<% }else{ %>
<p class="profile-item profile-item-oneliner" id="profile-signature">Signature: <span class="profile-content" id="profile-signature-content"><%- profile.signature %></span></p>
<% } %> <% } %>
</span> </span>

View file

@ -14,10 +14,10 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %> along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<span class="profile-item profile-toke-count"> <span class="profile-item profile-toke-count">
<p class="profile-item profile-toke-count">tokes: <%- profile.tokeCount %> </p> <p class="profile-item profile-toke-count interactive">Toke Count: (<%- profile.tokeCount %> Total) </p>
<i class="profile-item bi-caret-left-fill profile-toke-count" id="toggle-toke-list"></i> <i class="-item bi-caret-left-fill profile-toke-count" id="toggle-toke-list"></i>
</span> </span>
<div class="profile-item dynamic-container" id="profile-tokes"> <div class="profile-item nested-dynamic-container" id="profile-tokes">
<% profile.tokes.forEach((count, toke) => { %> <% profile.tokes.forEach((count, toke) => { %>
<p class="profile-item profile-toke" id='profile-tokes<%-toke%>'>!<%- toke %>: <%- count %></p> <p class="profile-item profile-toke" id='profile-tokes<%-toke%>'>!<%- toke %>: <%- count %></p>
<% }); %> <% }); %>

View file

@ -29,18 +29,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<body> <body>
<%- include('partial/navbar', {user}); %> <%- include('partial/navbar', {user}); %>
<% if(profile){ %> <% if(profile){ %>
<div class="profile" id="profile-div"> <div id="account">
<h1 class="profile-item" id="profile-username"><%- profile.user %></h1> <div class="profile dynamic-container" id="profile-div">
<%- include('partial/profile/image', {profile, selfProfile}); %> <span id="profile-info" class="profile">
<%- include('partial/profile/tokeCount', {profile, selfProfile}); %> <h1 class="profile-item" id="profile-username"><%- profile.user %></h1>
<%- include('partial/profile/signature', {profile, selfProfile}); %> <%- include('partial/profile/image', {profile, selfProfile}); %>
<%- include('partial/profile/bio', {profile, selfProfile}); %> <%- include('partial/profile/pronouns', {profile, selfProfile}); %>
<%- include('partial/profile/date', {profile, selfProfile}); %> <%- include('partial/profile/signature', {profile, selfProfile}); %>
<%- include('partial/profile/badges', {profile, selfProfile}); %> <%- include('partial/profile/tokeCount', {profile, selfProfile}); %>
<%- include('partial/profile/date', {profile, selfProfile}); %>
</span>
<span id="profile-info-aux" class="profile">
<%- include('partial/profile/bio', {profile, selfProfile}); %>
<%- include('partial/profile/badges', {profile, selfProfile}); %>
</span>
</div>
<% if(selfProfile){ %>
<%- include('partial/profile/settings', {profile, selfProfile}); %>
<% } %>
</div> </div>
<% if(selfProfile){ %>
<%- include('partial/profile/settings', {profile, selfProfile}); %>
<% } %>
<% }else if(user){ %> <% }else if(user){ %>
<h1 class="profile-item" id="profile-error-label">Profile not found!</h1> <h1 class="profile-item" id="profile-error-label">Profile not found!</h1>
<% } else {%> <% } else {%>

View file

@ -65,7 +65,7 @@ input.control-prompt{
flex: 1; flex: 1;
} }
div.dynamic-container{ div.dynamic-container, div.nested-dynamic-container{
overflow: auto; overflow: auto;
} }

View file

@ -13,53 +13,96 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
#account{
display: flex;
margin-top: 2em;
justify-content: space-evenly;
}
p.profile-item-edit{ #profile-div{
display: inline; display: flex;
padding: 1em;
flex-direction: row;
overflow-y: hidden;
}
#profile-info{
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
#profile-info-aux{
flex: 2;
margin: 0 1em;
}
#account-settings-div{
display: flex;
flex-direction: column;
padding: 1em;
}
#account-settings-buttons{
display: flex;
flex-direction: column;
justify-content: space-around;
flex: 1;
}
#profile-username{
margin: 0;
}
#profile-img{
position: relative;
}
#profile-img-content{
margin: 1em 0;
}
#profile-img-prompt{
position: absolute;
left: -2em;
right: -2em;
top: calc(50% - 1.3em);
}
.profile-toke-count{
margin: 0;
} }
span.profile-item{ span.profile-item{
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
p.profile-item-label{
display: inline;
}
span.account-settings{
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
input.account-settings-password-reset{
display: block;
}
a#account-settings-delete-button{
font-weight: bold;
}
span.profile-toke-count{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: fit-content; width: fit-content;
margin: 0.2em;
} }
p.profile-toke-count{ p.profile-item{
margin: 0; margin: 0;
} }
span.profile-item-oneliner{
text-wrap: nowrap
}
.profile-item-prompt{
display: none;
}
#profile-tokes{ #profile-tokes{
resize: vertical; resize: vertical;
max-width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
min-height: 1.5em; min-height: 1.5em;
max-height: 5.8em; max-height: 5.8em;
display: none; padding: 0.5em 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
text-wrap: nowrap;
visibility: collapse;
} }
/*Little hacky but this keeps initial max-height from fucking up resizing*/ /*Little hacky but this keeps initial max-height from fucking up resizing*/
@ -70,3 +113,22 @@ p.profile-toke-count{
.profile-toke{ .profile-toke{
margin: 0.2em 1em; margin: 0.2em 1em;
} }
#profile-bio-label{
margin-bottom: 0.2em;
}
#profile-bio-content{
width: 30VW;
}
#profile-bio-prompt{
width: 30VW;
height: 11em;
resize: vertical;
}
/* temp */
input:not([type="checkbox"]):not(.navbar-item):not(.profile-item-prompt){
display: block;
}

View file

@ -187,10 +187,23 @@ textarea{
.dynamic-container{ .dynamic-container{
background-color: var(--bg1); background-color: var(--bg1);
color: var(--accent1); color: var(--accent1);
}
.nested-dynamic-container{
border: 1px var(--accent1) solid;
background-color: var(--bg0);
color: var(--accent0);
}
.dynamic-container, .nested-dynamic-container{
box-shadow: 3px 3px 1px var(--bg1-alt0) inset; box-shadow: 3px 3px 1px var(--bg1-alt0) inset;
border-radius: 1em; border-radius: 1em;
} }
.dynamic-container a{
color: var(--accent1);
}
tr{ tr{
box-shadow: var(--accent1) 0px 1em 1px -2em, var(--accent1) 0px -1em 1px -1em; box-shadow: var(--accent1) 0px 1em 1px -2em, var(--accent1) 0px -1em 1px -1em;

View file

@ -14,104 +14,125 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Base Class class profileUpdatePrompt{
class profileEditPrompt{ constructor(field){
constructor(field, content, useTextArea = false){
this.field = field; this.field = field;
this.useTextArea = useTextArea; this.contentNode = document.querySelector(`#profile-${field}-content`);
this.content = content; this.promptNode = document.querySelector(`#profile-${field}-prompt`);
this.link = document.querySelector(`#profile-${field}-edit`);
//Bail out if something ain't right if(this.promptNode != null){
if(!this.link || !this.content){ this.setupInput();
return;
}
this.setupPrompt();
}
setupPrompt(){
if(this.link != null){
this.link.addEventListener("click", this.prompt.bind(this));
} }
} }
prompt(){ setupInput(){
//Create input element this.contentNode.addEventListener('click', this.popPrompt.bind(this));
if(this.useTextArea){ this.promptNode.addEventListener('keydown', this.closePrompt.bind(this));
this.prompt = document.createElement("textArea"); }
popPrompt(){
this.promptNode.style.display = 'inline';
this.promptNode.focus();
}
closePrompt(event){
//Check if we're finished
const fin = event.key == "Escape" || event.key == "Enter";
//IF we are
if(fin){
//Display the prompt
this.promptNode.style.display = 'none';
}
//Return whether or not we're done
return fin;
}
}
class profileUpdateTextPrompt extends profileUpdatePrompt{
constructor(field, fillPrompt = false){
//call derived constructor
super(field);
//Set fill prompt
this.fillPrompt = fillPrompt;
}
popPrompt(){
//Call derived method
super.popPrompt();
//If we're filling the prompt
if(this.fillPrompt){
//Fill the prompt content and placeholder
this.promptNode.value = this.contentNode.textContent;
this.promptNode.placeholder = this.field;
//otherwise
}else{ }else{
this.prompt = document.createElement("input"); //Just fill the placeholder
this.promptNode.placeholder = this.contentNode.textContent;
} }
//Setup properties //Hide content
this.prompt.id = `profile-${this.field}-prompt`; this.contentNode.style.display = 'none';
this.prompt.classList.add("profile-edit-prompt");
if(this.field == "img"){
this.prompt.placeholder = this.content.src;
}else{
this.prompt.placeholder = this.content.innerHTML;
}
//Setup event listener
this.prompt.addEventListener("keydown", this.update.bind(this));
//replace label
this.content.replaceWith(this.prompt);
} }
async update(event){ async closePrompt(event){
if((!event || event.key == "Enter") && this.prompt.value){ //Call derived constructor and check if we're finished, assuming we're finished...
//setup object if(super.closePrompt(event)){
var updateObj = {}; //Display
updateObj[this.field] = this.prompt.value; this.contentNode.style.display = 'inline-block';
//contact server, and collect response //If we're not cancelling
var updated_content = (await utils.ajax.updateProfile(updateObj))[this.field]; if(event.key == "Enter"){
//Create empty update object
let updateObj = {};
//Update label //Fill field
if(updated_content != null){ updateObj[this.field] = this.promptNode.value;
if(this.field == "img"){
this.content.src = updated_content; //Send er' off
}else{ const update = await utils.ajax.updateProfile(updateObj);
this.content.innerHTML = updated_content;
} //Fill content from update
this.contentNode.innerHTML = update[this.field];
} }
this.finish();
}else if(event.key == "Escape" || event.key == "Enter"){
this.finish();
} }
} }
finish(){
this.prompt.replaceWith(this.content);
}
} }
class profileTextEditPrompt extends profileEditPrompt{ class profileUpdateImagePrompt extends profileUpdatePrompt{
constructor(field, useTextArea = false){
//Get content based on field name
var content = document.querySelector(`#profile-${field}-content`);
//Derived Constructor
super(field, content, useTextArea);
}
prompt(){
super.prompt();
}
async update(event){
await super.update(event)
}
}
//Child Classes
class profileImgEditPrompt extends profileEditPrompt{
constructor(){ constructor(){
//Get content based on field name //call derived constructor
var content = document.querySelector(`#profile-img`); super('img');
//Derived constructor }
super("img", content, false);
popPrompt(){
//Call derived method
super.popPrompt();
this.promptNode.placeholder = this.contentNode.src;
}
async closePrompt(event){
//Call derived constructor
super.closePrompt(event);
//If we're not cancelling
if(event.key == "Enter"){
//Create empty update object
let updateObj = {};
//Fill field
updateObj[this.field] = this.promptNode.value;
//Send er' off
const update = await utils.ajax.updateProfile(updateObj);
//Fill content from update
this.contentNode.src = update[this.field];
}
} }
} }
@ -169,11 +190,11 @@ class tokeList{
} }
toggleTokeList(){ toggleTokeList(){
if(this.tokeList.checkVisibility()){ if(this.tokeList.style.visibility == "visible"){
this.tokeList.style.display = "none"; this.tokeList.style.visibility = "collapse";
this.tokeListToggleIcon.classList.replace("bi-caret-down-fill","bi-caret-left-fill"); this.tokeListToggleIcon.classList.replace("bi-caret-down-fill","bi-caret-left-fill");
}else{ }else{
this.tokeList.style.display = "block"; this.tokeList.style.visibility = "visible";
this.tokeListToggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill"); this.tokeListToggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill");
} }
} }
@ -183,7 +204,9 @@ class deleteAccountButton{
constructor(){ constructor(){
this.deleteLink = document.querySelector('#account-settings-delete-button'); this.deleteLink = document.querySelector('#account-settings-delete-button');
this.setupInput(); if(this.deleteLink != null){
this.setupInput();
}
} }
setupInput(){ setupInput(){
@ -218,9 +241,10 @@ class deleteAccountPopup{
} }
//Object Instantiation //Object Instantiation
new profileTextEditPrompt("signature"); const imgPrompt = new profileUpdateImagePrompt();
new profileTextEditPrompt("bio", true); const pronounsPrompt = new profileUpdateTextPrompt('pronouns');
new profileImgEditPrompt(); const signaturePrompt = new profileUpdateTextPrompt('signature');
new tokeList(); const bioPrompt = new profileUpdateTextPrompt('bio', true);
new passwordResetPrompt(); const profileTokeList = new tokeList();
new deleteAccountButton(); const accountPassResetPrompt = new passwordResetPrompt();
const accountDeleteButton = new deleteAccountButton();