mirror of
https://github.com/Akif9748/akf-forum.git
synced 2024-11-23 04:10:40 +03:00
Compare commits
No commits in common. "27198d31312c7a648edb1f6b218e732d63d62d70" and "16a25046658277cdb5aae13c90660fa0af9cecc5" have entirely different histories.
27198d3131
...
16a2504665
8 changed files with 20 additions and 3668 deletions
|
@ -70,7 +70,6 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
|
||||||
- give admin button, not is admin
|
- give admin button, not is admin
|
||||||
- edit user ++
|
- edit user ++
|
||||||
- rewrite main page, list new messages
|
- rewrite main page, list new messages
|
||||||
https://medium.com/@minhquocece/how-to-create-an-upload-avatar-feature-like-facebook-by-cropper-js-and-slider-879990fdce82
|
|
||||||
|
|
||||||
## Major Version History
|
## Major Version History
|
||||||
- V4: Caching
|
- V4: Caching
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "akf-forum",
|
"name": "akf-forum",
|
||||||
"version": "5.1.0",
|
"version": "5.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "akf-forum",
|
"name": "akf-forum",
|
||||||
"version": "5.1.0",
|
"version": "5.0.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "akf-forum",
|
"name": "akf-forum",
|
||||||
"version": "5.1.0",
|
"version": "5.0.0",
|
||||||
"description": "A Node.js based forum software",
|
"description": "A Node.js based forum software",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,304 +0,0 @@
|
||||||
/*!
|
|
||||||
* Cropper.js v1.5.12
|
|
||||||
* https://fengyuanchen.github.io/cropperjs
|
|
||||||
*
|
|
||||||
* Copyright 2015-present Chen Fengyuan
|
|
||||||
* Released under the MIT license
|
|
||||||
*
|
|
||||||
* Date: 2021-06-12T08:00:11.623Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
.cropper-container {
|
|
||||||
direction: ltr;
|
|
||||||
font-size: 0;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
-ms-touch-action: none;
|
|
||||||
touch-action: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-container img {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
image-orientation: 0deg;
|
|
||||||
max-height: none !important;
|
|
||||||
max-width: none !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-wrap-box,
|
|
||||||
.cropper-canvas,
|
|
||||||
.cropper-drag-box,
|
|
||||||
.cropper-crop-box,
|
|
||||||
.cropper-modal {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-wrap-box,
|
|
||||||
.cropper-canvas {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-drag-box {
|
|
||||||
background-color: #fff;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-modal {
|
|
||||||
background-color: #000;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-view-box {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
outline: 1px solid #39f;
|
|
||||||
outline-color: rgba(51, 153, 255, 0.75);
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-dashed {
|
|
||||||
border: 0 dashed #eee;
|
|
||||||
display: block;
|
|
||||||
opacity: 0.5;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-dashed.dashed-h {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-top-width: 1px;
|
|
||||||
height: calc(100% / 3);
|
|
||||||
left: 0;
|
|
||||||
top: calc(100% / 3);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-dashed.dashed-v {
|
|
||||||
border-left-width: 1px;
|
|
||||||
border-right-width: 1px;
|
|
||||||
height: 100%;
|
|
||||||
left: calc(100% / 3);
|
|
||||||
top: 0;
|
|
||||||
width: calc(100% / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-center {
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
left: 50%;
|
|
||||||
opacity: 0.75;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-center::before,
|
|
||||||
.cropper-center::after {
|
|
||||||
background-color: #eee;
|
|
||||||
content: ' ';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-center::before {
|
|
||||||
height: 1px;
|
|
||||||
left: -3px;
|
|
||||||
top: 0;
|
|
||||||
width: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-center::after {
|
|
||||||
height: 7px;
|
|
||||||
left: 0;
|
|
||||||
top: -3px;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-face,
|
|
||||||
.cropper-line,
|
|
||||||
.cropper-point {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0.1;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-face {
|
|
||||||
background-color: #fff;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-line {
|
|
||||||
background-color: #39f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-line.line-e {
|
|
||||||
cursor: ew-resize;
|
|
||||||
right: -3px;
|
|
||||||
top: 0;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-line.line-n {
|
|
||||||
cursor: ns-resize;
|
|
||||||
height: 5px;
|
|
||||||
left: 0;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-line.line-w {
|
|
||||||
cursor: ew-resize;
|
|
||||||
left: -3px;
|
|
||||||
top: 0;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-line.line-s {
|
|
||||||
bottom: -3px;
|
|
||||||
cursor: ns-resize;
|
|
||||||
height: 5px;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point {
|
|
||||||
background-color: #39f;
|
|
||||||
height: 5px;
|
|
||||||
opacity: 0.75;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-e {
|
|
||||||
cursor: ew-resize;
|
|
||||||
margin-top: -3px;
|
|
||||||
right: -3px;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-n {
|
|
||||||
cursor: ns-resize;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -3px;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-w {
|
|
||||||
cursor: ew-resize;
|
|
||||||
left: -3px;
|
|
||||||
margin-top: -3px;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-s {
|
|
||||||
bottom: -3px;
|
|
||||||
cursor: s-resize;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-ne {
|
|
||||||
cursor: nesw-resize;
|
|
||||||
right: -3px;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-nw {
|
|
||||||
cursor: nwse-resize;
|
|
||||||
left: -3px;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-sw {
|
|
||||||
bottom: -3px;
|
|
||||||
cursor: nesw-resize;
|
|
||||||
left: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-se {
|
|
||||||
bottom: -3px;
|
|
||||||
cursor: nwse-resize;
|
|
||||||
height: 20px;
|
|
||||||
opacity: 1;
|
|
||||||
right: -3px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.cropper-point.point-se {
|
|
||||||
height: 15px;
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.cropper-point.point-se {
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.cropper-point.point-se {
|
|
||||||
height: 5px;
|
|
||||||
opacity: 0.75;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-point.point-se::before {
|
|
||||||
background-color: #39f;
|
|
||||||
bottom: -50%;
|
|
||||||
content: ' ';
|
|
||||||
display: block;
|
|
||||||
height: 200%;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: -50%;
|
|
||||||
width: 200%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-invisible {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-bg {
|
|
||||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-hide {
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-move {
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-crop {
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-disabled .cropper-drag-box,
|
|
||||||
.cropper-disabled .cropper-face,
|
|
||||||
.cropper-disabled .cropper-line,
|
|
||||||
.cropper-disabled .cropper-point {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
3259
public/js/cropper.js
3259
public/js/cropper.js
File diff suppressed because it is too large
Load diff
|
@ -84,7 +84,6 @@ const storage = multer.diskStorage({
|
||||||
cb(null, './public/images/avatars')
|
cb(null, './public/images/avatars')
|
||||||
},
|
},
|
||||||
filename: function (req, _file, cb) {
|
filename: function (req, _file, cb) {
|
||||||
console.log(_file)
|
|
||||||
cb(null, req.member.id + ".jpg")
|
cb(null, req.member.id + ".jpg")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -92,7 +91,6 @@ const storage = multer.diskStorage({
|
||||||
const upload = multer({ storage })
|
const upload = multer({ storage })
|
||||||
|
|
||||||
app.post("/:id/avatar", upload.single('avatar'), async (req, res) => {
|
app.post("/:id/avatar", upload.single('avatar'), async (req, res) => {
|
||||||
|
|
||||||
const { member } = req;
|
const { member } = req;
|
||||||
|
|
||||||
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");
|
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");
|
||||||
|
|
|
@ -13,7 +13,7 @@ app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {
|
||||||
let { name, password, about, email } = req.body;
|
let { name, password, about, email } = req.body;
|
||||||
|
|
||||||
if (!name || !password || !email) return res.error(400, "You forgot entering some values");
|
if (!name || !password || !email) return res.error(400, "You forgot entering some values");
|
||||||
if (!emailRegEx.test(email)) return res.error(400, "E-mail is not valid");
|
if (!email || !emailRegEx.test(email)) return res.error(400, "E-mail is not valid");
|
||||||
const { names } = req.app.get("limits");
|
const { names } = req.app.get("limits");
|
||||||
if (name.length < 3 || name.length > names) return res.error(400, "Name must be between 3 - 25 characters");
|
if (name.length < 3 || name.length > names) return res.error(400, "Name must be between 3 - 25 characters");
|
||||||
if (password.length < 3 || password.length > names) return res.error(400, "Password must be between 3 - 25 characters");
|
if (password.length < 3 || password.length > names) return res.error(400, "Password must be between 3 - 25 characters");
|
||||||
|
|
|
@ -4,109 +4,27 @@
|
||||||
<%- include("extra/meta", {title: "Avatar Upload Panel!" }) %>
|
<%- include("extra/meta", {title: "Avatar Upload Panel!" }) %>
|
||||||
|
|
||||||
<body style="text-align: center;">
|
<body style="text-align: center;">
|
||||||
<link rel="stylesheet" href="/css/cropper.css">
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
margin: 20px auto;
|
|
||||||
max-width: 640px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-view-box,
|
|
||||||
.cropper-face {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-view-box {
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: 0 0 0 1px #39f;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="/js/cropper.js"></script>
|
|
||||||
|
|
||||||
<%- include("extra/navbar") %>
|
<%- include("extra/navbar") %>
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<h1>Upload avatar for <%= member.name %></h1>
|
<h1>Upload avatar for <%= member.name %></h1>
|
||||||
|
<form>
|
||||||
|
<input type="file" name="avatar" /><br>
|
||||||
|
<input class="btn-primary" type="submit" value="Upload" />
|
||||||
|
</form>
|
||||||
|
|
||||||
<input type="file" id="file-input">
|
|
||||||
<img id="image" src="<%= member.avatar %>">
|
|
||||||
<p>
|
|
||||||
<button type="button" id="button">Upload</button>
|
|
||||||
</p>
|
|
||||||
<file id="cropped-image" name="avatar">
|
|
||||||
<form id="form"></form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script>
|
<script>
|
||||||
function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
|
const form = document.querySelector('form');
|
||||||
|
form.addEventListener('submit', async e => {
|
||||||
const byteCharacters = atob(b64Data);
|
e.preventDefault();
|
||||||
const byteArrays = [];
|
|
||||||
|
|
||||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
||||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
|
||||||
|
|
||||||
const byteNumbers = new Array(slice.length);
|
|
||||||
for (let i = 0; i < slice.length; i++)
|
|
||||||
byteNumbers[i] = slice.charCodeAt(i);
|
|
||||||
const byteArray = new Uint8Array(byteNumbers);
|
|
||||||
byteArrays.push(byteArray);
|
|
||||||
}
|
|
||||||
const blob = new Blob(byteArrays, {
|
|
||||||
type: contentType
|
|
||||||
});
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
const image = document.getElementById('image');
|
|
||||||
const button = document.getElementById('button');
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
document.getElementById("file-input")
|
|
||||||
.addEventListener("change", function() {
|
|
||||||
reader.onload = () => {
|
|
||||||
image.src = reader.result;
|
|
||||||
|
|
||||||
let croppable = false;
|
|
||||||
const cropper = new Cropper(image, {
|
|
||||||
aspectRatio: 1,
|
|
||||||
viewMode: 1,
|
|
||||||
ready: function() {
|
|
||||||
croppable = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
button.onclick = async () => {
|
|
||||||
if (!croppable) return;
|
|
||||||
|
|
||||||
const croppedCanvas = cropper.getCroppedCanvas();
|
|
||||||
const croppedImage = document.getElementById('cropped-image');
|
|
||||||
const body = new FormData(document.createElement('form'))
|
|
||||||
const block = croppedCanvas.toDataURL().split(";");
|
|
||||||
const contentType = block[0].split(":")[1];
|
|
||||||
const realData = block[1].split(",")[1];
|
|
||||||
body.append('avatar', b64toBlob(realData, contentType));
|
|
||||||
|
|
||||||
const res = await fetch('/api/users/<%= member.id %>/avatar', {
|
const res = await fetch('/api/users/<%= member.id %>/avatar', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body
|
body: new FormData(form)
|
||||||
}).then(res => res.json());
|
})
|
||||||
|
|
||||||
if (res.error) return alert(res.error);
|
if (res.error) return alert(res.error);
|
||||||
alert('Success!');
|
alert('Success!');
|
||||||
location.reload();
|
location.href = "/users/<%= member.id %>"
|
||||||
};
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(event.target.files[0]);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<%- include("extra/footer") %>
|
<%- include("extra/footer") %>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in a new issue