Added default PFP; Added PFP upload functionality; Show pfp in Navbar

This commit is contained in:
EisiBaer 2023-09-23 16:01:51 +02:00
parent fea9a0d0d2
commit 53edb8c7da
19 changed files with 570 additions and 60 deletions

View File

@ -1,4 +1,3 @@
const GameModel = require("../models/GameModel");
const PlayerModel = require("../models/PlayerModel"); const PlayerModel = require("../models/PlayerModel");
/** /**

View File

@ -1,9 +1,10 @@
const bcrypt = require("bcryptjs"); const bcrypt = require("bcryptjs");
const fs = require("node:fs/promises");
const UserModel = require("../models/UserModel"); const UserModel = require("../models/UserModel");
const BoardModel = require("../models/BoardModel"); const BoardModel = require("../models/BoardModel");
exports.listUsers = (req, res) => { exports.listUsers = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
UserModel.find({}, "-password") UserModel.find({}, "-password")
.then((users) => { .then((users) => {
@ -141,3 +142,30 @@ exports.addBoardToUser = ( board ) => {
}) })
}); });
}; };
exports.updateProfilePicture = ( userId, pfpFilename ) => {
return new Promise( ( resolve, reject ) => {
UserModel.findByIdAndUpdate( userId, { pfpFilename: pfpFilename } )
.then( ( userBefore ) => {
if( userBefore === null ){
let userNotFoundError = new Error(`No user found in session"`);
userNotFoundError.name = "NotFoundError";
throw userNotFoundError;
}
if( userBefore.pfpFilename !== null ){
return fs.rm( "public/uploads/" + userBefore.pfpFilename );
} else {
return undefined;
}
})
.then( () => {
return UserModel.findById( userId , {}, { new: true });
})
.then( ( user ) => {
resolve( user );
})
.catch( ( err ) => {
reject( err );
})
});
};

View File

@ -11,6 +11,7 @@ const UserSchema = new Schema({
}, },
password: { type: String, required: true, }, password: { type: String, required: true, },
boards: [{ type: Schema.Types.ObjectId, ref: "Board" }], boards: [{ type: Schema.Types.ObjectId, ref: "Board" }],
pfpFilename: [{ type: String, required: false, default: null }],
}); });
// Virtual for player's URL // Virtual for player's URL

View File

@ -60,21 +60,26 @@ const fileFilterFn = function( req, file, cb ){
return; return;
} }
let board = JSON.parse( req.body.board ); if( req.body.board ){
boardController.isBoardFromUser( board.boardId, req.session.user ) let board = JSON.parse( req.body.board );
.then( ( isFromUser ) => {
if( !isFromUser && board.boardId !== undefined ){ boardController.isBoardFromUser( board.boardId, req.session.user )
cb( new Error( "The associated board is not the users" ) ); .then( ( isFromUser ) => {
} else { if( !isFromUser && board.boardId !== undefined ){
if( req.session.user === undefined ){ cb( new Error( "The associated board is not the users" ) );
cb( new Error( "Only logged in Users can upload pictures" ) ); } else {
return; if( req.session.user === undefined ){
cb( new Error( "Only logged in Users can upload pictures" ) );
return;
}
cb( null, true );
} }
});
cb( null, true ); } else {
} cb( null, true );
}); }
} }
@ -84,7 +89,7 @@ router.get("/", (req, res)=>{
if( req.session.user !== undefined ){ if( req.session.user !== undefined ){
userController.findUser( req.session.user ) userController.findUser( req.session.user )
.then( user => { .then( user => {
res.send({success: true, user: { username: user.username } } ); res.send({success: true, user: { username: user.username, pfpFilename: user.pfpFilename } } );
}) })
.catch( err => { .catch( err => {
console.debug(err); console.debug(err);
@ -124,6 +129,7 @@ router.post("/login", (req, res)=>{
res.send({success: true, user: { username: req.session.user } } ); res.send({success: true, user: { username: req.session.user } } );
}) })
.catch( ( err ) => { .catch( ( err ) => {
console.error( err );
res.send({success:false, error: "Error with logging you in" }); res.send({success:false, error: "Error with logging you in" });
}) })
}else{ }else{
@ -240,19 +246,45 @@ router.get("/boards/:id", (req, res) => {
}); });
router.get("/pfp/:filename", (req, res) => {
console.log("Getting pfp: ", req.params.filename);
let options = {
root: 'public/uploads',
dotfiles: 'deny',
headers: {
'Access-Control-Allow-Origin': '*',
}
}
res.sendFile( req.params.filename, options );
});
router.post("/pfp", upload.single( "pfp" ), (req, res) => { router.post("/pfp", upload.single( "pfp" ), (req, res) => {
if( req.session.user === undefined ){ if( req.session.user === undefined ){
res.send( { success: false, error: "Not logged in!" } ); res.send( { success: false, error: "Not logged in!" } );
} else { } else {
let imageFile = null;
if( !req.file ){ if( !req.file ){
console.error( "No file attached" ); console.error( "No file attached" );
return; throw new Error("No file attached to be saved" );
} }
imageFile = req.file; let imageFile = req.file;
return userController.updateProfilePicture( req.session.user, imageFile.filename ) return userController.updateProfilePicture( req.session.user, imageFile.filename )
.then( ( user ) => { .then( ( user ) => {
res.send( { success: true, newProfilePicture: user.profilePicture } ); res.send( { success: true, newProfilePicture: user.pfpFilename } );
})
.catch( ( err ) => {
res.send( { success: false, error: err } );
});
}
});
router.delete("/pfp", (req, res) => {
if( req.session.user === undefined ){
res.send( { success: false, error: "Not logged in!" } );
} else {
return userController.updateProfilePicture( req.session.user, null )
.then( ( _user ) => {
res.send( { success: true, } );
}) })
.catch( ( err ) => { .catch( ( err ) => {
res.send( { success: false, error: err } ); res.send( { success: false, error: err } );

View File

@ -34,7 +34,7 @@ app.use(helmet(
"media-src": ["'self'", "data:"], "media-src": ["'self'", "data:"],
"style-src-elem": ["'self'", "'unsafe-inline'"], "style-src-elem": ["'self'", "'unsafe-inline'"],
"connect-src": ["'self'", "ws:"], "connect-src": ["'self'", "ws:"],
"img-src": ["'self'", "blob:", "data:"], "img-src": ["'self'", "blob:", "data:"],
} }
} }
} }

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="PFP_Bear.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showguides="true"
inkscape:zoom="0.7071068"
inkscape:cx="48.790367"
inkscape:cy="195.16147"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:2.94695;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="ellipse9"
ry="8.7389574"
rx="7.3490396"
cy="0.21687971"
cx="-74.58239"
transform="matrix(-0.88593468,-0.46381003,-0.43331387,0.90124308,0,0)" />
<ellipse
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:2.94695;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="ellipse8"
ry="8.7389574"
rx="7.3490396"
cy="46.607407"
cx="15.560421"
transform="matrix(0.88593467,-0.46381004,0.43331386,0.90124309,0,0)" />
<path
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:3.20808;stroke-dasharray:none;stroke-opacity:1"
d="M 0.09642571,100.42076 C 7.8465924,67.218518 20.053978,42.128065 50.000563,42.128065 c 29.949567,0 42.15361,25.090329 49.904141,58.294115"
id="path1"
sodipodi:nodetypes="ccc" />
<circle
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
id="path2"
cx="50"
cy="51.3937"
r="22.843529" />
<path
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
d="m 45.498078,55.013425 c 0.503992,-1.440484 2.975715,-1.436799 4.501822,-1.436799 1.526107,0 3.997459,-0.0036 4.501822,1.436799 0.75684,2.161377 -2.209427,5.387564 -4.499483,5.388153 -2.291078,5.89e-4 -5.260782,-3.225617 -4.504161,-5.388153 z"
id="path5"
sodipodi:nodetypes="sssss" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 49.983098,60.437125 c 0,3.055652 -0.219217,3.50138 -1.243542,4.092774 -1.678595,0.969137 -3.486678,-0.345521 -4.177109,-0.744142"
id="path6"
sodipodi:nodetypes="ccc" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 49.952982,60.437125 c 0,3.055652 0.219217,3.50138 1.243542,4.092774 1.678595,0.969137 3.486678,-0.345521 4.177109,-0.744142"
id="path7"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 56.5,44.887112 3.181598,-3.181598 3.180744,3.180745"
id="path9" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 36.5,44.887112 3.181598,-3.181598 3.180744,3.180745"
id="path10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="PFP_BearHead.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showguides="true"
inkscape:zoom="0.7071068"
inkscape:cx="48.790367"
inkscape:cy="195.16147"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" />
<defs
id="defs1" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(0.01884618,-0.22140619)">
<ellipse
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.42699;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="ellipse9"
ry="13.127905"
rx="11.039931"
cy="-10.383468"
cx="-78.467972"
transform="matrix(-0.88593467,-0.46381003,-0.43331387,0.90124309,0,0)" />
<ellipse
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.42699;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="ellipse8"
ry="13.127905"
rx="11.039931"
cy="36.007061"
cx="11.674847"
transform="matrix(0.88593467,-0.46381003,0.43331387,0.90124309,0,0)" />
<circle
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-dasharray:none;stroke-opacity:1"
id="path2"
cx="50.009468"
cy="51.98246"
r="34.316185" />
<path
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-dasharray:none;stroke-opacity:1"
d="m 43.246556,57.420114 c 0.75711,-2.163936 4.470201,-2.158401 6.762761,-2.158401 2.292561,0 6.005093,-0.0054 6.762762,2.158401 1.136946,3.246879 -3.319062,8.093347 -6.759248,8.094232 -3.441721,8.75e-4 -7.902893,-4.845611 -6.766275,-8.094232 z"
id="path5"
sodipodi:nodetypes="sssss" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 49.984077,65.567745 c 0,4.590286 -0.329314,5.259871 -1.868084,6.148279 -2.521632,1.455864 -5.237785,-0.51905 -6.274968,-1.11787"
id="path6"
sodipodi:nodetypes="ccc" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 49.938836,65.567745 c 0,4.590286 0.329314,5.259871 1.868083,6.148279 2.521632,1.455864 5.237784,-0.51905 6.274969,-1.11787"
id="path7"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 59.773948,42.208084 4.779484,-4.779484 4.778202,4.778204"
id="path9" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.50669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 29.729393,42.208084 4.779485,-4.779484 4.778201,4.778204"
id="path10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="PFP_draft1.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showguides="true"
inkscape:zoom="0.3535534"
inkscape:cx="-465.27625"
inkscape:cy="96.16652"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
d="M 8.2680968e-4,99.998029 C 7.7658401,71.018605 19.996612,49.119274 50.000563,49.119274 c 30.00694,0 42.234362,21.899223 49.999737,50.879996"
id="path1"
sodipodi:nodetypes="ccc" />
<circle
style="fill:#e86a92;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
id="path2"
cx="50"
cy="31.188124"
r="22.843529" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -87,6 +87,8 @@ $modal-content-bg: $dark-blue;
$font-family-base: "Urbanist"; $font-family-base: "Urbanist";
$enable-caret: false;
// 2. Include any default variable overrides here // 2. Include any default variable overrides here
@ -188,4 +190,8 @@ $utilities: map-merge(
.image-contain{ .image-contain{
object-fit: contain; object-fit: contain;
}
.pfp{
object-fit: cover;
} }

View File

@ -1,11 +1,17 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useUserStore } from "@/stores/UserStore";
import GenericMultiButtonModal from '@/components/views/GenericMultiButtonModal.vue';
import { openModal } from "@/services/util";
import ProfilePicture from '@/components/blocks/ProfilePicture.vue';
const emit = defineEmits(["profilePictureChanged"]);
let uploadedFileUrl = ref(null); let uploadedFileObj = ref(null);
let uploadingInProcess = ref(false);
let imageInput = ref(null);
const userStore = useUserStore();
function newImageUploaded( event ){ function newImageUploaded( event ){
let files = event.target.files || event.dataTransfer.files; let files = event.target.files || event.dataTransfer.files;
@ -16,14 +22,43 @@ function newImageUploaded( event ){
data: files[0], data: files[0],
url: URL.createObjectURL( files[0] ), url: URL.createObjectURL( files[0] ),
}; };
uploadedFileUrl.value = fileObj; uploadedFileObj.value = fileObj;
} }
function revealAnswer(){ function saveProfilePicture(){
emit("profilePictureChanged"); if( uploadedFileObj.value === null || uploadingInProcess.value === true ){
return;
}
uploadingInProcess.value = true;
userStore.saveProfilePicture( uploadedFileObj.value.data )
.finally( () => {
uploadingInProcess.value = false;
resetUploadedFileObj();
})
} }
revealAnswer(); function deleteProfilePicture(){
openModal('confirmDeletePfpModal');
}
function resetUploadedFileObj(){
uploadedFileObj.value = null;
imageInput.value.value = null;
}
function handleModalButtonClick( buttonIndex ){
switch( buttonIndex ){
case 0:
userStore.deleteProfilePicture()
.finally( () => {
uploadedFileObj.value = null;
})
break;
case 1:
default:
return;
}
}
</script> </script>
@ -36,23 +71,51 @@ revealAnswer();
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-2"> <div class="col-auto">
<div class="ratio ratio-16x9"> <ProfilePicture
<img src="" alt="Current Profile Picture"> :srcOverride="(uploadedFileObj !== null ? uploadedFileObj.url : null )"
</div> :isPreview="uploadedFileObj !== null"
@previewDiscarded="resetUploadedFileObj()"
/>
</div> </div>
<div class="col"> <div class="col-auto border-start">
<div class="row"> <div class="h-100 d-flex flex-column justify-content-evenly">
<div class="col"> <div>
<img src="" alt="Standard Profile Picture"> <label class="form-label fs-5" for="question-image">Upload new profile picture</label>
<input ref="imageInput" class="form-control bg-dark-blue" type="file" name="question-image" id="question-image" @change="newImageUploaded" accept="image/*">
</div>
<div>
<button class="btn btn-pink-accent-primary me-3" :disabled="uploadedFileObj === null" @click="saveProfilePicture">
<font-awesome-icon v-if="uploadingInProcess" icon="fa-solid fa-spinner" spin/>
<font-awesome-icon v-else icon="fa-solid fa-floppy-disk" />
Save
</button>
<button class="btn btn-danger" :disabled="userStore.pfpFilename === null" @click="deleteProfilePicture">
<font-awesome-icon icon="fa-solid fa-floppy-disk" />
Delete Current Profile Picture
</button>
</div> </div>
</div>
<div>
<label class="form-label fs-4 mt-3" for="question-image">Upload new profile picture</label>
<input class="form-control bg-dark-blue" type="file" name="question-image" id="question-image" @change="newImageUploaded( questionIndex, $event )" accept="image/*">
</div> </div>
</div> </div>
</div> </div>
</template>
<style scoped></style> <GenericMultiButtonModal
:id="'confirmDeletePfpModal'"
:hasTitle="true"
:title="'Are you sure?'"
:modalText="'Are you sure you want to delete your current profile picture?'"
:buttonList="[
{
text: 'Yes, delete!',
emitsEvent: 'discardClicked',
bgColorClass: 'btn-danger',
},
{
text: 'Cancel',
bgColorClass: 'btn-pink-accent-primary',
},
]"
@buttonClicked="handleModalButtonClick"
/>
</template>

View File

@ -0,0 +1,72 @@
<script setup>
import { computed } from 'vue';
import { useUserStore } from "@/stores/UserStore"
const props = defineProps({
srcOverride: {
type: [String, null],
default: null,
required: false,
},
sizingClasses: {
type: Array,
default: () => ["pfp-sizing"],
required: false,
},
isPreview: {
type: Boolean,
default: false,
required: false,
}
});
const emit = defineEmits(["previewDiscarded"]);
const userStore = useUserStore();
let protocol = ('https:' == document.location.protocol ? 'https://' : 'http://');
let hostname = window.location.hostname;
if( window.location.hostname.includes("localhost" ) ){
hostname += ':3000';
}
const API_URL = `${protocol}${hostname}/api`;
const pfpSrc = computed( () => {
if( props.srcOverride !== null ){
return props.srcOverride;
} else if( userStore.pfpFilename === null ){
return "/src/webapp/assets/images/PFP_BearHead.svg";
} else {
return `${API_URL}/user/pfp/${userStore.pfpFilename}`;
}
})
</script>
<template>
<div>
<div class="border border-1 border-white rounded-3 overflow-hidden">
<img :src="pfpSrc" alt="Profile Picture of the user" class="pfp" :class="sizingClasses" />
<div v-show="props.isPreview" class="position-relative">
<span class="position-absolute bottom-0 end-0 bg-black bg-opacity-50 px-1 rounded-2">
Preview
<font-awesome-icon icon="fa-solid fa-rotate-left" size="sm" @click="emit('previewDiscarded')" title="Discard uploaded image" class="pointer"/>
</span>
</div>
</div>
</div>
</template>
<style scoped>
.pfp-sizing{
height: 10rem;
width: 10rem;
max-height: 100vh;
max-width: 100vw;
}
.pfp-sizing-navbar{
height: 2em;
width: 2em;
}
</style>

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import { useUserStore } from '@/stores/UserStore'; import { useUserStore } from '@/stores/UserStore';
import ProfilePicture from '@/components/blocks/ProfilePicture.vue';
const userStore = useUserStore(); const userStore = useUserStore();
@ -67,7 +68,13 @@ function logoutButtonClicked(_event){
<div v-if="userStore.loggedIn"> <div v-if="userStore.loggedIn">
<div class="dropdown text-center"> <div class="dropdown text-center">
<a class="dropdown-toggle text-decoration-none" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="dropdown-toggle text-decoration-none" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ userStore.username }} <div class="d-flex align-items-center justify-content-around">
<font-awesome-icon icon="fa-solid fa-angle-down" size="sm" class="me-2"/>
<span class="me-1">{{ userStore.username }}</span>
<ProfilePicture
:sizingClasses="['pfp-sizing-navbar']"
/>
</div>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark bg-dark-blue"> <ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark bg-dark-blue">
<li> <li>
@ -96,7 +103,13 @@ function logoutButtonClicked(_event){
<div v-if="userStore.loggedIn"> <div v-if="userStore.loggedIn">
<div class="dropdown"> <div class="dropdown">
<a class="dropdown-toggle text-decoration-none" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="dropdown-toggle text-decoration-none" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ userStore.username }} <div class="d-flex align-items-center justify-content-around">
<font-awesome-icon icon="fa-solid fa-angle-down" size="sm" class="me-2"/>
<span class="me-1">{{ userStore.username }}</span>
<ProfilePicture
:sizingClasses="['pfp-sizing-navbar']"
/>
</div>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark bg-dark-blue"> <ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark bg-dark-blue">
<li> <li>
@ -127,4 +140,8 @@ function logoutButtonClicked(_event){
.nav-logo{ .nav-logo{
height: 3.75em; height: 3.75em;
} }
.pfp-sizing-navbar{
height: 2em;
width: 2em;
}
</style> </style>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Modal } from "bootstrap"; import { openModal } from "@/services/util";
import BoardEntryEditView from '@/components/views/BoardEntryEditView.vue'; import BoardEntryEditView from '@/components/views/BoardEntryEditView.vue';
import CategoryEditView from '@/components/views/CategoryEditView.vue'; import CategoryEditView from '@/components/views/CategoryEditView.vue';
@ -192,13 +192,6 @@ function exitCreatePage(){
router.push("/profile"); router.push("/profile");
} }
//Maybe extract
function openModal( modalId ){
let modalElement = document.getElementById( modalId );
let modalInstance = Modal.getOrCreateInstance( modalElement );
modalInstance.show();
}
function toggleBottomView(){ function toggleBottomView(){
showingBottomView.value = !showingBottomView.value; showingBottomView.value = !showingBottomView.value;
} }
@ -328,7 +321,6 @@ if( route.params.boardId !== undefined ){
:buttonList="[ :buttonList="[
{ {
text: 'Yes, discard!', text: 'Yes, discard!',
emitsEvent: 'discardClicked',
bgColorClass: 'btn-danger', bgColorClass: 'btn-danger',
}, },
{ {

View File

@ -20,12 +20,10 @@ const props = defineProps({
return [ return [
{ {
text: "Ok", text: "Ok",
emitsEvent: "b1Clicked",
bgColorClass: "btn-pink-accent-primary", bgColorClass: "btn-pink-accent-primary",
}, },
{ {
text: "Cancel", text: "Cancel",
emitsEvent: "b2Clicked",
bgColorClass: "btn-outline-danger", bgColorClass: "btn-outline-danger",
}, },
]; ];

View File

@ -4,7 +4,8 @@ import { createPinia } from "pinia";
import { FontAwesomeIcon, FontAwesomeLayers } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon, FontAwesomeLayers } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { faDragon, faRightToBracket, faUsers, faUserPlus, faSpinner, faPlusSquare, faBorderAll, faPen, faTrash, faAngleDown, faAngleUp, import { faDragon, faRightToBracket, faUsers, faUserPlus, faSpinner, faPlusSquare, faBorderAll, faPen, faTrash, faAngleDown, faAngleUp,
faPlus, faMinus, faAngleRight, faSquare, faPlay, faCircleExclamation, faSquareCheck, faSquareMinus, faHandPointer } from "@fortawesome/free-solid-svg-icons"; faPlus, faMinus, faAngleRight, faSquare, faPlay, faCircleExclamation, faSquareCheck, faSquareMinus, faHandPointer, faFloppyDisk,
faEye, faRotateLeft } from "@fortawesome/free-solid-svg-icons";
import { faCircleUser, faSquarePlus } from "@fortawesome/free-regular-svg-icons"; import { faCircleUser, faSquarePlus } from "@fortawesome/free-regular-svg-icons";
@ -40,6 +41,9 @@ library.add({
faPlay, faPlay,
faCircleExclamation, faCircleExclamation,
faHandPointer, faHandPointer,
faFloppyDisk,
faEye,
faRotateLeft,
}); });

View File

@ -88,7 +88,7 @@ export default class UserService{
} }
); );
} }
getBoardFromUser( boardId ){ getBoardFromUser( boardId ){
return axios.get( return axios.get(
API_URL + "/user/boards/" + boardId, API_URL + "/user/boards/" + boardId,
@ -101,4 +101,22 @@ export default class UserService{
); );
} }
saveNewProfilePicture( pfpFormData ){
return axios.post(
API_URL + "/user/pfp",
pfpFormData,
{
withCredentials: true,
}
);
}
deleteProfilePicture( ){
return axios.delete(
API_URL + "/user/pfp",
{
withCredentials: true,
}
);
}
} }

View File

@ -1,7 +1,10 @@
import { Modal } from "bootstrap";
import BoardEntry from "@/models/BoardEntry"; import BoardEntry from "@/models/BoardEntry";
import Category from "@/models/Category"; import Category from "@/models/Category";
import Board from "@/models/Board"; import Board from "@/models/Board";
export function boardResponseToBoardModel( boardResponse ){ export function boardResponseToBoardModel( boardResponse ){
let categories = []; let categories = [];
@ -28,4 +31,10 @@ export function boardResponseToBoardModel( boardResponse ){
} }
return new Board( boardResponse._id, boardResponse.name, categories ); return new Board( boardResponse._id, boardResponse.name, categories );
}
export function openModal( modalId ){
let modalElement = document.getElementById( modalId );
let modalInstance = Modal.getOrCreateInstance( modalElement );
modalInstance.show();
} }

View File

@ -247,10 +247,10 @@ export const useGameStore = defineStore('game', {
this.websocketConnection.onerror = ( _event ) => { this.websocketConnection.onerror = ( _event ) => {
console.error("Websocket Error"); console.error("Websocket Error");
}; };
this.websocketConnection.onclose = ( event ) => { this.websocketConnection.onclose = ( _event ) => {
clearInterval( this.keepAliveInterval ); clearInterval( this.keepAliveInterval );
this.keepAliveInterval = undefined; this.keepAliveInterval = undefined;
const userStore = useUserStore(); const userStore = useUserStore();
userStore.resetInitialUserDataPromise(); userStore.resetInitialUserDataPromise();
userStore.initialUserPromise userStore.initialUserPromise
.then( ( userData ) => { .then( ( userData ) => {
@ -317,7 +317,7 @@ export const useGameStore = defineStore('game', {
this.players = data.payload.players; this.players = data.payload.players;
} }
}); });
this.addSocketListener("payloadIncomplete", ( data ) => { this.addSocketListener("payloadIncomplete", ( _data ) => {
console.error("Invalid or Incomplete Payload!"); console.error("Invalid or Incomplete Payload!");
}); });
} }

View File

@ -9,6 +9,7 @@ export const useUserStore = defineStore('user', {
loggedIn: false, loggedIn: false,
username: "", username: "",
admin: false, admin: false,
pfpFilename: null,
initialUserPromise: new Promise( (resolve, reject ) => { initialUserPromise: new Promise( (resolve, reject ) => {
uService.getUserFromSession() uService.getUserFromSession()
.then( res => { .then( res => {
@ -43,6 +44,7 @@ export const useUserStore = defineStore('user', {
setUser( user ){ setUser( user ){
this.loggedIn = true; this.loggedIn = true;
this.username = user.username; this.username = user.username;
this.pfpFilename = user.pfpFilename;
}, },
resetInitialUserDataPromise(){ resetInitialUserDataPromise(){
this.initialUserPromise = new Promise( (resolve, reject ) => { this.initialUserPromise = new Promise( (resolve, reject ) => {
@ -59,6 +61,39 @@ export const useUserStore = defineStore('user', {
reject( err ); reject( err );
}); });
}); });
} },
saveProfilePicture( imageData ){
return new Promise( ( resolve, reject ) => {
let formData = new FormData();
formData.append( "pfp", imageData );
this.userService.saveNewProfilePicture( formData )
.then( ( response ) => {
this.pfpFilename = response.data.newProfilePicture;
resolve();
})
.catch( ( error ) => {
console.error( error );
reject();
});
});
},
deleteProfilePicture(){
return new Promise( ( resolve, reject ) => {
this.userService.deleteProfilePicture()
.then( ( response ) => {
if( response.data.success ){
this.pfpFilename = null;
resolve();
} else {
console.warn( "Profile picture could not be deleted" );
reject();
}
})
.catch( ( error ) => {
console.error( error );
reject();
});
});
},
}, },
}) })