Browse Source

added header image

master
Rob Colbert 2 years ago
parent
commit
a913e7c1be
  1. 60
      app/controllers/user.js
  2. 1
      app/models/user.js
  3. 8
      app/services/image.js
  4. 45
      app/services/user.js
  5. 7
      app/views/components/file-upload-image.pug
  6. 48
      app/views/layouts/public-profile.pug
  7. 26
      app/views/user/settings.pug
  8. BIN
      client/img/default-header.png
  9. 35
      client/js/site-app.js
  10. 31
      client/less/site/profile.less
  11. 1
      client/less/style.less
  12. 10
      config/limiter.js

60
app/controllers/user.js

@ -64,6 +64,12 @@ class UserController extends SiteController {
this.postProfilePhoto.bind(this),
);
router.post('/:userId/header-image',
limiterService.create(limiterService.config.user.postHeaderImage),
upload.single('imageFile'),
this.postHeaderImage.bind(this),
);
router.post('/:userId/settings',
limiterService.create(limiterService.config.user.postUpdateSettings),
upload.none(),
@ -96,6 +102,13 @@ class UserController extends SiteController {
checkProfileOwner,
this.deleteProfilePhoto.bind(this),
);
router.delete('/:userId/header-image',
limiterService.create(limiterService.config.user.deleteHeaderImage),
authRequired,
checkProfileOwner,
this.deleteHeaderImage.bind(this),
);
}
async populateUser (req, res, next, userId) {
@ -153,10 +166,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('profile-photo');
this.log.debug('received profile photo', { file: req.file });
await userService.updatePhoto(req.user, req.file);
displayList.showNotification(
'Profile photo updated successfully.',
'success',
@ -173,6 +183,27 @@ class UserController extends SiteController {
}
}
async postHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('header-image');
await userService.updateHeaderImage(req.user, req.file);
displayList.showNotification(
'Header image updated successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update header image', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postUpdateSettings (req, res) {
const { user: userService } = this.dtp.services;
try {
@ -219,9 +250,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('app-settings');
await userService.removePhoto(req.user);
displayList.showNotification(
'Profile photo removed successfully.',
'success',
@ -237,6 +266,27 @@ class UserController extends SiteController {
});
}
}
async deleteHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('remove-header-image');
await userService.removeHeaderImage(req.user);
displayList.showNotification(
'Header image removed successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to remove header image', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {

1
app/models/user.js

@ -34,6 +34,7 @@ const UserSchema = new Schema({
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
header: { type: Schema.ObjectId, ref: 'Image' },
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },

8
app/services/image.js

@ -35,8 +35,6 @@ class ImageService extends SiteService {
const NOW = new Date();
const { minio: minioService } = this.dtp.services;
try {
this.log.debug('processing uploaded image', { imageDefinition, file });
const sharpImage = await sharp(file.path);
const metadata = await sharpImage.metadata();
@ -72,7 +70,6 @@ class ImageService extends SiteService {
// save the Image model to the db
await image.save();
this.log.info('processed uploaded image', { ownerId, imageId, fileKey });
return image.toObject();
} catch (error) {
this.log.error('failed to process image', { error });
@ -111,7 +108,6 @@ class ImageService extends SiteService {
}
async processImageFile (owner, file, outputs, options) {
this.log.debug('processing image file', { owner, file, outputs });
const sharpImage = sharp(file.path);
return this.processImage(owner, sharpImage, outputs, options);
}
@ -133,8 +129,6 @@ class ImageService extends SiteService {
outputMetadata.width = output.width;
outputMetadata.height = output.height;
service.log.debug('processing image', { output, outputMetadata });
const image = new SiteImage();
image.created = NOW;
image.owner = owner._id;
@ -180,8 +174,6 @@ class ImageService extends SiteService {
// save the Image model to the db
await image.save();
service.log.info('processed uploaded image', { ownerId, imageId, fileKey });
if (options.removeWorkFiles) {
service.log.debug('removing work file', { path: output.filePath });
await fs.promises.unlink(output.filePath);

45
app/services/user.js

@ -270,6 +270,9 @@ class UserService {
{
path: 'picture.small',
},
{
path: 'header',
},
])
.lean();
if (!user) {
@ -313,7 +316,7 @@ class UserService {
username = username.trim().toLowerCase();
const user = await User
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture');
.select('_id created username username_lc displayName bio picture header');
return user;
}
@ -450,7 +453,6 @@ class UserService {
async updatePhoto (user, file) {
const { image: imageService } = this.dtp.services;
const images = [
{
width: 512,
@ -484,19 +486,48 @@ class UserService {
async removePhoto (user) {
const { image: imageService } = this.dtp.services;
this.log.info('remove profile photo', { user });
this.log.info('remove profile photo', { user: user._id });
user = await this.getUserAccount(user._id);
await imageService.deleteImage(user.picture.large);
await imageService.deleteImage(user.picture.small);
if (user.picture.large) {
await imageService.deleteImage(user.picture.large);
}
if (user.picture.small) {
await imageService.deleteImage(user.picture.small);
}
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
}
async updateHeaderImage (user, file) {
const { image: imageService } = this.dtp.services;
const images = [
{
width: 1400,
height: 400,
format: 'jpeg',
formatParameters: {
quality: 80,
},
},
];
await imageService.processImageFile(user, file, images);
await User.updateOne(
{ _id: user._id },
{
$unset: {
'picture': '',
$set: {
'header': images[0].image._id,
},
},
);
}
async removeHeaderImage (user) {
const { image: imageService } = this.dtp.services;
user = await this.getUserAccount(user._id);
if (user.header) {
await imageService.deleteImage(user.header);
}
await User.updateOne({ _id: user._id }, { $unset: { 'header': '' } });
}
}
module.exports = {

7
app/views/components/file-upload-image.pug

@ -8,9 +8,9 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
div(class="uk-width-1-1 uk-width-auto@m")
.upload-image-container.size-512
if !!currentImage
img(id= imageId, data-cropper-options= cropperOptions, src= `/image/${currentImage._id}`, class= imageClass).sb-large
img(id= imageId, src= `/image/${currentImage._id}`, class= imageClass).sb-large
else
img(id= imageId, data-cropper-options= cropperOptions, src= defaultImage, class= imageClass).sb-large
img(id= imageId, src= defaultImage, class= imageClass).sb-large
div(class="uk-width-1-1 uk-width-auto@m")
.uk-text-small.uk-margin(hidden= !!currentImage)
@ -28,8 +28,7 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
data-file-size-element= "file-size",
data-file-max-size= 15 * 1024000,
data-image-id= imageId,
data-image-w= 512,
data-image-h= 512,
data-cropper-options= cropperOptions,
onchange="return dtp.app.selectImageFile(event);",
)
button(type="button", tabindex="-1").uk-button.dtp-button-default Select

48
app/views/layouts/public-profile.pug

@ -1,30 +1,38 @@
extends main
block content-container
section.uk-section.uk-section-default.public-profile
.uk-container
if userProfile.header
.uk-margin.profile-header-container.header-offset
img(src= `/image/${userProfile.header._id}`).profile-header
if userProfile.picture && userProfile.picture.large
img(src= `/image/${userProfile.picture.large._id}`).profile-picture
else
img(src= "/img/icon/icon-512x512.png").profile-picture
else
.uk-margin.profile-header-container
if userProfile.picture && userProfile.picture.large
img(src= `/image/${userProfile.picture.large._id}`).profile-picture
else
img(src= "/img/icon/icon-512x512.png").profile-picture
.uk-width-xlarge.uk-margin-auto
//- if user && user.flags.isAdmin
//- .uk-margin.uk-text-center
//- a(href=`/admin/user/${userProfile._id}`).uk-button.dtp-button-danger User Admin
section.uk-section.uk-section-default
.uk-container.uk-width-xlarge
if user && user.flags.isAdmin
.uk-margin.uk-text-center
a(href=`/admin/user/${userProfile._id}`).uk-button.dtp-button-danger User Admin
if userProfile.displayName
h1.uk-margin-remove= userProfile.displayName
.uk-text-muted @#{userProfile.username}
else
h1.uk-margin-remove= userProfile.username
.uk-margin.uk-margin-auto.uk-width-small
if userProfile.picture && userProfile.picture.large
img(src= `/image/${userProfile.picture.large._id}`).responsive
else
img(src= "/img/icon/icon-512x512.png").responsive
.uk-margin.uk-text-center
if userProfile.displayName
h1.uk-margin-remove= userProfile.displayName
.uk-text-muted @#{userProfile.username}
else
h1.uk-margin-remove= userProfile.username
.uk-margin.uk-text-center
div= userProfile.bio
.uk-margin.uk-text-center
div= userProfile.bio
block content
block content
block page-footer
section.uk-section.uk-section-default.uk-section-small.dtp-site-footer

26
app/views/user/settings.pug

@ -14,11 +14,31 @@ block content
div(uk-grid)
div(class="uk-width-1-1 uk-width-1-3@m")
-
var currentImage = null;
var currentProfile = null;
if (user.picture && user.picture.large) {
currentImage = user.picture.large;
currentProfile = user.picture.large;
}
+renderFileUploadImage(`/user/${user._id}/profile-photo`, 'test-image-upload', 'profile-picture-file', 'site-profile-picture', `/img/default-member.png`, currentImage)
.uk-margin
+renderFileUploadImage(
`/user/${user._id}/header-image`,
'header-image-upload',
'header-image-file',
'header-image-picture',
`/img/default-header.png`,
user.header,
{ aspectRatio: 1400 / 400 },
)
.uk-margin
+renderFileUploadImage(
`/user/${user._id}/profile-photo`,
'profile-picture-upload',
'profile-picture-file',
'site-profile-picture',
`/img/default-member.png`,
currentProfile,
{ aspectRatio: 1 },
)
div(class="uk-width-1-1 uk-width-expand@m")
form(method="POST", action=`/user/${user._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form

BIN
client/img/default-header.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

35
client/js/site-app.js

@ -243,7 +243,10 @@ export default class DtpSiteApp extends DtpApp {
const imageId = event.target.getAttribute('data-image-id');
//z read the cropper options from the element on the page
const cropperOptions = event.target.getAttribute('data-cropper-options');
let cropperOptions = event.target.getAttribute('data-cropper-options');
if (cropperOptions) {
cropperOptions = JSON.parse(cropperOptions);
}
this.log.debug('selectImageFile', 'cropper options', { cropperOptions }); //z remove when done
const fileSelectContainerId = event.target.getAttribute('data-file-select-container');
@ -285,20 +288,11 @@ export default class DtpSiteApp extends DtpApp {
return;
}
// const IMAGE_WIDTH = parseInt(event.target.getAttribute('data-image-w'));
// const IMAGE_HEIGHT = parseInt(event.target.getAttribute('data-image-h'));
const reader = new FileReader();
reader.onload = (e) => {
const img = document.getElementById(imageId);
img.onload = (e) => {
console.log('image loaded', e, img.naturalWidth, img.naturalHeight);
// if (img.naturalWidth !== IMAGE_WIDTH || img.naturalHeight !== IMAGE_HEIGHT) {
// UIkit.modal.alert(`This image must be ${IMAGE_WIDTH}x${IMAGE_HEIGHT}`);
// img.setAttribute('hidden', '');
// img.src = '';
// return;
// }
fileSelectContainer.querySelector('#file-name').textContent = selectedFile.name;
fileSelectContainer.querySelector('#file-modified').textContent = moment(selectedFile.lastModifiedDate).fromNow();
@ -315,16 +309,15 @@ export default class DtpSiteApp extends DtpApp {
img.src = e.target.result;
//z create cropper and set options here
this.createImageCropper(img);
this.createImageCropper(img, cropperOptions);
};
// read in the file, which will trigger everything else in the event handler above.
reader.readAsDataURL(selectedFile);
}
async createImageCropper (img) {
this.log.info("createImageCropper", "Creating image cropper", { img });
this.cropper = new Cropper(img, {
async createImageCropper (img, options) {
options = Object.assign({
aspectRatio: 1,
dragMode: 'move',
autoCropArea: 0.85,
@ -332,11 +325,13 @@ export default class DtpSiteApp extends DtpApp {
guides: false,
center: false,
highlight: false,
cropBoxMovable: false,
cropBoxResizable: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
modal: true,
});
}, options);
this.log.info("createImageCropper", "Creating image cropper", { img });
this.cropper = new Cropper(img, options);
}
async attachTinyMCE (editor) {
@ -505,7 +500,11 @@ export default class DtpSiteApp extends DtpApp {
let response;
switch (imageType) {
case 'profile-picture-file':
case 'header-image-file':
response = await fetch(`/user/${this.user._id}/header-image`, { method: 'DELETE' });
break;
case 'profile-picture-file':
response = await fetch(`/user/${this.user._id}/profile-photo`, { method: 'DELETE' });
break;

31
client/less/site/profile.less

@ -0,0 +1,31 @@
.public-profile {
.profile-header-container {
position: relative;
img.profile-header {
border: solid 2px #4a4a4a;
border-radius: 16px;
}
img.profile-picture {
display: block;
width: 128px;
height: auto;
margin: 0 auto;
border: solid 2px #4a4a4a;
border-radius: 50%;
}
&.header-offset {
margin-bottom: 64px;
img.profile-picture {
position: absolute;
bottom: -48px;
left: calc(50% - 64px);
}
}
}
}

1
client/less/style.less

@ -13,6 +13,7 @@
@import "site/nav.less";
@import "site/dashboard.less";
@import "site/profile.less";
@import "site/site.less";
@import "site/site-home.less";
@import "site/form.less";

10
config/limiter.js

@ -161,6 +161,11 @@ module.exports = {
expire: ONE_MINUTE * 5,
message: 'You are updating your profile photo too quickly',
},
postHeaderImage: {
total: 5,
expire: ONE_MINUTE * 5,
message: 'You are updating your header image too quickly',
},
postUpdateSettings: {
total: 4,
expire: ONE_MINUTE,
@ -181,6 +186,11 @@ module.exports = {
expire: ONE_MINUTE * 5,
message: 'You are deleting your profile photo too quickly',
},
deleteHeaderImage: {
total: 5,
expire: ONE_MINUTE * 5,
message: 'You are deleting your header images too quickly',
},
},
welcome: {

Loading…
Cancel
Save