mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57:59 -05:00
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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,6 +8,11 @@ backend/*.pyc
|
||||
backend/logs/*.log
|
||||
backend/*.log
|
||||
|
||||
# user image folder images
|
||||
backend/user_images/*.jpeg
|
||||
backend/user_images/*.png
|
||||
backend/user_images/*.jpg
|
||||
|
||||
# Frontend
|
||||
frontend/img/users_img/*.*
|
||||
# Logs
|
||||
|
||||
@@ -42,7 +42,7 @@ ENV JAGGER_PORT=4317
|
||||
ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30
|
||||
ENV FRONTEND_PROTOCOL="http"
|
||||
ENV FRONTEND_HOST="frontend"
|
||||
ENV FRONTEND_PORT=80
|
||||
ENV FRONTEND_PORT=8080
|
||||
ENV GEOCODES_MAPS_API="changeme"
|
||||
|
||||
# Run main.py when the container launches
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
@@ -11,6 +13,25 @@ import models
|
||||
# Define a loggger created on main.py
|
||||
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):
|
||||
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
|
||||
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:
|
||||
# Rollback the transaction
|
||||
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,
|
||||
detail="Internal Server Error",
|
||||
) 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):
|
||||
@@ -392,6 +441,9 @@ def delete_user_photo(user_id: int, db: Session):
|
||||
|
||||
# Commit the transaction
|
||||
db.commit()
|
||||
|
||||
# Delete the user photo in the filesystem
|
||||
delete_user_photo_filesystem(user_id)
|
||||
except Exception as err:
|
||||
# Rollback the transaction
|
||||
db.rollback()
|
||||
@@ -420,6 +472,9 @@ def delete_user(user_id: int, db: Session):
|
||||
|
||||
# Commit the transaction
|
||||
db.commit()
|
||||
|
||||
# Delete the user photo in the filesystem
|
||||
delete_user_photo_filesystem(user_id)
|
||||
except Exception as err:
|
||||
# Rollback the transaction
|
||||
db.rollback()
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from alembic.config import Config
|
||||
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 = [
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:5173",
|
||||
os.environ.get("FRONTEND_PROTOCOL")
|
||||
+ "://"
|
||||
+ os.environ.get("FRONTEND_HOST")
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from typing import Annotated, Callable
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import shutil
|
||||
|
||||
from schemas import schema_users
|
||||
from crud import crud_user_integrations, crud_users
|
||||
from dependencies import (
|
||||
@@ -158,6 +161,52 @@ async def create_user(
|
||||
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"])
|
||||
async def edit_user(
|
||||
user_attributtes: schema_users.User,
|
||||
|
||||
0
backend/user_images/__init__.py
Normal file
0
backend/user_images/__init__.py
Normal file
@@ -6,7 +6,7 @@ services:
|
||||
image: ghcr.io/joaovitoriasilva/endurain/frontend:latest
|
||||
#environment:
|
||||
#- 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
|
||||
#volumes:
|
||||
# - <local_path>/endurain/frontend:/app
|
||||
@@ -25,7 +25,9 @@ services:
|
||||
- STRAVA_CLIENT_SECRET=changeme
|
||||
- STRAVA_AUTH_CODE=changeme
|
||||
- GEOCODES_MAPS_API=changeme
|
||||
- FRONTEND_PROTOCOL=http # default is http
|
||||
- FRONTEND_HOST=frontend # default is frontend
|
||||
- FRONTEND_PORT=8080 # default is 80
|
||||
ports:
|
||||
- "98:80" # API port, change per your needs
|
||||
# Configure volume if you want to edit the code locally by clomming the repo
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
<div v-else>
|
||||
<div class="d-flex justify-content-between">
|
||||
<!-- user name and photo zone -->
|
||||
<div class="d-flex align-items-center">
|
||||
<img :src="userActivity.photo_path" alt="User Photo" width="55" height="55" class="rounded-circle" v-if="userActivity.photo_path">
|
||||
<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="d-flex align-items-center" v-if="userActivity">
|
||||
<UserAvatarComponent :userProp="userActivity" :width=55 :height=55 />
|
||||
<div class="ms-3 me-3">
|
||||
<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'">
|
||||
@@ -190,6 +188,7 @@ import { useRouter } from 'vue-router';
|
||||
// Importing the components
|
||||
import LoadingComponent from '@/components/LoadingComponent.vue';
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
|
||||
// Importing the stores
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
// Importing the services
|
||||
@@ -202,6 +201,7 @@ export default {
|
||||
components: {
|
||||
LoadingComponent,
|
||||
ErrorToastComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
props: {
|
||||
activity: {
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<UserAvatarComponent :userProp="userFollower" :width=55 :height=55 />
|
||||
<div class="ms-3">
|
||||
<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">
|
||||
@@ -151,10 +149,12 @@ import { useRoute } from 'vue-router';
|
||||
import { users } from '@/services/user';
|
||||
import { followers } from '@/services/followers';
|
||||
import LoadingComponent from '@/components/LoadingComponent.vue';
|
||||
import UserAvatarComponent from '../Users/UserAvatarComponent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
emits: ['followerDeleted', 'followingDeleted', 'followerAccepted'],
|
||||
props: {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<br>
|
||||
<input type="text" class="form-control" id="inputTextFieldToSearch" :placeholder='$t("footer.searchInputPlaceholder")' v-model="inputSearch">
|
||||
<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 -->
|
||||
<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}}
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
<div class="navbar-nav">
|
||||
<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">
|
||||
<img :src="userMe.photo_path" alt="User Photo" width="24" height="24" class="rounded-circle align-top" v-if="userMe.photo_path">
|
||||
<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>
|
||||
<UserAvatarComponent :userProp="userMe" :width=24 :height=24 :alignTop=2 />
|
||||
<span class="ms-2">{{ $t("navbar.profile") }}</span>
|
||||
</router-link>
|
||||
<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 { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
import UserAvatarComponent from './Users/UserAvatarComponent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatarComponent,
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
<div class="row row-gap-3">
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<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">
|
||||
<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>
|
||||
<UserAvatarComponent :userProp="userMe" :width=180 :height=180 />
|
||||
</div>
|
||||
|
||||
<!-- Delete profile photo section -->
|
||||
@@ -138,11 +136,13 @@ import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
// Importing the components
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
import UserAvatarComponent from '../Users/UserAvatarComponent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
setup() {
|
||||
const userMe = ref(JSON.parse(localStorage.getItem('userMe')));
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="modal-body">
|
||||
<!-- img fields -->
|
||||
<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 -->
|
||||
<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>
|
||||
@@ -133,7 +133,7 @@ export default {
|
||||
const isLoading = ref(true);
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const newUserPhoto = ref('');
|
||||
const newUserPhotoFile = ref(null);
|
||||
const newUserUsername = ref('');
|
||||
const newUserName = ref('');
|
||||
const newUserEmail = ref('');
|
||||
@@ -154,6 +154,27 @@ export default {
|
||||
const numRecords = 5;
|
||||
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() {
|
||||
// If the component is already loading or there are no more gears to fetch, return.
|
||||
if (isLoading.value || !hasMoreUsers.value) return;
|
||||
@@ -210,6 +231,7 @@ export default {
|
||||
async function submitAddUserForm() {
|
||||
try {
|
||||
if (isPasswordValid.value) {
|
||||
|
||||
// Create the gear data object.
|
||||
const data = {
|
||||
name: newUserName.value,
|
||||
@@ -229,6 +251,11 @@ export default {
|
||||
// Create the gear and get the created gear id.
|
||||
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.
|
||||
const newUser = await users.getUserById(createdUserId);
|
||||
usersArray.value.unshift(newUser);
|
||||
@@ -298,7 +325,7 @@ export default {
|
||||
isLoading,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
newUserPhoto,
|
||||
newUserPhotoFile,
|
||||
newUserUsername,
|
||||
newUserName,
|
||||
newUserEmail,
|
||||
@@ -310,6 +337,7 @@ export default {
|
||||
newUserPreferredLanguage,
|
||||
newUserAccessType,
|
||||
submitAddUserForm,
|
||||
handleFileChange,
|
||||
usersNumber,
|
||||
usersArray,
|
||||
searchUsername,
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<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">
|
||||
<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>
|
||||
<UserAvatarComponent :userProp="userProp" :width=55 :height=55 />
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
{{ userProp.username }}
|
||||
@@ -178,6 +176,7 @@ import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
import SettingsPasswordRequirementsComponent from '@/components/Settings/SettingsPasswordRequirementsComponent.vue';
|
||||
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
|
||||
// Importing the crypto-js
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
@@ -186,6 +185,7 @@ export default {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
SettingsPasswordRequirementsComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
|
||||
43
frontend/src/components/Users/UserAvatarComponent.vue
Normal file
43
frontend/src/components/Users/UserAvatarComponent.vue
Normal 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>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest } from '@/utils/serviceUtils';
|
||||
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest, fetchPostFileRequest } from '@/utils/serviceUtils';
|
||||
|
||||
export const users = {
|
||||
getUsersWithPagination(pageNumber, numRecords) {
|
||||
@@ -16,6 +16,9 @@ export const users = {
|
||||
createUser(data) {
|
||||
return fetchPostRequest('users/create', data)
|
||||
},
|
||||
uploadUserImage(data, user_id) {
|
||||
return fetchPostFileRequest(`users/${user_id}/upload/image`, data);
|
||||
},
|
||||
editUser(data) {
|
||||
return fetchPutRequest('users/edit', data)
|
||||
},
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<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">
|
||||
<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>
|
||||
<UserAvatarComponent :userProp="userMe" :width=120 :height=120 />
|
||||
</div>
|
||||
<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">
|
||||
@@ -138,6 +136,7 @@ import LoadingComponent from '@/components/LoadingComponent.vue';
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
import LoadingToastComponent from '@/components/Toasts/LoadingToastComponent.vue';
|
||||
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
|
||||
|
||||
//import { Modal } from 'bootstrap';
|
||||
|
||||
@@ -151,6 +150,7 @@ export default {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
LoadingToastComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
</div>
|
||||
<div class="vstack d-flex justify-content-center" v-else>
|
||||
<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">
|
||||
<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>
|
||||
<UserAvatarComponent :userProp="userMe" :width=120 :height=120 />
|
||||
</div>
|
||||
<div class="text-center mt-3 mb-3" v-if="userMe">
|
||||
<h3>
|
||||
@@ -292,6 +290,7 @@ import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
import FollowersListComponent from '@/components/Followers/FollowersListComponent.vue';
|
||||
import BackButtonComponent from '@/components/BackButtonComponent.vue';
|
||||
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -304,6 +303,7 @@ export default {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
BackButtonComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
setup () {
|
||||
const idFromParam = computed(() => route.params.id);
|
||||
|
||||
Reference in New Issue
Block a user