Frontend revamp with Vue

[frontend] Created UserAvatarComponent to make better reusability of this component and adapted views and components to it
[frontend] It is now possible to upload a photo of the user when creating the user
[frontend] Deleting the user deletes the photo in the filesystem
[backend] Added logic to receive uploaded user photo and store it in the filesystem under user_images directory
[backend] Added logic to /user_images be routed when requested
[docker] Updated docker files and example docker compose file
This commit is contained in:
João Vitória Silva
2024-05-23 14:05:19 +01:00
parent 2a6f92a5e9
commit 245cec7945
18 changed files with 224 additions and 30 deletions

5
.gitignore vendored
View File

@@ -8,6 +8,11 @@ backend/*.pyc
backend/logs/*.log backend/logs/*.log
backend/*.log backend/*.log
# user image folder images
backend/user_images/*.jpeg
backend/user_images/*.png
backend/user_images/*.jpg
# Frontend # Frontend
frontend/img/users_img/*.* frontend/img/users_img/*.*
# Logs # Logs

View File

@@ -42,7 +42,7 @@ ENV JAGGER_PORT=4317
ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30 ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30
ENV FRONTEND_PROTOCOL="http" ENV FRONTEND_PROTOCOL="http"
ENV FRONTEND_HOST="frontend" ENV FRONTEND_HOST="frontend"
ENV FRONTEND_PORT=80 ENV FRONTEND_PORT=8080
ENV GEOCODES_MAPS_API="changeme" ENV GEOCODES_MAPS_API="changeme"
# Run main.py when the container launches # Run main.py when the container launches

View File

@@ -1,3 +1,5 @@
import os
import glob
import logging import logging
from fastapi import HTTPException, status from fastapi import HTTPException, status
@@ -11,6 +13,25 @@ import models
# Define a loggger created on main.py # Define a loggger created on main.py
logger = logging.getLogger("myLogger") logger = logging.getLogger("myLogger")
def delete_user_photo_filesystem(user_id: int):
# Define the pattern to match files with the specified name regardless of the extension
folder = "user_images"
file = f"{user_id}.*"
print(os.path.join(folder, file))
# Find all files matching the pattern
files_to_delete = glob.glob(os.path.join(folder, file))
print(f"Files to delete: {files_to_delete}")
# Remove each file found
for file_path in files_to_delete:
print(f"Deleting: {file_path}")
if os.path.exists(file_path):
os.remove(file_path)
print(f"Deleted: {file_path}")
def format_user_birthdate(user): def format_user_birthdate(user):
user.birthdate = user.birthdate.strftime("%Y-%m-%d") if user.birthdate else None user.birthdate = user.birthdate.strftime("%Y-%m-%d") if user.birthdate else None
@@ -334,6 +355,10 @@ def edit_user(user: schema_users.User, db: Session):
# Commit the transaction # Commit the transaction
db.commit() db.commit()
if db_user.photo_path is None:
# Delete the user photo in the filesystem
delete_user_photo_filesystem(db_user.id)
except IntegrityError as integrity_error: except IntegrityError as integrity_error:
# Rollback the transaction # Rollback the transaction
db.rollback() db.rollback()
@@ -379,6 +404,30 @@ def edit_user_password(user_id: int, password: str, db: Session):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error", detail="Internal Server Error",
) from err ) from err
def edit_user_photo_path(user_id: int, photo_path: str, db: Session):
try:
# Get the user from the database
db_user = db.query(models.User).filter(models.User.id == user_id).first()
# Update the user
db_user.photo_path = photo_path
# Commit the transaction
db.commit()
except Exception as err:
# Rollback the transaction
db.rollback()
# Log the exception
logger.error(f"Error in edit_user_photo_path: {err}", exc_info=True)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error",
) from err
def delete_user_photo(user_id: int, db: Session): def delete_user_photo(user_id: int, db: Session):
@@ -392,6 +441,9 @@ def delete_user_photo(user_id: int, db: Session):
# Commit the transaction # Commit the transaction
db.commit() db.commit()
# Delete the user photo in the filesystem
delete_user_photo_filesystem(user_id)
except Exception as err: except Exception as err:
# Rollback the transaction # Rollback the transaction
db.rollback() db.rollback()
@@ -420,6 +472,9 @@ def delete_user(user_id: int, db: Session):
# Commit the transaction # Commit the transaction
db.commit() db.commit()
# Delete the user photo in the filesystem
delete_user_photo_filesystem(user_id)
except Exception as err: except Exception as err:
# Rollback the transaction # Rollback the transaction
db.rollback() db.rollback()

View File

@@ -3,6 +3,7 @@ import os
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from alembic.config import Config from alembic.config import Config
from alembic import command from alembic import command
@@ -150,9 +151,14 @@ app = FastAPI(
}, },
) )
# Add a route to serve the user images
app.mount("/user_images", StaticFiles(directory="user_images"), name="user_images")
# Add CORS middleware to allow requests from the frontend
origins = [ origins = [
"http://localhost", "http://localhost",
"http://localhost:8080", "http://localhost:8080",
"http://localhost:5173",
os.environ.get("FRONTEND_PROTOCOL") os.environ.get("FRONTEND_PROTOCOL")
+ "://" + "://"
+ os.environ.get("FRONTEND_HOST") + os.environ.get("FRONTEND_HOST")

View File

@@ -1,11 +1,14 @@
import os
import logging import logging
from typing import Annotated, Callable from typing import Annotated, Callable
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import shutil
from schemas import schema_users from schemas import schema_users
from crud import crud_user_integrations, crud_users from crud import crud_user_integrations, crud_users
from dependencies import ( from dependencies import (
@@ -158,6 +161,52 @@ async def create_user(
return created_user.id return created_user.id
@router.post(
"/users/{user_id}/upload/image",
status_code=201,
response_model=None,
tags=["users"],
)
async def upload_user_image(
user_id: int,
token_user_id: Annotated[
Callable,
Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
],
file: UploadFile,
db: Session = Depends(dependencies_database.get_db),
):
try:
upload_dir = "user_images"
os.makedirs(upload_dir, exist_ok=True)
# Get file extension
_, file_extension = os.path.splitext(file.filename)
filename = f"{user_id}{file_extension}"
file_path_to_save = os.path.join(upload_dir, filename)
with open(file_path_to_save, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
crud_users.edit_user_photo_path(user_id, file_path_to_save, db)
except Exception as err:
# Log the exception
logger.error(
f"Error in upload_user_image: {err}", exc_info=True
)
# Remove the file after processing
if os.path.exists(file_path_to_save):
os.remove(file_path_to_save)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error",
) from err
@router.put("/users/edit", tags=["users"]) @router.put("/users/edit", tags=["users"])
async def edit_user( async def edit_user(
user_attributtes: schema_users.User, user_attributtes: schema_users.User,

View File

View File

@@ -6,7 +6,7 @@ services:
image: ghcr.io/joaovitoriasilva/endurain/frontend:latest image: ghcr.io/joaovitoriasilva/endurain/frontend:latest
#environment: #environment:
#- MY_APP_BACKEND_PROTOCOL=http # http or https, default is http #- MY_APP_BACKEND_PROTOCOL=http # http or https, default is http
#- MY_APP_BACKEND_HOST=backend # api host, default is backend #- MY_APP_BACKEND_HOST=localhost:98 # api host, default is localhost:98
# Configure volume if you want to edit the code locally by clomming the repo # Configure volume if you want to edit the code locally by clomming the repo
#volumes: #volumes:
# - <local_path>/endurain/frontend:/app # - <local_path>/endurain/frontend:/app
@@ -25,7 +25,9 @@ services:
- STRAVA_CLIENT_SECRET=changeme - STRAVA_CLIENT_SECRET=changeme
- STRAVA_AUTH_CODE=changeme - STRAVA_AUTH_CODE=changeme
- GEOCODES_MAPS_API=changeme - GEOCODES_MAPS_API=changeme
- FRONTEND_PROTOCOL=http # default is http
- FRONTEND_HOST=frontend # default is frontend - FRONTEND_HOST=frontend # default is frontend
- FRONTEND_PORT=8080 # default is 80
ports: ports:
- "98:80" # API port, change per your needs - "98:80" # API port, change per your needs
# Configure volume if you want to edit the code locally by clomming the repo # Configure volume if you want to edit the code locally by clomming the repo

View File

@@ -8,10 +8,8 @@
<div v-else> <div v-else>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<!-- user name and photo zone --> <!-- user name and photo zone -->
<div class="d-flex align-items-center"> <div class="d-flex align-items-center" v-if="userActivity">
<img :src="userActivity.photo_path" alt="User Photo" width="55" height="55" class="rounded-circle" v-if="userActivity.photo_path"> <UserAvatarComponent :userProp="userActivity" :width=55 :height=55 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="55" height="55" class="rounded-circle" v-else-if="!userActivity.photo_path && userActivity.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="55" height="55" class="rounded-circle" v-else>
<div class="ms-3 me-3"> <div class="ms-3 me-3">
<div class="fw-bold"> <div class="fw-bold">
<router-link :to="{ name: 'activity', params: { id: activity.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="sourceProp === 'home'"> <router-link :to="{ name: 'activity', params: { id: activity.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="sourceProp === 'home'">
@@ -190,6 +188,7 @@ import { useRouter } from 'vue-router';
// Importing the components // Importing the components
import LoadingComponent from '@/components/LoadingComponent.vue'; import LoadingComponent from '@/components/LoadingComponent.vue';
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue'; import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
// Importing the stores // Importing the stores
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert'; import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
// Importing the services // Importing the services
@@ -202,6 +201,7 @@ export default {
components: { components: {
LoadingComponent, LoadingComponent,
ErrorToastComponent, ErrorToastComponent,
UserAvatarComponent,
}, },
props: { props: {
activity: { activity: {

View File

@@ -3,9 +3,7 @@
<LoadingComponent /> <LoadingComponent />
</div> </div>
<div class="d-flex align-items-center" v-if="!isLoading"> <div class="d-flex align-items-center" v-if="!isLoading">
<img :src="userMe.photo_path" alt="User Photo" width="55" height="55" class="rounded-circle" v-if="userFollower.photo_path"> <UserAvatarComponent :userProp="userFollower" :width=55 :height=55 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="55" height="55" class="rounded-circle" v-else-if="!userFollower.photo_path && userFollower.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="55" height="55" class="rounded-circle" v-else>
<div class="ms-3"> <div class="ms-3">
<div class="fw-bold"> <div class="fw-bold">
<router-link :to="{ name: 'user', params: { id: userFollower.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"> <router-link :to="{ name: 'user', params: { id: userFollower.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover">
@@ -151,10 +149,12 @@ import { useRoute } from 'vue-router';
import { users } from '@/services/user'; import { users } from '@/services/user';
import { followers } from '@/services/followers'; import { followers } from '@/services/followers';
import LoadingComponent from '@/components/LoadingComponent.vue'; import LoadingComponent from '@/components/LoadingComponent.vue';
import UserAvatarComponent from '../Users/UserAvatarComponent.vue';
export default { export default {
components: { components: {
LoadingComponent, LoadingComponent,
UserAvatarComponent,
}, },
emits: ['followerDeleted', 'followingDeleted', 'followerAccepted'], emits: ['followerDeleted', 'followingDeleted', 'followerAccepted'],
props: { props: {

View File

@@ -13,7 +13,7 @@
<br> <br>
<input type="text" class="form-control" id="inputTextFieldToSearch" :placeholder='$t("footer.searchInputPlaceholder")' v-model="inputSearch"> <input type="text" class="form-control" id="inputTextFieldToSearch" :placeholder='$t("footer.searchInputPlaceholder")' v-model="inputSearch">
<ul v-if="searchResults.length" class="list-group"> <ul v-if="searchResults.length" class="list-group">
<li v-for="result in searchResults" :key="result.id" class="list-group-item list-group-item-action" @click="goToResultPage(result)"> <li v-for="result in searchResults" :key="result.id" class="list-group-item list-group-item-action">
<!-- user link --> <!-- user link -->
<router-link :to="{ name: 'user', params: { id: result.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="searchSelectValue == 1"> <router-link :to="{ name: 'user', params: { id: result.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="searchSelectValue == 1">
{{ result.name}} - {{ result.username}} {{ result.name}} - {{ result.username}}

View File

@@ -21,9 +21,7 @@
<div class="navbar-nav"> <div class="navbar-nav">
<span class="border-top d-sm-none d-block mb-2" v-if="isLoggedIn"></span> <span class="border-top d-sm-none d-block mb-2" v-if="isLoggedIn"></span>
<router-link :to="{ name: 'user', params: { id: userMe.id } }" class="nav-link" v-if="isLoggedIn && userMe"> <router-link :to="{ name: 'user', params: { id: userMe.id } }" class="nav-link" v-if="isLoggedIn && userMe">
<img :src="userMe.photo_path" alt="User Photo" width="24" height="24" class="rounded-circle align-top" v-if="userMe.photo_path"> <UserAvatarComponent :userProp="userMe" :width=24 :height=24 :alignTop=2 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="24" height="24" class="rounded-circle align-top" v-else-if="!userMe.photo_path && userMe.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="24" height="24" class="rounded-circle align-top" v-else>
<span class="ms-2">{{ $t("navbar.profile") }}</span> <span class="ms-2">{{ $t("navbar.profile") }}</span>
</router-link> </router-link>
<span class="border-top d-sm-none d-block" v-if="isLoggedIn"></span> <span class="border-top d-sm-none d-block" v-if="isLoggedIn"></span>
@@ -61,7 +59,12 @@ import { watch, ref } from 'vue';
import { auth } from '@/services/auth'; import { auth } from '@/services/auth';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import UserAvatarComponent from './Users/UserAvatarComponent.vue';
export default { export default {
components: {
UserAvatarComponent,
},
setup() { setup() {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();

View File

@@ -6,9 +6,7 @@
<div class="row row-gap-3"> <div class="row row-gap-3">
<div class="col-lg-4 col-md-12"> <div class="col-lg-4 col-md-12">
<div class="justify-content-center align-items-center d-flex"> <div class="justify-content-center align-items-center d-flex">
<img :src="userMe.photo_path" alt="User Photo" width="180" height="180" class="rounded-circle" v-if="userMe.photo_path"> <UserAvatarComponent :userProp="userMe" :width=180 :height=180 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="180" height="180" class="rounded-circle" v-else-if="!userMe.photo_path && userMe.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="180" height="180" class="rounded-circle" v-else>
</div> </div>
<!-- Delete profile photo section --> <!-- Delete profile photo section -->
@@ -138,11 +136,13 @@ import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
// Importing the components // Importing the components
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue'; import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue'; import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
import UserAvatarComponent from '../Users/UserAvatarComponent.vue';
export default { export default {
components: { components: {
ErrorToastComponent, ErrorToastComponent,
SuccessToastComponent, SuccessToastComponent,
UserAvatarComponent,
}, },
setup() { setup() {
const userMe = ref(JSON.parse(localStorage.getItem('userMe'))); const userMe = ref(JSON.parse(localStorage.getItem('userMe')));

View File

@@ -20,7 +20,7 @@
<div class="modal-body"> <div class="modal-body">
<!-- img fields --> <!-- img fields -->
<label for="userImgAdd"><b>{{ $t("settingsUsersZone.addUserModalUserPhotoLabel") }}</b></label> <label for="userImgAdd"><b>{{ $t("settingsUsersZone.addUserModalUserPhotoLabel") }}</b></label>
<input class="form-control" type="file" accept="image/*" name="userImgAdd" id="userImgAdd"> <input class="form-control" type="file" accept="image/*" name="userImgAdd" id="userImgAdd" @change="handleFileChange">
<!-- username fields --> <!-- username fields -->
<label for="userUsernameAdd"><b>* {{ $t("settingsUsersZone.addUserModalUsernameLabel") }}</b></label> <label for="userUsernameAdd"><b>* {{ $t("settingsUsersZone.addUserModalUsernameLabel") }}</b></label>
<input class="form-control" type="text" name="userUsernameAdd" :placeholder='$t("settingsUsersZone.addUserModalUsernamePlaceholder")' maxlength="45" v-model="newUserUsername" required> <input class="form-control" type="text" name="userUsernameAdd" :placeholder='$t("settingsUsersZone.addUserModalUsernamePlaceholder")' maxlength="45" v-model="newUserUsername" required>
@@ -133,7 +133,7 @@ export default {
const isLoading = ref(true); const isLoading = ref(true);
const errorMessage = ref(''); const errorMessage = ref('');
const successMessage = ref(''); const successMessage = ref('');
const newUserPhoto = ref(''); const newUserPhotoFile = ref(null);
const newUserUsername = ref(''); const newUserUsername = ref('');
const newUserName = ref(''); const newUserName = ref('');
const newUserEmail = ref(''); const newUserEmail = ref('');
@@ -154,6 +154,27 @@ export default {
const numRecords = 5; const numRecords = 5;
const searchUsername = ref(''); const searchUsername = ref('');
async function handleFileChange(event) {
if (event.target.files && event.target.files[0]) {
newUserPhotoFile.value = event.target.files[0];
} else {
newUserPhotoFile.value = null;
}
}
async function uploadImage(file, userId) {
const formData = new FormData();
formData.append('file', file);
try {
return await users.uploadUserImage(formData, userId);
} catch (error) {
// Set the error message
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
errorAlertStore.setAlertMessage(errorMessage.value);
}
}
async function fetchMoreUsers() { async function fetchMoreUsers() {
// If the component is already loading or there are no more gears to fetch, return. // If the component is already loading or there are no more gears to fetch, return.
if (isLoading.value || !hasMoreUsers.value) return; if (isLoading.value || !hasMoreUsers.value) return;
@@ -210,6 +231,7 @@ export default {
async function submitAddUserForm() { async function submitAddUserForm() {
try { try {
if (isPasswordValid.value) { if (isPasswordValid.value) {
// Create the gear data object. // Create the gear data object.
const data = { const data = {
name: newUserName.value, name: newUserName.value,
@@ -229,6 +251,11 @@ export default {
// Create the gear and get the created gear id. // Create the gear and get the created gear id.
const createdUserId = await users.createUser(data); const createdUserId = await users.createUser(data);
// If there is a photo, upload it and get the photo url.
if (newUserPhotoFile.value) {
await uploadImage(newUserPhotoFile.value, createdUserId);
}
// Get the created gear and add it to the userGears array. // Get the created gear and add it to the userGears array.
const newUser = await users.getUserById(createdUserId); const newUser = await users.getUserById(createdUserId);
usersArray.value.unshift(newUser); usersArray.value.unshift(newUser);
@@ -298,7 +325,7 @@ export default {
isLoading, isLoading,
errorMessage, errorMessage,
successMessage, successMessage,
newUserPhoto, newUserPhotoFile,
newUserUsername, newUserUsername,
newUserName, newUserName,
newUserEmail, newUserEmail,
@@ -310,6 +337,7 @@ export default {
newUserPreferredLanguage, newUserPreferredLanguage,
newUserAccessType, newUserAccessType,
submitAddUserForm, submitAddUserForm,
handleFileChange,
usersNumber, usersNumber,
usersArray, usersArray,
searchUsername, searchUsername,

View File

@@ -4,9 +4,7 @@
<li class="list-group-item d-flex justify-content-between"> <li class="list-group-item d-flex justify-content-between">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<img :src="userProp.photo_path" alt="User Photo" width="55" height="55" class="rounded-circle" v-if="userProp.photo_path"> <UserAvatarComponent :userProp="userProp" :width=55 :height=55 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="55" height="55" class="rounded-circle" v-else-if="!userProp.photo_path && userProp.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="55" height="55" class="rounded-circle" v-else>
<div class="ms-3"> <div class="ms-3">
<div class="fw-bold"> <div class="fw-bold">
{{ userProp.username }} {{ userProp.username }}
@@ -178,6 +176,7 @@ import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue'; import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue'; import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
import SettingsPasswordRequirementsComponent from '@/components/Settings/SettingsPasswordRequirementsComponent.vue'; import SettingsPasswordRequirementsComponent from '@/components/Settings/SettingsPasswordRequirementsComponent.vue';
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
// Importing the crypto-js // Importing the crypto-js
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
@@ -186,6 +185,7 @@ export default {
ErrorToastComponent, ErrorToastComponent,
SuccessToastComponent, SuccessToastComponent,
SettingsPasswordRequirementsComponent, SettingsPasswordRequirementsComponent,
UserAvatarComponent,
}, },
props: { props: {
user: { user: {

View File

@@ -0,0 +1,43 @@
<template>
<img :src="userPhotoUrl" :alt="altText" :width="width" :height="height" class="rounded-circle" :class="{ 'align-top': alignTopValue == 2 }" v-if="userProp.photo_path">
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" :width="width" :height="height" class="rounded-circle" :class="{ 'align-top': alignTopValue == 2 }" v-else-if="!userProp.photo_path && userProp.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" :width="width" :height="height" class="rounded-circle" :class="{ 'align-top': alignTopValue == 2 }" v-else>
</template>
<script>
import { ref } from 'vue';
export default {
props: {
userProp: {
type: Object,
required: true
},
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true
},
alignTop: {
type: Number,
default: 1,
required: false
}
},
emits: ['userDeleted'],
setup(props) {
const altText = ref('User Avatar');
const userPhotoUrl = ref(`${import.meta.env.VITE_BACKEND_PROTOCOL}://${import.meta.env.VITE_BACKEND_HOST}/${props.userProp.photo_path}`);
const alignTopValue = ref(props.alignTop);
return {
altText,
userPhotoUrl,
alignTopValue,
};
},
};
</script>

View File

@@ -1,4 +1,4 @@
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest } from '@/utils/serviceUtils'; import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest, fetchPostFileRequest } from '@/utils/serviceUtils';
export const users = { export const users = {
getUsersWithPagination(pageNumber, numRecords) { getUsersWithPagination(pageNumber, numRecords) {
@@ -16,6 +16,9 @@ export const users = {
createUser(data) { createUser(data) {
return fetchPostRequest('users/create', data) return fetchPostRequest('users/create', data)
}, },
uploadUserImage(data, user_id) {
return fetchPostFileRequest(`users/${user_id}/upload/image`, data);
},
editUser(data) { editUser(data) {
return fetchPutRequest('users/edit', data) return fetchPutRequest('users/edit', data)
}, },

View File

@@ -10,9 +10,7 @@
</div> </div>
<div v-else> <div v-else>
<div class="justify-content-center d-flex" v-if="userMe"> <div class="justify-content-center d-flex" v-if="userMe">
<img :src="userMe.photo_path" alt="User Photo" width="120" height="120" class="rounded-circle" v-if="userMe.photo_path"> <UserAvatarComponent :userProp="userMe" :width=120 :height=120 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="120" height="120" class="rounded-circle" v-else-if="!userMe.photo_path && userMe.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="120" height="120" class="rounded-circle" v-else>
</div> </div>
<div class="text-center mt-3 mb-3 fw-bold" v-if="userMe"> <div class="text-center mt-3 mb-3 fw-bold" v-if="userMe">
<router-link :to="{ name: 'user', params: { id: userMe.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"> <router-link :to="{ name: 'user', params: { id: userMe.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover">
@@ -138,6 +136,7 @@ import LoadingComponent from '@/components/LoadingComponent.vue';
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue'; import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue'; import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
import LoadingToastComponent from '@/components/Toasts/LoadingToastComponent.vue'; import LoadingToastComponent from '@/components/Toasts/LoadingToastComponent.vue';
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
//import { Modal } from 'bootstrap'; //import { Modal } from 'bootstrap';
@@ -151,6 +150,7 @@ export default {
ErrorToastComponent, ErrorToastComponent,
SuccessToastComponent, SuccessToastComponent,
LoadingToastComponent, LoadingToastComponent,
UserAvatarComponent,
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();

View File

@@ -13,9 +13,7 @@
</div> </div>
<div class="vstack d-flex justify-content-center" v-else> <div class="vstack d-flex justify-content-center" v-else>
<div class="d-flex justify-content-center" v-if="userMe"> <div class="d-flex justify-content-center" v-if="userMe">
<img :src="userMe.photo_path" alt="User Photo" width="120" height="120" class="rounded-circle" v-if="userMe.photo_path"> <UserAvatarComponent :userProp="userMe" :width=120 :height=120 />
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="120" height="120" class="rounded-circle" v-else-if="!userMe.photo_path && userMe.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="120" height="120" class="rounded-circle" v-else>
</div> </div>
<div class="text-center mt-3 mb-3" v-if="userMe"> <div class="text-center mt-3 mb-3" v-if="userMe">
<h3> <h3>
@@ -292,6 +290,7 @@ import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue'; import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
import FollowersListComponent from '@/components/Followers/FollowersListComponent.vue'; import FollowersListComponent from '@/components/Followers/FollowersListComponent.vue';
import BackButtonComponent from '@/components/BackButtonComponent.vue'; import BackButtonComponent from '@/components/BackButtonComponent.vue';
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
export default { export default {
components: { components: {
@@ -304,6 +303,7 @@ export default {
ErrorToastComponent, ErrorToastComponent,
SuccessToastComponent, SuccessToastComponent,
BackButtonComponent, BackButtonComponent,
UserAvatarComponent,
}, },
setup () { setup () {
const idFromParam = computed(() => route.params.id); const idFromParam = computed(() => route.params.id);