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/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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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')));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
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 = {
|
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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user