Merge pull request #14 from joaovitoriasilva/frontend_revamp_vue
Frontend Vue revamp
39
.gitignore
vendored
@@ -8,5 +8,42 @@ 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/*.*
|
||||
frontend/img/users_img/*.*
|
||||
# Logs
|
||||
frontend/.gitignore
|
||||
frontend/logs
|
||||
frontend/*.log
|
||||
frontend/npm-debug.log*
|
||||
frontend/yarn-debug.log*
|
||||
frontend/yarn-error.log*
|
||||
frontend/pnpm-debug.log*
|
||||
frontend/lerna-debug.log*
|
||||
|
||||
frontend/node_modules
|
||||
frontend/.DS_Store
|
||||
frontend/dist
|
||||
frontend/dist-ssr
|
||||
frontend/coverage
|
||||
frontend/*.local
|
||||
frontend/README.md
|
||||
|
||||
frontend/cypress/videos/
|
||||
frontend/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
frontend/.vscode/*
|
||||
frontend/!.vscode/extensions.json
|
||||
frontend/.idea
|
||||
frontend/*.suo
|
||||
frontend/*.ntvs*
|
||||
frontend/*.njsproj
|
||||
frontend/*.sln
|
||||
frontend/*.sw?
|
||||
|
||||
frontend/*.tsbuildinfo
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM python:3.11
|
||||
|
||||
# Links Docker image with repository
|
||||
LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/gearguardian
|
||||
LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/endurain
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
@@ -17,26 +17,33 @@ RUN pip install --no-cache-dir --upgrade -r requirements.txt \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Make port 80 available to the world outside this container
|
||||
EXPOSE 80
|
||||
|
||||
# Copy the directory backend contents to /app
|
||||
COPY backend /app
|
||||
|
||||
# Define environment variable
|
||||
ENV DB_HOST=""
|
||||
# Make port 80 available to the world outside this container
|
||||
EXPOSE 80
|
||||
|
||||
# Define environment variables
|
||||
ENV DB_HOST="mariadb"
|
||||
ENV DB_PORT=3306
|
||||
ENV DB_USER=""
|
||||
ENV DB_PASSWORD=""
|
||||
ENV DB_DATABASE=""
|
||||
ENV SECRET_KEY=""
|
||||
ENV DB_USER="endurain"
|
||||
ENV DB_PASSWORD="changeme"
|
||||
ENV DB_DATABASE="endurain"
|
||||
ENV SECRET_KEY="changeme"
|
||||
ENV ALGORITHM="HS256"
|
||||
ENV ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
ENV STRAVA_CLIENT_ID=""
|
||||
ENV STRAVA_CLIENT_SECRET=""
|
||||
ENV STRAVA_AUTH_CODE=""
|
||||
ENV JAEGER_HOST=""
|
||||
ENV STRAVA_CLIENT_ID="changeme"
|
||||
ENV STRAVA_CLIENT_SECRET="changeme"
|
||||
ENV STRAVA_AUTH_CODE="changeme"
|
||||
ENV JAEGER_ENABLED="true"
|
||||
ENV JAEGER_HOST="jaeger"
|
||||
ENV JAEGER_PROTOCOL="http"
|
||||
ENV JAGGER_PORT=4317
|
||||
ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30
|
||||
ENV FRONTEND_PROTOCOL="http"
|
||||
ENV FRONTEND_HOST="frontend"
|
||||
ENV FRONTEND_PORT=8080
|
||||
ENV GEOCODES_MAPS_API="changeme"
|
||||
|
||||
# Run main.py when the container launches
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
@@ -1,38 +1,34 @@
|
||||
# Use an official PHP runtime as a parent image
|
||||
FROM php:8.3-apache
|
||||
# Use an official node runtime as a parent image
|
||||
FROM node:20-alpine as build-stage
|
||||
|
||||
# Links Docker image with repository
|
||||
LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/gearguardian
|
||||
LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/endurain
|
||||
|
||||
# Set the working directory to /var/www/html
|
||||
WORKDIR /var/www/html
|
||||
# Set the working directory to /app
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the current directory contents into the container at /var/www/html
|
||||
COPY frontend/ /var/www/html
|
||||
# Copy package.json and package-lock.json
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy custom php.ini
|
||||
COPY custom_php.ini /usr/local/etc/php/php.ini
|
||||
# Copy the current directory contents into the container at /app
|
||||
COPY frontend ./
|
||||
|
||||
# Install any dependencies your application needs
|
||||
RUN apt-get update
|
||||
# Build the app
|
||||
RUN npm run build
|
||||
|
||||
# Change ownership of the directory to www-data:www-data
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
# Use nginx to serve the built app
|
||||
FROM nginx:alpine as production-stage
|
||||
|
||||
# Change permissions of the directory to 755
|
||||
RUN chmod -R 755 /var/www/html
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx-custom.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY frontend_env.sh /docker-entrypoint.d/frontend_env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/frontend_env.sh
|
||||
|
||||
# Expose port 80 to the outside world
|
||||
EXPOSE 80
|
||||
|
||||
# Define environment variables
|
||||
ENV APACHE_DOCUMENT_ROOT /var/www/html
|
||||
ENV MY_APP_BACKEND_PROTOCOL=http
|
||||
ENV MY_APP_BACKEND_HOST=localhost:98
|
||||
|
||||
# Enable Apache modules
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# Update the default virtual host to use the environment variable
|
||||
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
|
||||
|
||||
# Start Apache
|
||||
CMD ["apache2-foreground"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
107
README.md
@@ -1,8 +1,10 @@
|
||||
<div align="center">
|
||||
<img src="frontend/img/logo/logo.png" width="128" height="128">
|
||||
<img src="frontend/public/logo/logo.png" width="128" height="128">
|
||||
|
||||
# Endurain
|
||||
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/endurain"><img src="https://badges.crowdin.net/endurain/localized.svg"></a>
|
||||
|
||||
A self-hosted fitness tracking service • Endurain <a href="https://fosstodon.org/@endurain">Mastodon</a> profile
|
||||
|
||||
<img src="screenshot_01.png">
|
||||
@@ -11,7 +13,7 @@
|
||||
> [!WARNING]
|
||||
> This project is currently in **Alpha** state. You can try it out at your own risk, but be aware that things might break and **DATA LOSS** may occur.
|
||||
|
||||
Endurain is a self-hosted fitness tracking service that operates much like Strava but allows users to have complete control over their data and the hosting environment. The application's frontend is built using a combination of PHP, HTML, basic JavaScript, and Bootstrap CSS. On the backend, it leverages Python FastAPI, Alembic, SQLAlchemy, stravalib and gpxpy for seamless integration with Strava and .gpx file import. The MariaDB database engine is employed to efficiently store and manage user data, while Jaeger is used for basic observability.
|
||||
Endurain is a self-hosted fitness tracking service that operates much like Strava but allows users to have complete control over their data and the hosting environment. The application's frontend is built using Vue.js and Bootstrap CSS. On the backend, it leverages Python FastAPI, Alembic, SQLAlchemy, stravalib and gpxpy for seamless integration with Strava and .gpx file import. The MariaDB database engine is employed to efficiently store and manage user data, while Jaeger is used for basic observability.
|
||||
|
||||
To deploy Endurain, Docker images are available, and a comprehensive example can be found in the "docker-compose.yml" file provided. Configuration is facilitated through environment variables, ensuring flexibility and ease of customization.
|
||||
|
||||
@@ -55,76 +57,77 @@ More screenshots: https://imgur.com/a/lDR0sBf
|
||||
# Frontend
|
||||
Table bellow shows supported environemnt variables. Variables marked with optional "No" should be set to avoid errors.
|
||||
|
||||
Environemnt variable | Default value | Optional
|
||||
--- | --- | ---
|
||||
BACKEND_PROTOCOL* | http | Yes
|
||||
BACKEND_HOST** | backend | Yes
|
||||
|
||||
*BACKEND_PROTOCOL needs to be https if you want to enable Strava integration
|
||||
**BACKEND_HOST needs to be set and be Internet faced/resolved if you want to enable Strava integration. Strava callback relies on this.
|
||||
Environemnt variable | Default value | Optional | Notes
|
||||
--- | --- | --- | ---
|
||||
MY_APP_BACKEND_PROTOCOL | http | Yes | Needs to be https if you want to enable Strava integration. You may need to update this variable based on docker image spin up
|
||||
MY_APP_BACKEND_HOST | localhost:98 | Yes | Needs to be set and be Internet faced/resolved if you want to enable Strava integration. Strava callback relies on this. You may need to update this variable based on docker image spin up
|
||||
|
||||
Frontend dependencies:
|
||||
- php:8.3-apache
|
||||
- vue@3.4.24
|
||||
- vue-router@4.3.2
|
||||
- vue-i18n@9.13.1
|
||||
- vite@5.2.10
|
||||
- pinia@2.1.7
|
||||
- crypto-js@4.2.0
|
||||
- chart.js@4.4.2
|
||||
- User avatars create using DiceBear (https://www.dicebear.com) avataaars style.
|
||||
- Bootstrap CSS v5.3.2
|
||||
- leaflet v1.7.1
|
||||
- fontawesome icons free version
|
||||
- Bootstrap CSS v5.3.3
|
||||
- leaflet v1.9.4
|
||||
- fontawesome icons free version@6.5.2 and vue-fontawesome@3.0.6
|
||||
- Logo created using Canvas
|
||||
|
||||
---
|
||||
# Backend
|
||||
Table bellow shows supported environemnt variables. Variables marked with optional "No" should be set to avoid errors.
|
||||
|
||||
Environemnt variable | Default value | Optional
|
||||
--- | --- | ---
|
||||
DB_HOST | mariadb | Yes
|
||||
DB_PORT | 3306 | Yes
|
||||
DB_USER | gearguardian | Yes
|
||||
DB_PASSWORD | changeme | `No`
|
||||
DB_DATABASE | gearguardian | Yes
|
||||
SECRET_KEY | changeme | `No`
|
||||
ALGORITHM | HS256 | Yes
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES | 30 | Yes
|
||||
STRAVA_CLIENT_ID | changeme | `No`
|
||||
STRAVA_CLIENT_SECRET | changeme | `No`
|
||||
STRAVA_AUTH_CODE | changeme | `No`
|
||||
JAEGER_ENABLED | true | Yes
|
||||
JAEGER_PROTOCOL | http | Yes
|
||||
JAEGER_HOST | jaeger | Yes
|
||||
JAGGER_PORT | 4317 | Yes
|
||||
STRAVA_DAYS_ACTIVITIES_ONLINK | 30 | Yes
|
||||
FRONTEND_HOST* | frontend | Yes
|
||||
GEOCODES_MAPS_API** | changeme | `No`
|
||||
|
||||
*FRONTEND_HOST needs to be set if you want to enable Strava integration
|
||||
**<a href="https://geocode.maps.co/">Geocode maps</a> offers a free plan consisting of 1 Request/Second. Registration necessary.
|
||||
Environemnt variable | Default value | Optional | Notes
|
||||
--- | --- | --- | ---
|
||||
DB_HOST | mariadb | Yes | N/A
|
||||
DB_PORT | 3306 | Yes | N/A
|
||||
DB_USER | gearguardian | Yes | N/A
|
||||
DB_PASSWORD | changeme | `No` | N/A
|
||||
DB_DATABASE | gearguardian | Yes | N/A
|
||||
SECRET_KEY | changeme | `No` | N/A
|
||||
ALGORITHM | HS256 | Yes | N/A
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES | 30 | Yes | N/A
|
||||
STRAVA_CLIENT_ID | changeme | `No` | N/A
|
||||
STRAVA_CLIENT_SECRET | changeme | `No` | N/A
|
||||
STRAVA_AUTH_CODE | changeme | `No` | N/A
|
||||
JAEGER_ENABLED | true | Yes | N/A
|
||||
JAEGER_PROTOCOL | http | Yes | N/A
|
||||
JAEGER_HOST | jaeger | Yes | N/A
|
||||
JAGGER_PORT | 4317 | Yes | N/A
|
||||
STRAVA_DAYS_ACTIVITIES_ONLINK | 30 | Yes | N/A
|
||||
FRONTEND_PROTOCOL | http | Yes | Needs to be set if you want to enable Strava integration. You may need to update this variable based on docker image spin up
|
||||
FRONTEND_HOST | frontend | Yes | Needs to be set if you want to enable Strava integration. You may need to update this variable based on docker image spin up
|
||||
FRONTEND_PORT | frontend | Yes | Needs to be set if you want to enable Strava integration. You may need to update this variable based on docker image spin up
|
||||
GEOCODES_MAPS_API | changeme | `No` | <a href="https://geocode.maps.co/">Geocode maps</a> offers a free plan consisting of 1 Request/Second. Registration necessary.
|
||||
|
||||
Table bellow shows the obligatory environemnt variables for mariadb container. You should set them based on what was also set for backend container.
|
||||
|
||||
Environemnt variable | Default value | Optional
|
||||
--- | --- | ---
|
||||
MYSQL_ROOT_PASSWORD | changeme | `No`
|
||||
MYSQL_DATABASE | gearguardian | `No`
|
||||
MYSQL_USER | gearguardian | `No`
|
||||
MYSQL_PASSWORD | changeme | `No`
|
||||
Environemnt variable | Default value | Optional | Notes
|
||||
--- | --- | --- | ---
|
||||
MYSQL_ROOT_PASSWORD | changeme | `No` | N/A
|
||||
MYSQL_DATABASE | gearguardian | `No` | N/A
|
||||
MYSQL_USER | gearguardian | `No` | N/A
|
||||
MYSQL_PASSWORD | changeme | `No` | N/A
|
||||
|
||||
Python backend dependencies used:
|
||||
- python:3.11
|
||||
- fastapi==0.108.0
|
||||
- pydantic==1.10.9
|
||||
- uvicorn==0.25.0
|
||||
- python-dotenv==1.0.0
|
||||
- sqlalchemy==2.0.25
|
||||
- mysqlclient==2.2.1
|
||||
- fastapi==0.111.0
|
||||
- pydantic==1.10.15
|
||||
- uvicorn==0.29.0
|
||||
- python-dotenv==1.0.1
|
||||
- sqlalchemy==2.0.30
|
||||
- mysqlclient==2.2.4
|
||||
- python-jose[cryptography]==3.3.0
|
||||
- passlib[bcrypt]==1.7.4
|
||||
- apscheduler==3.10.4
|
||||
- requests==2.31.0
|
||||
- stravalib==1.5
|
||||
- requests==2.32.2
|
||||
- stravalib==1.7
|
||||
- opentelemetry-sdk==1.22.0
|
||||
- opentelemetry-instrumentation-fastapi==0.43b0
|
||||
- opentelemetry.exporter.otlp==1.22.0
|
||||
- python-multipart==0.0.6
|
||||
- python-multipart==0.0.9
|
||||
- gpxpy==1.6.2
|
||||
- alembic==1.13.1
|
||||
|
||||
|
||||
19
backend/.env
@@ -1,19 +0,0 @@
|
||||
# .env
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_USER=endurain
|
||||
DB_PASSWORD=changeme
|
||||
DB_DATABASE=endurain
|
||||
SECRET_KEY=changeme # openssl rand -hex 32
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
STRAVA_CLIENT_ID=changeme
|
||||
STRAVA_CLIENT_SECRET=changeme
|
||||
STRAVA_AUTH_CODE=changeme
|
||||
JAEGER_ENABLED=true
|
||||
JAEGER_PROTOCOL=http
|
||||
JAEGER_HOST=jaeger
|
||||
JAGGER_PORT=4317
|
||||
STRAVA_DAYS_ACTIVITIES_ONLINK=30
|
||||
FRONTEND_HOST=frontend
|
||||
GEOCODES_MAPS_API=changeme
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Remove access_tokens table
|
||||
|
||||
Revision ID: 0ab200a7f196
|
||||
Revises: 5fd61bc55e09
|
||||
Create Date: 2024-05-24 13:39:50.917676
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0ab200a7f196'
|
||||
down_revision: Union[str, None] = '5fd61bc55e09'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# Drop the foreign key constraint first
|
||||
op.drop_constraint('access_tokens_ibfk_1', 'access_tokens', type_='foreignkey')
|
||||
# Then drop the index
|
||||
op.drop_index('ix_access_tokens_user_id', table_name='access_tokens')
|
||||
op.drop_table('access_tokens')
|
||||
op.alter_column('users_integrations', 'user_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
comment='User ID that the integration belongs',
|
||||
existing_comment='User ID that the token belongs',
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('users_integrations', 'user_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
comment='User ID that the token belongs',
|
||||
existing_comment='User ID that the integration belongs',
|
||||
existing_nullable=False)
|
||||
op.create_table('access_tokens',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('token', mysql.VARCHAR(length=256), nullable=False, comment='User token'),
|
||||
sa.Column('user_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False, comment='User ID that the token belongs'),
|
||||
sa.Column('created_at', mysql.DATETIME(), nullable=False, comment='Token creation date (date)'),
|
||||
sa.Column('expires_at', mysql.DATETIME(), nullable=False, comment='Token expiration date (date)'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='access_tokens_ibfk_1', ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
# Recreate the index first
|
||||
op.create_index('ix_access_tokens_user_id', 'access_tokens', ['user_id'])
|
||||
# Then recreate the foreign key constraint
|
||||
op.create_foreign_key('access_tokens_ibfk_1', 'access_tokens', 'users', ['user_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Add cascade delete to followers foreign keys
|
||||
|
||||
Revision ID: 5fd61bc55e09
|
||||
Revises: 1bce2bd27873
|
||||
Create Date: 2024-05-21 20:59:54.984566
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5fd61bc55e09'
|
||||
down_revision: Union[str, None] = '1bce2bd27873'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('followers_ibfk_2', 'followers', type_='foreignkey')
|
||||
op.drop_constraint('followers_ibfk_1', 'followers', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'followers', 'users', ['following_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key(None, 'followers', 'users', ['follower_id'], ['id'], ondelete='CASCADE')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'followers', type_='foreignkey')
|
||||
op.drop_constraint(None, 'followers', type_='foreignkey')
|
||||
op.create_foreign_key('followers_ibfk_1', 'followers', 'users', ['follower_id'], ['id'])
|
||||
op.create_foreign_key('followers_ibfk_2', 'followers', 'users', ['following_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
|
||||
# Constant related to version
|
||||
API_VERSION="v0.1.5"
|
||||
API_VERSION="v0.2.0"
|
||||
|
||||
# JWT Token constants
|
||||
JWT_ALGORITHM = os.environ.get("ALGORITHM")
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import logging
|
||||
|
||||
import models
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Define a loggger created on main.py
|
||||
logger = logging.getLogger("myLogger")
|
||||
|
||||
|
||||
def get_acess_tokens_by_user_id(user_id: int, db: Session):
|
||||
try:
|
||||
access_tokens = (
|
||||
db.query(models.AccessToken)
|
||||
.filter(models.AccessToken.user_id == user_id)
|
||||
.all()
|
||||
)
|
||||
if access_tokens is None:
|
||||
# If the user was not found, return a 404 Not Found error
|
||||
return None
|
||||
|
||||
return access_tokens
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
logger.error(f"Error in get_acess_tokens_by_user_id: {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 create_access_token(token, db: Session):
|
||||
try:
|
||||
# Create a new access token in the database
|
||||
db_access_token = models.AccessToken(
|
||||
token=token.token,
|
||||
user_id=token.user_id,
|
||||
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
expires_at=token.expires_at,
|
||||
)
|
||||
|
||||
# Add the access token to the database and commit the transaction
|
||||
db.add(db_access_token)
|
||||
db.commit()
|
||||
db.refresh(db_access_token)
|
||||
|
||||
# return the access token
|
||||
return db_access_token
|
||||
except Exception as err:
|
||||
# Handle database-related exceptions
|
||||
db.rollback() # Rollback the transaction to maintain database consistency
|
||||
|
||||
# Log the exception
|
||||
logger.error(f"Error in create_access_token: {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_access_token(token: str, db: Session):
|
||||
try:
|
||||
# Delete the access token from the database
|
||||
db_access_token = (
|
||||
db.query(models.AccessToken)
|
||||
.filter(models.AccessToken.token == token)
|
||||
.delete()
|
||||
)
|
||||
|
||||
# Commit the transaction to the database
|
||||
if db_access_token:
|
||||
db.delete(db_access_token)
|
||||
db.commit()
|
||||
logger.info(f"{db_access_token} access tokens deleted from the database")
|
||||
return db_access_token
|
||||
else:
|
||||
# If the access token was not found, return a 404 Not Found error
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Access token not found",
|
||||
)
|
||||
except Exception as err:
|
||||
# Handle database-related exceptions
|
||||
db.rollback() # Rollback the transaction to maintain database consistency
|
||||
|
||||
# Log the exception
|
||||
logger.error(f"Error in delete_access_token: {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_access_tokens(expiration_time: str, db: Session):
|
||||
try:
|
||||
# Delete the access tokens from the database
|
||||
db_access_tokens = (
|
||||
db.query(models.AccessToken)
|
||||
.filter(models.AccessToken.created_at < expiration_time)
|
||||
.delete()
|
||||
)
|
||||
|
||||
# Commit the transaction to the database
|
||||
if db_access_tokens:
|
||||
db.commit()
|
||||
return db_access_tokens
|
||||
else:
|
||||
# If no access tokens were found, return 0
|
||||
return 0
|
||||
except Exception as err:
|
||||
# Handle database-related exceptions
|
||||
db.rollback() # Rollback the transaction to maintain database consistency
|
||||
|
||||
# Log the exception
|
||||
logger.error(f"Error in delete_access_tokens: {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
|
||||
@@ -5,6 +5,7 @@ from fastapi import HTTPException, status
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func, desc
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from urllib.parse import unquote
|
||||
|
||||
import models
|
||||
from schemas import schema_activities
|
||||
@@ -404,6 +405,46 @@ def get_activity_by_strava_id_from_user_id(
|
||||
) from err
|
||||
|
||||
|
||||
def get_activities_if_contains_name(
|
||||
name: str, user_id: int, db: Session
|
||||
):
|
||||
try:
|
||||
# Define a search term
|
||||
partial_name = unquote(name).replace("+", " ")
|
||||
|
||||
# Get the activities from the database
|
||||
activities = (
|
||||
db.query(models.Activity)
|
||||
.filter(
|
||||
models.Activity.user_id == user_id,
|
||||
models.Activity.name.like(f"%{partial_name}%"),
|
||||
)
|
||||
.order_by(desc(models.Activity.start_time))
|
||||
.all()
|
||||
)
|
||||
|
||||
# Check if there are activities if not return None
|
||||
if not activities:
|
||||
return None
|
||||
|
||||
# Iterate and format the dates
|
||||
for activity in activities:
|
||||
activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Return the activities
|
||||
return activities
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
logger.error(f"Error in get_activities_if_contains_name: {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 create_activity(activity: schema_activities.Activity, db: Session):
|
||||
try:
|
||||
# Create a new activity
|
||||
|
||||
@@ -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,33 @@ 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()
|
||||
|
||||
# Return the photo path
|
||||
return photo_path
|
||||
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 +444,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 +475,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()
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
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
|
||||
@@ -37,15 +39,13 @@ def startup_event():
|
||||
# Run Alembic migrations to ensure the database is up to date
|
||||
alembic_cfg = Config("alembic.ini")
|
||||
# Disable the logger configuration in Alembic to avoid conflicts with FastAPI
|
||||
alembic_cfg.attributes['configure_logger'] = False
|
||||
alembic_cfg.attributes["configure_logger"] = False
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
# Create a scheduler to run background jobs
|
||||
scheduler.start()
|
||||
|
||||
# Job to remove expired tokens every 5 minutes
|
||||
logger.info("Added scheduler job to remove expired tokens every 5 minutes")
|
||||
scheduler.add_job(remove_expired_tokens_job, "interval", minutes=5)
|
||||
# Add scheduler jobs to refresh Strava tokens and retrieve last day activities
|
||||
logger.info("Added scheduler job to refresh Strava user tokens every 60 minutes")
|
||||
scheduler.add_job(refresh_strava_tokens_job, "interval", minutes=60)
|
||||
logger.info(
|
||||
@@ -64,17 +64,6 @@ def shutdown_event():
|
||||
scheduler.shutdown()
|
||||
|
||||
|
||||
def remove_expired_tokens_job():
|
||||
# Create a new database session
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Remove expired tokens from the database
|
||||
schema_access_tokens.remove_expired_tokens(db=db)
|
||||
finally:
|
||||
# Ensure the session is closed after use
|
||||
db.close()
|
||||
|
||||
|
||||
def refresh_strava_tokens_job():
|
||||
# Create a new database session
|
||||
db = SessionLocal()
|
||||
@@ -121,7 +110,9 @@ required_env_vars = [
|
||||
"JAEGER_HOST",
|
||||
"JAGGER_PORT",
|
||||
"STRAVA_DAYS_ACTIVITIES_ONLINK",
|
||||
"FRONTEND_PROTOCOL",
|
||||
"FRONTEND_HOST",
|
||||
"FRONTEND_PORT",
|
||||
"GEOCODES_MAPS_API",
|
||||
]
|
||||
|
||||
@@ -147,6 +138,29 @@ 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")
|
||||
+ ":"
|
||||
+ os.environ.get("FRONTEND_PORT"),
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Router files
|
||||
app.include_router(router_session.router)
|
||||
app.include_router(router_users.router)
|
||||
|
||||
@@ -20,14 +20,14 @@ class Follower(Base):
|
||||
|
||||
follower_id = Column(
|
||||
Integer,
|
||||
ForeignKey("users.id"),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
comment="ID of the follower user",
|
||||
)
|
||||
following_id = Column(
|
||||
Integer,
|
||||
ForeignKey("users.id"),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
comment="ID of the following user",
|
||||
@@ -103,12 +103,6 @@ class User(Base):
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
# Define a relationship to AccessToken model
|
||||
access_tokens = relationship(
|
||||
"AccessToken",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
# Define a relationship to Gear model
|
||||
gear = relationship(
|
||||
"Gear",
|
||||
@@ -147,7 +141,7 @@ class UserIntegrations(Base):
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="User ID that the token belongs",
|
||||
comment="User ID that the integration belongs",
|
||||
)
|
||||
strava_state = Column(String(length=45), default=None, nullable=True)
|
||||
strava_token = Column(String(length=250), default=None, nullable=True)
|
||||
@@ -164,28 +158,6 @@ class UserIntegrations(Base):
|
||||
user = relationship("User", back_populates="users_integrations")
|
||||
|
||||
|
||||
# Data model for access_tokens table using SQLAlchemy's ORM
|
||||
class AccessToken(Base):
|
||||
__tablename__ = "access_tokens"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
token = Column(String(length=256), nullable=False, comment="User token")
|
||||
user_id = Column(
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="User ID that the token belongs",
|
||||
)
|
||||
created_at = Column(DateTime, nullable=False, comment="Token creation date (date)")
|
||||
expires_at = Column(
|
||||
DateTime, nullable=False, comment="Token expiration date (date)"
|
||||
)
|
||||
|
||||
# Define a relationship to the User model
|
||||
user = relationship("User", back_populates="access_tokens")
|
||||
|
||||
|
||||
# Data model for gear table using SQLAlchemy's ORM
|
||||
class Gear(Base):
|
||||
__tablename__ = "gear"
|
||||
|
||||
@@ -332,17 +332,35 @@ async def read_activities_activity_from_id(
|
||||
return crud_activities.get_activity_by_id_from_user_id_or_has_visibility(activity_id, token_user_id, db)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/activities/name/contains/{name}",
|
||||
response_model=list[schema_activities.Activity] | None,
|
||||
tags=["activities"],
|
||||
)
|
||||
async def read_activities_contain_name(
|
||||
name: str,
|
||||
token_user_id: Annotated[
|
||||
Callable,
|
||||
Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
|
||||
],
|
||||
db: Session = Depends(dependencies_database.get_db),
|
||||
):
|
||||
# Get the activities from the database by name
|
||||
return crud_activities.get_activities_if_contains_name(name, token_user_id, db)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/activities/{user_id}/create/upload",
|
||||
"/activities/create/upload",
|
||||
status_code=201,
|
||||
response_model=int,
|
||||
tags=["activities"],
|
||||
)
|
||||
async def create_activity_with_uploaded_file(
|
||||
user_id: int,
|
||||
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
|
||||
token_user_id: Annotated[
|
||||
Callable,
|
||||
Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
|
||||
],
|
||||
file: UploadFile,
|
||||
validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)],
|
||||
db: Session = Depends(dependencies_database.get_db),
|
||||
):
|
||||
try:
|
||||
@@ -360,10 +378,10 @@ async def create_activity_with_uploaded_file(
|
||||
# Choose the appropriate parser based on file extension
|
||||
if file_extension.lower() == ".gpx":
|
||||
# Parse the GPX file
|
||||
parsed_info = gpx_processor.parse_gpx_file(file.filename, user_id)
|
||||
parsed_info = gpx_processor.parse_gpx_file(file.filename, token_user_id)
|
||||
elif file_extension.lower() == ".fit":
|
||||
# Parse the FIT file
|
||||
parsed_info = fit_processor.parse_fit_file(file.filename, user_id)
|
||||
parsed_info = fit_processor.parse_fit_file(file.filename, token_user_id)
|
||||
else:
|
||||
# file extension not supported raise an HTTPException with a 406 Not Acceptable status code
|
||||
raise HTTPException(
|
||||
|
||||
@@ -80,7 +80,7 @@ async def strava_link(
|
||||
redirect_url = (
|
||||
"https://"
|
||||
+ os.environ.get("FRONTEND_HOST")
|
||||
+ "/settings/settings.php?integrationsSettings=1&stravaLinked=1"
|
||||
+ "/settings?stravaLinked=1"
|
||||
)
|
||||
|
||||
# Return a RedirectResponse to the redirect URL
|
||||
|
||||
@@ -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=str | 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)
|
||||
|
||||
return 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,
|
||||
|
||||
@@ -7,7 +7,6 @@ from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from crud import crud_access_tokens
|
||||
from constants import (
|
||||
JWT_EXPIRATION_IN_MINUTES,
|
||||
JWT_ALGORITHM,
|
||||
@@ -69,9 +68,10 @@ def validate_token_expiration(db: Session, token: str = Depends(oauth2_scheme)):
|
||||
or datetime.utcfromtimestamp(expiration_timestamp) < datetime.utcnow()
|
||||
):
|
||||
logger.warning(
|
||||
"Token expired | Will force remove_expired_tokens to run | Returning 401 response"
|
||||
"Token expired | Returning 401 response"
|
||||
)
|
||||
remove_expired_tokens(db)
|
||||
|
||||
# Raise an HTTPException with a 401 Unauthorized status code
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token no longer valid",
|
||||
@@ -80,10 +80,9 @@ def validate_token_expiration(db: Session, token: str = Depends(oauth2_scheme)):
|
||||
except Exception:
|
||||
# Log the error and raise the exception
|
||||
logger.info(
|
||||
"Token expired during validation | Will force remove_expired_tokens to run | Returning 401 response"
|
||||
"Token expired during validation | Returning 401 response"
|
||||
)
|
||||
# Remove expired tokens from the database
|
||||
remove_expired_tokens(db)
|
||||
|
||||
# Raise an HTTPException with a 401 Unauthorized status code
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -159,29 +158,5 @@ def create_access_token(
|
||||
# Encode the data and return the token
|
||||
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
|
||||
|
||||
# Save the token in the database
|
||||
db_access_token = crud_access_tokens.create_access_token(
|
||||
CreateToken(
|
||||
token=encoded_jwt,
|
||||
user_id=data.get("id"),
|
||||
expires_at=expire.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
),
|
||||
db,
|
||||
)
|
||||
if db_access_token:
|
||||
# Return the token
|
||||
return encoded_jwt
|
||||
else:
|
||||
# If the token could not be saved in the database return None
|
||||
return None
|
||||
|
||||
|
||||
def remove_expired_tokens(db: Session):
|
||||
# Calculate the expiration time
|
||||
expiration_time = datetime.utcnow() - timedelta(minutes=JWT_EXPIRATION_IN_MINUTES)
|
||||
|
||||
# Delete the expired tokens from the database
|
||||
rows_deleted = crud_access_tokens.delete_access_tokens(expiration_time, db)
|
||||
|
||||
# Log the number of tokens deleted
|
||||
logger.info(f"{rows_deleted} access tokens deleted from the database")
|
||||
# Return the token
|
||||
return encoded_jwt
|
||||
|
||||
0
backend/user_images/__init__.py
Normal file
@@ -1,18 +0,0 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
http:
|
||||
|
||||
processors:
|
||||
batch:
|
||||
|
||||
exporters:
|
||||
jaeger:
|
||||
endpoint: "jaeger:14268" # Use the service name defined in your Jaeger service
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [jaeger]
|
||||
7
crowdin.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
files:
|
||||
- source: /frontend_vue/src/i18n/en/*.json
|
||||
translation: /frontend_vue/src/i18n/%two_letters_code%/%file_name%.%file_extension%
|
||||
- source: /frontend_vue/src/i18n/en/components/*.json
|
||||
translation: /frontend_vue/src/i18n/%two_letters_code%/components/%file_name%.%file_extension%
|
||||
- source: /frontend_vue/src/i18n/en/gears/*.json
|
||||
translation: /frontend_vue/src/i18n/%two_letters_code%/gears/%file_name%.%file_extension%
|
||||
@@ -4,16 +4,14 @@ services:
|
||||
frontend:
|
||||
container_name: frontend
|
||||
image: ghcr.io/joaovitoriasilva/endurain/frontend:latest
|
||||
environment:
|
||||
- BACKEND_PROTOCOL=https # http or https, default is http
|
||||
- BACKEND_HOST=backend # api host, default is backend
|
||||
#environment:
|
||||
#- MY_APP_BACKEND_PROTOCOL=http # http or https, default is http
|
||||
#- MY_APP_BACKEND_HOST=localhost:98 # api host or local ip (example: 192.168.1.10:98), default is localhost:98
|
||||
# Configure volume if you want to edit the code locally by clomming the repo
|
||||
#volumes:
|
||||
# - <local_path>/endurain/frontend:/var/www/html
|
||||
# - <local_path>/endurain/frontend:/app
|
||||
ports:
|
||||
- "8080:80" # frontend port, change per your needs
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
|
||||
# API logic
|
||||
@@ -26,14 +24,15 @@ services:
|
||||
- STRAVA_CLIENT_ID=changeme
|
||||
- STRAVA_CLIENT_SECRET=changeme
|
||||
- STRAVA_AUTH_CODE=changeme
|
||||
- FRONTEND_HOST=frontend # default is frontend
|
||||
- GEOCODES_MAPS_API=changeme
|
||||
- FRONTEND_PROTOCOL=http # default is http
|
||||
- FRONTEND_HOST=frontend # frontend host or local ip (example: 192.168.1.10:8080), 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
|
||||
#volumes:
|
||||
# - <local_path>/endurain/backend:/app
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- mariadb
|
||||
- jaeger
|
||||
|
||||
5
frontend/.env
Executable file → Normal file
@@ -1,3 +1,2 @@
|
||||
# .env
|
||||
BACKEND_PROTOCOL=http
|
||||
BACKEND_HOST=backend
|
||||
VITE_BACKEND_PROTOCOL=MY_APP_BACKEND_PROTOCOL
|
||||
VITE_BACKEND_HOST=MY_APP_BACKEND_HOST
|
||||
14
frontend/.eslintrc.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
33
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
8
frontend/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
8
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
@@ -1,973 +0,0 @@
|
||||
<?php
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
|
||||
$page = "activity";
|
||||
$activity = [];
|
||||
$addGearToActivity = -9000;
|
||||
$editGearActivity = -9000;
|
||||
$deleteGearActivity = -9000;
|
||||
$deleteActivity = -9000;
|
||||
|
||||
if (!isLogged()) {
|
||||
header("Location: ../login.php");
|
||||
die();
|
||||
}
|
||||
|
||||
if (!isTokenValid($_SESSION["token"])) {
|
||||
header("Location: ../logout.php?sessionExpired=1");
|
||||
die();
|
||||
}
|
||||
|
||||
if (!isset($_GET["activityID"])) {
|
||||
header("Location: ../index.php?invalidActivity=1");
|
||||
die();
|
||||
}
|
||||
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsActivitiesActivity = include $_SERVER['DOCUMENT_ROOT'] . '/lang/activities/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsActivitiesActivity = include $_SERVER['DOCUMENT_ROOT'] . '/lang/activities/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsActivitiesActivity = include $_SERVER['DOCUMENT_ROOT'] . '/lang/activities/en.php';
|
||||
}
|
||||
|
||||
/* Add gear to activity action */
|
||||
if (isset($_GET["addGearToActivity"]) && $_GET["addGearToActivity"] == 1) {
|
||||
$addGearToActivity = addGearToActivity($_GET["activityID"], $_POST["gearIDAdd"]);
|
||||
}
|
||||
|
||||
/* Edit activity gear */
|
||||
if (isset($_GET["editGearActivity"]) && $_GET["editGearActivity"] == 1) {
|
||||
$editGearActivity = addGearToActivity($_GET["activityID"], $_POST["gearIDEdit"]);
|
||||
}
|
||||
|
||||
/* Delete activity gear */
|
||||
if (isset($_GET["deleteGearActivity"]) && $_GET["deleteGearActivity"] == 1) {
|
||||
$deleteGearActivity = unsetActivityGear($_GET["activityID"]);
|
||||
}
|
||||
|
||||
/* Delete activity */
|
||||
if (isset($_GET["deleteActivity"]) && $_GET["deleteActivity"] == 1) {
|
||||
$deleteActivity = deleteActivity($_GET["activityID"]);
|
||||
if ($deleteActivity == 0) {
|
||||
header("Location: ../index.php?deleteActivity=1");
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
$activity = getActivityFromId($_GET["activityID"]);
|
||||
if ($activity == NULL) {
|
||||
header("Location: ../index.php?invalidActivity=1");
|
||||
die();
|
||||
}
|
||||
$activityStreams = getActivityActivitiesStream($activity["id"]);
|
||||
$hrStream = [];
|
||||
$cadStream = [];
|
||||
$powerStream = [];
|
||||
$eleStream = [];
|
||||
$velStream = [];
|
||||
$paceStream = [];
|
||||
foreach ($activityStreams as $stream) {
|
||||
if($stream["stream_type"] == 1){
|
||||
$hrStream = $stream["stream_waypoints"];
|
||||
}
|
||||
if($stream["stream_type"] == 2){
|
||||
$powerStream = $stream["stream_waypoints"];
|
||||
}
|
||||
if($stream["stream_type"] == 3){
|
||||
$cadStream = $stream["stream_waypoints"];
|
||||
}
|
||||
if($stream["stream_type"] == 4){
|
||||
$eleStream = $stream["stream_waypoints"];
|
||||
}
|
||||
if($stream["stream_type"] == 5){
|
||||
#$velStream = $stream["stream_waypoints"];
|
||||
foreach($stream["stream_waypoints"] as $velData){
|
||||
$velStream[] = (float) number_format(($velData['vel'] * 3.6), 0);
|
||||
}
|
||||
}
|
||||
#if($stream["stream_type"] == 6){
|
||||
# $paceStream = $stream["stream_waypoints"];
|
||||
#}
|
||||
if($stream["stream_type"] == 7){
|
||||
$latlonStream = $stream["stream_waypoints"];
|
||||
}
|
||||
#$velStream[] = (float) number_format(($waypoint['vel'] * 3.6), 0);
|
||||
if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) {
|
||||
if($stream["stream_type"] == 6){
|
||||
foreach($stream["stream_waypoints"] as $paceData){
|
||||
if ($paceData['pace'] == 0 || $paceData['pace'] == null) {
|
||||
$paceStream[] = 0;
|
||||
} else {
|
||||
$paceStream[] = ($paceData["pace"] * 1000) / 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($activity["activity_type"] == 9) {
|
||||
if($stream["stream_type"] == 6){
|
||||
foreach($stream["stream_waypoints"] as $paceData){
|
||||
if ($paceData['pace'] == 0 || $paceData['pace'] == null) {
|
||||
$paceStream[] = 0;
|
||||
} else {
|
||||
$paceStream[] = ($paceData["pace"] * 100) / 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$activityUser = getUserFromId($activity['user_id']);
|
||||
|
||||
if ($activity["gear_id"] != null) {
|
||||
$activityGear = getGearFromId($activity["gear_id"]);
|
||||
}
|
||||
|
||||
if($activityUser["id"] == $_SESSION["id"]){
|
||||
if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) {
|
||||
$activityGearOptions = getGearFromType(2);
|
||||
} else {
|
||||
if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7) {
|
||||
$activityGearOptions = getGearFromType(1);
|
||||
} else {
|
||||
if ($activity["activity_type"] == 8 || $activity["activity_type"] == 9) {
|
||||
$activityGearOptions = getGearFromType(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Top.php" ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- Error banners -->
|
||||
<?php if ($addGearToActivity == -1 || $addGearToActivity == -2 || $editGearActivity == -1 || $editGearActivity == -2 || $deleteGearActivity == -1 || $deleteGearActivity == -2 || $deleteActivity == -1 || $deleteActivity == -2) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($addGearToActivity == -1 || $editGearActivity == -1 || $deleteActivity == -1) { ?>
|
||||
API ERROR |
|
||||
<?php echo $translationsActivitiesActivity['activity_addEditGear_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($addGearToActivity == -2 || $editGearActivity == -2 || $deleteActivity == -2) { ?>
|
||||
API ERROR |
|
||||
<?php echo $translationsActivitiesActivity['activity_addEditGear_API_error_-2']; ?> (-2).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($addGearToActivity == 0 || $editGearActivity == 0 || $deleteGearActivity == 0) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($addGearToActivity == 0) { ?>
|
||||
<?php echo $translationsActivitiesActivity['activity_success_gearAdded']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if ($editGearActivity == 0) { ?>
|
||||
<?php echo $translationsActivitiesActivity['activity_success_gearEdited']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if ($deleteGearActivity == 0) { ?>
|
||||
<?php echo $translationsActivitiesActivity['activity_success_gearDeleted']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<!-- Activity user details -->
|
||||
<div class="d-flex align-items-center">
|
||||
<img src=<?php if (is_null($activityUser["photo_path"])) {
|
||||
if ($activityUser["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($activityUser["photo_path"]);
|
||||
} ?> alt="userPicture" class="rounded-circle" width="55" height="55">
|
||||
<div class="ms-3 me-3">
|
||||
<div class="fw-bold">
|
||||
<a href="../users/user.php?userID=<?php echo ($activityUser["id"]); ?>">
|
||||
<?php echo ($activityUser["name"]); ?>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Activity time and place details -->
|
||||
<h7>
|
||||
<?php if ($activity["activity_type"] == 1) {
|
||||
echo '<i class="fa-solid fa-person-running"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7 || $activity["activity_type"] == 8) {
|
||||
echo '<i class="fa-solid fa-person-biking"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 9) {
|
||||
echo '<i class="fa-solid fa-person-swimming"></i>';
|
||||
}
|
||||
}
|
||||
} ?>
|
||||
<?php echo (new DateTime($activity["start_time"]))->format("d/m/y"); ?>@
|
||||
<?php echo (new DateTime($activity["start_time"]))->format("H:i"); ?>
|
||||
<?php if (isset($activity["city"]) || isset($activity["country"])) {
|
||||
echo " - ";
|
||||
} ?>
|
||||
<?php if (isset($activity["city"]) && !empty($activity["city"])) {
|
||||
echo $activity["city"] . ", ";
|
||||
} ?>
|
||||
<?php if (isset($activity["country"]) && !empty($activity["country"])) {
|
||||
echo $activity["country"];
|
||||
} ?>
|
||||
</h7>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-flex">
|
||||
<?php if (isset($activity['strava_activity_id'])) { ?>
|
||||
<a class="btn btn-link btn-lg mt-1" href="https://www.strava.com/activities/<?php echo $activity['strava_activity_id']; ?>" role="button">
|
||||
<i class="fa-brands fa-strava"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<button class="btn btn-link btn-lg" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<!--<?php if (isset($activity['strava_activity_id'])) { ?>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="https://www.strava.com/activities/<?php echo $activity['strava_activity_id']; ?>">
|
||||
<?php echo $translationsActivitiesActivity['activity_title_dropdown_seeItOnStrava']; ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>-->
|
||||
<li>
|
||||
<a class="dropdown-item <?php if(isset($activity["strava_activity_id"])){ echo "disabled"; } ?>" href="#" data-bs-toggle="modal" data-bs-target="#deleteActivityModal">
|
||||
<?php echo $translationsActivitiesActivity['activity_title_dropdown_deleteActivity']; ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal delete gear -->
|
||||
<div class="modal fade" id="deleteActivityModal" tabindex="-1" aria-labelledby="deleteActivityModal"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteActivityModal">
|
||||
<?php echo $translationsActivitiesActivity['activity_title_dropdown_deleteActivity']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php echo $translationsActivitiesActivity['activity_title_dropdown_deleteActivity_modal_body']; ?>
|
||||
<b>
|
||||
<?php echo $activity['name']; ?>
|
||||
</b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<a type="button" class="btn btn-danger"
|
||||
href="../activities/activity.php?activityID=<?php echo $activity["id"]; ?>&deleteActivity=1">
|
||||
<?php echo $translationsActivitiesActivity['activity_title_dropdown_deleteActivity']; ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity title -->
|
||||
<h1 class="mt-3">
|
||||
<?php echo $activity["name"]; ?>
|
||||
</h1>
|
||||
|
||||
<!-- Details -->
|
||||
<div class=" row d-flex mt-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_distance']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if ($activity["activity_type"] != 9) { ?>
|
||||
<?php echo number_format(($activity["distance"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
<?php echo ($activity["distance"]); ?> m
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_time']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php
|
||||
$startDateTime = new DateTime($activity["start_time"]);
|
||||
$endDateTime = new DateTime($activity["end_time"]);
|
||||
$interval = $startDateTime->diff($endDateTime);
|
||||
|
||||
if ($interval->h < 1) {
|
||||
// If the difference is less than one hour
|
||||
echo $interval->i . "m " . $interval->s . "s";
|
||||
} else {
|
||||
// If the difference is one hour or more
|
||||
echo $interval->h . "h " . $interval->i . "m";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<?php if ($activity["activity_type"] != 9 && $activity["activity_type"] != 1) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_elevationGain']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo ($activity["elevation_gain"]); ?> m
|
||||
<?php } else { ?>
|
||||
<?php if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_pace']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo floor(($activity["pace"] * 1000) / 60) . ":" . number_format((((($activity["pace"] * 1000) / 60) - floor(($activity["pace"] * 1000) / 60)) * 60), 0); ?>
|
||||
min/km
|
||||
<?php } else { ?>
|
||||
<?php if ($activity["activity_type"] == 9) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_pace']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo floor(($activity["pace"] * 100) / 60) . ":" . number_format((((($activity["pace"] * 100) / 60) - floor(($activity["pace"] * 100) / 60)) * 60), 0); ?>
|
||||
min/100m
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- other metrics -->
|
||||
<div class="row d-flex mt-3">
|
||||
<?php if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) { ?>
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_avgPower']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if ($activity["average_power"]) {
|
||||
echo ($activity["average_power"]); ?> W
|
||||
<?php } else {
|
||||
echo "No data";
|
||||
} ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_elevationGain']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo ($activity["elevation_gain"]); ?> m
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_elevationLoss']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo ($activity["elevation_loss"]); ?> m
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($activity["activity_type"] != 9 && $activity["activity_type"] != 1) { ?>
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_avgSpeed']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo (number_format($activity["average_speed"] * 3.6)); ?> km/h
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_avgPower']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if ($activity["average_power"]) {
|
||||
echo ($activity["average_power"]); ?> W
|
||||
<?php } else {
|
||||
echo "No data";
|
||||
} ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_detail_elevationLoss']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo $activity["elevation_loss"]; ?> m
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div>
|
||||
|
||||
<!-- Map -->
|
||||
<?php if (isset($latlonStream)) { ?>
|
||||
<div class="mt-3 mb-3" id="map" style="height: 500px"></div>
|
||||
<?php } ?>
|
||||
|
||||
<!--<?php if (isset($activity['strava_activity_id'])) { ?>
|
||||
<?php if (!isset($latlonStream)) { ?>
|
||||
<br>
|
||||
<?php } ?>
|
||||
<a href="https://www.strava.com/activities/<?php echo $activity['strava_activity_id']; ?>">
|
||||
<?php echo $translationsActivitiesActivity['activity_title_dropdown_seeItOnStrava']; ?>
|
||||
</a>
|
||||
<?php } ?>-->
|
||||
|
||||
|
||||
<script>
|
||||
// JavaScript code to create the map for this activity
|
||||
var waypoints = <?php echo json_encode($latlonStream); ?>;
|
||||
var mapId = "map";
|
||||
|
||||
var map = L.map(mapId, {
|
||||
//dragging: false, // Disable panning
|
||||
//touchZoom: false, // Disable touch zoom
|
||||
//scrollWheelZoom: false, // Disable scroll wheel zoom
|
||||
//zoomControl: false // Remove zoom control buttons
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
var latlngs = waypoints.map(function (waypoint) {
|
||||
return [waypoint.lat, waypoint.lon];
|
||||
});
|
||||
|
||||
L.polyline(latlngs, {
|
||||
color: 'blue'
|
||||
}).addTo(map);
|
||||
|
||||
// Calculate the bounds of the polyline and fit the map to those bounds
|
||||
var bounds = L.latLngBounds(latlngs);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
// Add green dot for the first waypoint
|
||||
L.marker([waypoints[0].lat, waypoints[0].lon], {
|
||||
icon: L.divIcon({
|
||||
className: 'bg-success dot'
|
||||
})
|
||||
}).addTo(map);
|
||||
|
||||
// Add red dot for the last waypoint
|
||||
L.marker([waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].lon], {
|
||||
icon: L.divIcon({
|
||||
className: 'bg-danger dot'
|
||||
})
|
||||
}).addTo(map);
|
||||
</script>
|
||||
|
||||
<!-- gear -->
|
||||
<hr class="mb-2 mt-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<p class="pt-2">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_title']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if ($activity["activity_type"] == 1) {
|
||||
echo '<i class="fa-solid fa-person-running"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7 || $activity["activity_type"] == 8) {
|
||||
echo '<i class="fa-solid fa-person-biking"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 9) {
|
||||
echo '<i class="fa-solid fa-person-swimming"></i>';
|
||||
}
|
||||
}
|
||||
} ?>
|
||||
<?php if ($activity["gear_id"] == null) { ?>
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_notset']; ?>
|
||||
<?php } else { ?>
|
||||
<?php echo $activityGear['nickname']; ?>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<div class="justify-content-end">
|
||||
<?php if ($activity["gear_id"] == null) { ?>
|
||||
<!-- add gear zone -->
|
||||
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#addGearToActivityModal"><i class="fa-solid fa-plus"></i></a>
|
||||
|
||||
<!-- modal -->
|
||||
<div class="modal fade" id="addGearToActivityModal" tabindex="-1"
|
||||
aria-labelledby="addGearToActivityModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addGearToActivityModal">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_addGear_title']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form
|
||||
action="../activities/activity.php?activityID=<?php echo $activity["id"]; ?>&addGearToActivity=1"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- gear type fields -->
|
||||
<label for="gearIDAdd"><b>*
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_addGear_label']; ?>
|
||||
</b></label>
|
||||
<select class="form-control" name="gearIDAdd">
|
||||
<?php foreach ($activityGearOptions as $option) { ?>
|
||||
<option value="<?php echo $option["id"]; ?>">
|
||||
<?php echo $option["nickname"]; ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" name="addGearToActivity">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_addGear_submit']; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<!-- Edit zone -->
|
||||
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#editGearActivityModal"><i class="fa-regular fa-pen-to-square"></i></a>
|
||||
|
||||
<!-- modal -->
|
||||
<div class="modal fade" id="editGearActivityModal" tabindex="-1"
|
||||
aria-labelledby="editGearActivityModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editGearActivityModal">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_editGear_title']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form
|
||||
action="../activities/activity.php?activityID=<?php echo $activity["id"]; ?>&editGearActivity=1"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- gear type fields -->
|
||||
<label for="gearIDEdit"><b>*
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_addGear_label']; ?>
|
||||
</b></label>
|
||||
<select class="form-control" name="gearIDEdit">
|
||||
<?php foreach ($activityGearOptions as $option) { ?>
|
||||
<option value="<?php echo $option["id"]; ?>">
|
||||
<?php echo $option["nickname"]; ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" name="editGearActivity">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_editGear_submit']; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete zone -->
|
||||
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#deleteGearActivityModal"><i class="fa-solid fa-trash"></i></a>
|
||||
|
||||
<!-- Modal delete gear -->
|
||||
<div class="modal fade" id="deleteGearActivityModal" tabindex="-1"
|
||||
aria-labelledby="deleteGearActivityModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteGearActivityModal">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_deleteGear_title']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_deleteGear_body']; ?> <b>
|
||||
<?php echo $activityGear['nickname']; ?>
|
||||
</b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<a type="button" class="btn btn-danger"
|
||||
href="../activities/activity.php?activityID=<?php echo $activity["id"]; ?>&deleteGearActivity=1">
|
||||
<?php echo $translationsActivitiesActivity['activity_gear_deleteGear_title']; ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- waypoints graph -->
|
||||
<hr class="mb-2 mt-2">
|
||||
<div class="row">
|
||||
|
||||
<!--<h2 class="mb-3"><?php echo $translationsActivitiesActivity['activity_dataGraph_title']; ?></h2>-->
|
||||
|
||||
<div class="col-md-2">
|
||||
<p>
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_dataSelection']; ?>
|
||||
</p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="hrCheckbox" checked>
|
||||
<label class="form-check-label" for="hrCheckbox">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_hr']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="cadenceCheckbox">
|
||||
<label class="form-check-label" for="cadenceCheckbox">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_cad']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="powerCheckbox">
|
||||
<label class="form-check-label" for="powerCheckbox">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_power']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="elevationCheckbox">
|
||||
<label class="form-check-label" for="elevationCheckbox">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_ele']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7 || $activity["activity_type"] == 8) { ?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="velocityCheckbox">
|
||||
<label class="form-check-label" for="velocityCheckbox">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_vel']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="paceCheckbox">
|
||||
<label class="form-check-label" for="paceCheckbox">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_pace']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col">
|
||||
<canvas id="dataChart" height="100"></canvas>
|
||||
<p class="fw-lighter">
|
||||
<?php echo $translationsActivitiesActivity['activity_dataGraph_downsampleDataInfo']; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var ctx = document.getElementById('dataChart').getContext('2d');
|
||||
var activityType = <?php echo $activity["activity_type"]; ?>;
|
||||
|
||||
const downsampledDataHr = downsampleData(<?php foreach($hrStream as $hrValue){ $auxhr[] = (int)$hrValue["hr"]; } echo json_encode($auxhr); ?>, 200);
|
||||
|
||||
const downsampledDataCad = downsampleData(<?php foreach($cadStream as $cadValue){ $auxcad[] = (int)$cadValue["cad"]; } echo json_encode($auxcad); ?>, 200);
|
||||
|
||||
const downsampledDataPower = downsampleData(<?php foreach($powerStream as $powerValue){ $auxpower[] = (int)$powerValue["power"]; } echo json_encode($auxpower); ?>, 200);
|
||||
|
||||
const downsampledDataEle = downsampleData(<?php foreach($eleStream as $eleValue){ $auxele[] = (int)$eleValue["ele"]; } echo json_encode($auxele); ?>, 200);
|
||||
|
||||
if (activityType === 4 || activityType === 5 || activityType === 6 || activityType === 7 || activityType === 8) {
|
||||
const downsampledDataVel = downsampleData(<?php echo json_encode($velStream); ?>, 200);
|
||||
|
||||
var selectedData = {
|
||||
hr: true,
|
||||
cadence: false,
|
||||
power: false,
|
||||
elevation: false,
|
||||
velocity: false
|
||||
};
|
||||
|
||||
var data = {
|
||||
hr: downsampledDataHr,
|
||||
cadence: downsampledDataCad,
|
||||
power: downsampledDataPower,
|
||||
elevation: downsampledDataEle,
|
||||
velocity: downsampledDataVel
|
||||
};
|
||||
} else {
|
||||
const downsampledDataPace = downsampleData(<?php echo json_encode($paceStream); ?>, 200);
|
||||
|
||||
var selectedData = {
|
||||
hr: true,
|
||||
cadence: false,
|
||||
power: false,
|
||||
elevation: false,
|
||||
pace: false
|
||||
};
|
||||
|
||||
var data = {
|
||||
hr: downsampledDataHr,
|
||||
cadence: downsampledDataCad,
|
||||
power: downsampledDataPower,
|
||||
elevation: downsampledDataEle,
|
||||
pace: downsampledDataPace
|
||||
};
|
||||
}
|
||||
|
||||
var timestamps = data.hr.map(function (value, index) {
|
||||
return index;
|
||||
});
|
||||
|
||||
var dataChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timestamps,
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
title: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function downsampleData(data, threshold) {
|
||||
if (data.length <= threshold) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const factor = Math.ceil(data.length / threshold);
|
||||
const downsampledData = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += factor) {
|
||||
const chunk = data.slice(i, i + factor);
|
||||
const average = chunk.reduce((a, b) => a + b) / chunk.length;
|
||||
downsampledData.push(average);
|
||||
}
|
||||
|
||||
return downsampledData;
|
||||
}
|
||||
|
||||
function formatPace(pace) {
|
||||
const minutes = Math.floor(pace); // Extract whole minutes
|
||||
let decimalFraction = pace - minutes; // Get the decimal fraction
|
||||
let seconds = Math.round(decimalFraction * 60); // Convert the fraction to seconds
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')} min/km`;
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
// Determine which data series to display
|
||||
var datasets = [];
|
||||
|
||||
if (selectedData.hr) {
|
||||
datasets.push({
|
||||
label: 'Heart Rate',
|
||||
data: data.hr,
|
||||
borderColor: 'rgb(255, 0, 0)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedData.cadence) {
|
||||
datasets.push({
|
||||
label: 'Cadence',
|
||||
data: data.cadence,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedData.power) {
|
||||
datasets.push({
|
||||
label: 'Power (W)',
|
||||
data: data.power,
|
||||
borderColor: 'rgb(0, 0, 255)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedData.elevation) {
|
||||
datasets.push({
|
||||
label: 'Elevation',
|
||||
data: data.elevation,
|
||||
borderColor: 'rgb(0, 255, 0)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
if (activityType === 4 || activityType === 5 || activityType === 6 || activityType === 7 || activityType === 8) {
|
||||
if (selectedData.velocity) {
|
||||
datasets.push({
|
||||
label: 'Velocity (km/h)',
|
||||
data: data.velocity,
|
||||
borderColor: 'rgb(255, 255, 0)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (activityType === 1 || activityType === 2 || activityType === 3) {
|
||||
if (selectedData.pace) {
|
||||
datasets.push({
|
||||
label: 'Pace (min/km)',
|
||||
data: data.pace,
|
||||
borderColor: 'rgb(255, 255, 0)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selectedData.pace) {
|
||||
datasets.push({
|
||||
label: 'Pace (min/100m)',
|
||||
data: data.pace,
|
||||
borderColor: 'rgb(255, 255, 0)',
|
||||
fill: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the chart
|
||||
dataChart.data.datasets = datasets;
|
||||
dataChart.options.scales.y.title.display = datasets.length === 1; // Show Y-axis label if only one dataset is selected
|
||||
dataChart.update();
|
||||
}
|
||||
|
||||
// Listen for checkbox changes
|
||||
document.getElementById('hrCheckbox').addEventListener('change', function () {
|
||||
selectedData.hr = this.checked;
|
||||
updateChart();
|
||||
});
|
||||
|
||||
document.getElementById('cadenceCheckbox').addEventListener('change', function () {
|
||||
selectedData.cadence = this.checked;
|
||||
updateChart();
|
||||
});
|
||||
|
||||
document.getElementById('powerCheckbox').addEventListener('change', function () {
|
||||
selectedData.power = this.checked;
|
||||
updateChart();
|
||||
});
|
||||
|
||||
document.getElementById('elevationCheckbox').addEventListener('change', function () {
|
||||
selectedData.elevation = this.checked;
|
||||
updateChart();
|
||||
});
|
||||
|
||||
if (activityType === 4 || activityType === 5 || activityType === 6 || activityType === 7 || activityType === 8) {
|
||||
document.getElementById('velocityCheckbox').addEventListener('change', function () {
|
||||
selectedData.velocity = this.checked;
|
||||
updateChart();
|
||||
});
|
||||
} else {
|
||||
document.getElementById('paceCheckbox').addEventListener('change', function () {
|
||||
selectedData.pace = this.checked;
|
||||
updateChart();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial chart update
|
||||
updateChart();
|
||||
</script>
|
||||
|
||||
<!-- <script>
|
||||
var ctx = document.getElementById('hrChart').getContext('2d');
|
||||
|
||||
var hrStream = <?php echo json_encode($hrStream); ?>;
|
||||
var timestamps = hrStream.map(function(value, index) {
|
||||
return index;
|
||||
});
|
||||
|
||||
var hrChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timestamps,
|
||||
datasets: [{
|
||||
label: 'Heart Rate',
|
||||
data: hrStream,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
fill: false,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false, // Hide x-axis
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heart Rate',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false, // Hide legend
|
||||
},
|
||||
tooltips: {
|
||||
enabled: true, // Enable tooltips
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script> -->
|
||||
|
||||
<div>
|
||||
<br class="d-lg-none">
|
||||
<button onclick="window.history.back();" type="button" class="w-100 btn btn-primary d-lg-none">
|
||||
<?php echo $translationsTemplateTop['template_top_global_back']; ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Bottom.php" ?>
|
||||
@@ -1,390 +0,0 @@
|
||||
<?php
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
|
||||
$page = "gear";
|
||||
$gear = [];
|
||||
$editGearAction = -9000;
|
||||
$deleteGearAction = -9000;
|
||||
|
||||
if (!isLogged()) {
|
||||
header("Location: ../login.php");
|
||||
die();
|
||||
}
|
||||
|
||||
if (!isTokenValid($_SESSION["token"])) {
|
||||
header("Location: ../logout.php?sessionExpired=1");
|
||||
die();
|
||||
}
|
||||
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsGearGear = include $_SERVER['DOCUMENT_ROOT'] . '/lang/gear/gear/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsGearGear = include $_SERVER['DOCUMENT_ROOT'] . '/lang/gear/gear/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsGearGear = include $_SERVER['DOCUMENT_ROOT'] . '/lang/gear/gear/en.php';
|
||||
}
|
||||
|
||||
/* Edit action */
|
||||
if (isset($_POST["editGear"]) && $_GET["editGear"] == 1) {
|
||||
if (empty(trim($_POST["gearBrandEdit"]))) {
|
||||
$_POST["gearBrandEdit"] = NULL;
|
||||
}
|
||||
if (empty(trim($_POST["gearModelEdit"]))) {
|
||||
$_POST["gearModelEdit"] = NULL;
|
||||
}
|
||||
$editGearAction = editGear($_GET["gearID"], urlencode(trim($_POST["gearBrandEdit"])), urlencode(trim($_POST["gearModelEdit"])), urlencode(trim($_POST["gearNicknameEdit"])), $_POST["gearTypeEdit"], date("Y-m-d H:i:s", strtotime($_POST["gearDateEdit"] . " 00:00:00")), $_POST["gearIsActiveEdit"]);
|
||||
}
|
||||
|
||||
/* Delete gear */
|
||||
if (isset($_GET["deleteGear"]) && $_GET["deleteGear"] == 1) {
|
||||
$deleteGearAction = deleteGear($_GET["gearID"]);
|
||||
if ($deleteGearAction == 0) {
|
||||
header("Location: ../gear/gears.php?deleteGear=1");
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
$gear = getGearFromId($_GET["gearID"]);
|
||||
if ($gear == NULL) {
|
||||
header("Location: ../gear/gears.php?invalidGear=1");
|
||||
die();
|
||||
}
|
||||
|
||||
$gearActivities = getGearActivities($_SESSION["id"], $_GET["gearID"]);
|
||||
|
||||
$gearTotalDistance = 0;
|
||||
if(isset($gearActivities)){
|
||||
foreach ($gearActivities as $activity) {
|
||||
$gearTotalDistance += $activity["distance"];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Top.php" ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h1>
|
||||
<?php echo $gear["nickname"]; ?>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- Error banners -->
|
||||
<?php if ($editGearAction == -1 || $editGearAction == -2 || $deleteGearAction == -1 || $deleteGearAction == -2) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($editGearAction == -1 || $deleteGearAction == -1) { ?>
|
||||
API ERROR |
|
||||
<?php echo $translationsGearGear['gear_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($editGearAction == -2 || $deleteGearAction == -2) { ?>
|
||||
API ERROR |
|
||||
<?php echo $translationsGearGear['gear_API_error_-2']; ?> (-2).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($editGearAction == 0) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($editGearAction == 0) { ?>
|
||||
<?php echo $translationsGearGear['gear_success_gearEdited']; ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="row row-gap-3">
|
||||
<!-- left column -->
|
||||
<div class="col-lg-3 col-md-12">
|
||||
<!-- Gear photo -->
|
||||
<div class="justify-content-center align-items-center d-flex">
|
||||
<img src=<?php if ($gear["gear_type"] == 1) {
|
||||
echo ("../img/avatar/bicycle1.png");
|
||||
} else {
|
||||
if ($gear["gear_type"] == 2) {
|
||||
echo ("../img/avatar/running_shoe1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/wetsuit1.png");
|
||||
}
|
||||
} ?> alt="gearPicture" width="180" height="180">
|
||||
<!--<img src=<?php if ($gear["gear_type"] == 1) {
|
||||
echo ("../img/avatar/bicycle1.png");
|
||||
} else {
|
||||
if ($gear["gear_type"] == 2) {
|
||||
echo ("../img/avatar/running_shoe1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/wetsuit1.png");
|
||||
}
|
||||
} ?> alt="gearPicture" class="rounded-circle" width="180" height="180">-->
|
||||
</div>
|
||||
<br>
|
||||
<div class="vstack justify-content-center align-items-center d-flex">
|
||||
<!-- badges -->
|
||||
<div class="hstack justify-content-center">
|
||||
<?php if ($gear["is_active"] == 1) { ?>
|
||||
<span
|
||||
class="badge bg-success-subtle border border-success-subtle text-success-emphasis rounded-pill align-middle">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_isactive']; ?>
|
||||
</span>
|
||||
<?php } else { ?>
|
||||
<span
|
||||
class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis rounded-pill align-middle">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_isinactive']; ?>
|
||||
</span>
|
||||
<?php } ?>
|
||||
<?php if ($gear["gear_type"] == 1) { ?>
|
||||
<span
|
||||
class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis rounded-pill align-middle">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_gearisbike']; ?>
|
||||
</span>
|
||||
<?php } else {
|
||||
if ($gear["gear_type"] == 2) { ?>
|
||||
<span
|
||||
class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis rounded-pill align-middle">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_gearisshoe']; ?>
|
||||
</span>
|
||||
<?php } else { ?>
|
||||
<span
|
||||
class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis rounded-pill align-middle">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_geariswetsuit']; ?>
|
||||
</span>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php if (isset($gear["strava_gear_id"])) { ?>
|
||||
<span
|
||||
class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis rounded-pill align-middle">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_strava_gear']; ?>
|
||||
</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<!-- edit gear zone -->
|
||||
<a class="mt-2 w-100 btn btn-primary <?php if (isset($gear["strava_gear_id"])) { echo "disabled"; } ?>" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#editGearModal">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_editbutton']; ?>
|
||||
</a>
|
||||
|
||||
<!-- Modal edit gear -->
|
||||
<div class="modal fade" id="editGearModal" tabindex="-1" aria-labelledby="editGearModal"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editGearModal">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_editbutton']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../gear/gear.php?gearID=<?php echo ($gear["id"]); ?>&editGear=1"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- brand fields -->
|
||||
<label for="gearBrandEdit"><b>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_brandLabel']; ?>
|
||||
</b></label>
|
||||
<input class="form-control" type="text" name="gearBrandEdit"
|
||||
placeholder="<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_brandPlaceholder']; ?>"
|
||||
maxlength="45" value="<?php echo ($gear["brand"]); ?>">
|
||||
<!-- model fields -->
|
||||
<label for="gearModelEdit"><b>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_modelLabel']; ?>
|
||||
</b></label>
|
||||
<input class="form-control" type="text" name="gearModelEdit"
|
||||
placeholder="<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_modelPlaceholder']; ?>"
|
||||
maxlength="45" value="<?php echo ($gear["model"]); ?>">
|
||||
<!-- nickname fields -->
|
||||
<label for="gearNicknameEdit"><b>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_nicknameLabel']; ?>
|
||||
</b></label>
|
||||
<input class="form-control" type="text" name="gearNicknameEdit"
|
||||
placeholder="<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_nicknamePlaceholder']; ?>"
|
||||
maxlength="45" value="<?php echo ($gear["nickname"]); ?>">
|
||||
<!-- gear type fields -->
|
||||
<label for="gearTypeEdit"><b>*
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearTypeLabel']; ?>
|
||||
</b></label>
|
||||
<select class="form-control" name="gearTypeEdit">
|
||||
<option value="1" <?php if ($gear["gear_type"] == 1) { ?> selected="selected"
|
||||
<?php } ?>>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearTypeOption1']; ?>
|
||||
</option>
|
||||
<option value="2" <?php if ($gear["gear_type"] == 2) { ?> selected="selected"
|
||||
<?php } ?>>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearTypeOption2']; ?>
|
||||
</option>
|
||||
<option value="3" <?php if ($gear["gear_type"] == 3) { ?> selected="selected"
|
||||
<?php } ?>>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearTypeOption3']; ?>
|
||||
</option>
|
||||
</select required>
|
||||
<!-- date fields -->
|
||||
<label for="gearDateEdit"><b>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_dateLabel']; ?>
|
||||
</b></label>
|
||||
<input class="form-control" type="date" name="gearDateEdit"
|
||||
placeholder="<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_datePlaceholder']; ?>"
|
||||
value="<?php echo date("Y-m-d", strtotime($gear["created_at"])); ?>"
|
||||
required>
|
||||
<!-- gear is_active fields -->
|
||||
<label for="gearIsActiveEdit"><b>*
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearIsActiveLabel']; ?>
|
||||
</b></label>
|
||||
<select class="form-control" name="gearIsActiveEdit">
|
||||
<option value="1" <?php if ($gear["is_active"] == 1) { ?> selected="selected"
|
||||
<?php } ?>>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearIsActiveOption1']; ?>
|
||||
</option>
|
||||
<option value="0" <?php if ($gear["is_active"] == 0) { ?> selected="selected"
|
||||
<?php } ?>>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_editGear_gearIsActiveOption2']; ?>
|
||||
</option>
|
||||
</select required>
|
||||
*
|
||||
<?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" name="editGear">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_editbutton']; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- delete gear zone -->
|
||||
<a class="mt-2 w-100 btn btn-danger <?php if (isset($gear["strava_gear_id"])) { echo "disabled"; } ?>" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#deleteGearModal" <?php if (isset($gearActivities) && count($gearActivities) != 0) {
|
||||
echo 'aria-disabled="true"';
|
||||
} ?>><?php echo $translationsGearGear['gear_gear_infoZone_deletebutton']; ?>
|
||||
</a>
|
||||
|
||||
<!-- Modal delete gear -->
|
||||
<div class="modal fade" id="deleteGearModal" tabindex="-1" aria-labelledby="deleteGearModal"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteGearModal">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_deletebutton']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_modal_deleteGear_body']; ?> <b>
|
||||
<?php echo ($gear["nickname"]); ?>
|
||||
</b><span>?</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<a type="button" class="btn btn-danger"
|
||||
href="../gear/gear.php?gearID=<?php echo ($gear["id"]); ?>&deleteGear=1">
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_deletebutton']; ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- details -->
|
||||
<div class="vstack align-items-center">
|
||||
<span class="mt-2"><strong>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_distance']; ?>:
|
||||
</strong>
|
||||
<?php echo number_format($gearTotalDistance / 1000, 2); ?> km
|
||||
</span>
|
||||
<?php if (isset($gear["brand"])) { ?>
|
||||
<span class="mt-2"><strong>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_brand']; ?>:
|
||||
</strong>
|
||||
<?php echo $gear["brand"]; ?>
|
||||
</span>
|
||||
<?php } ?>
|
||||
<?php if (isset($gear["model"])) { ?>
|
||||
<span class="mt-2"><strong>
|
||||
<?php echo $translationsGearGear['gear_gear_infoZone_model']; ?>:
|
||||
</strong>
|
||||
<?php echo $gear["model"]; ?>
|
||||
</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- right column -->
|
||||
<div class="col">
|
||||
<div>
|
||||
<!-- Gear components -->
|
||||
|
||||
<!-- Last 10 gear activities -->
|
||||
<hr class="mb-2 mt-2 d-sm-none d-block">
|
||||
<div class="hstack align-items-baseline">
|
||||
<h5>
|
||||
<?php echo $translationsGearGear['gear_gear_gearActivities_title']; ?>
|
||||
</h5>
|
||||
<h7 class="ms-1">
|
||||
<?php echo $translationsGearGear['gear_gear_gearActivities_number']; ?>
|
||||
</h7>
|
||||
</div>
|
||||
<?php if (!isset($gearActivities)) { ?>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-info me-1"></i>
|
||||
<div>
|
||||
<?php echo $translationsGearGear['gear_gear_gearActivities_noactivities']; ?>.
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php for ($x = 0; $x < 10; $x += 1) { ?>
|
||||
<?php if ($x < count($gearActivities)) { ?>
|
||||
<li class="vstack list-group-item d-flex justify-content-between">
|
||||
<a href="../activities/activity.php?activityID=<?php echo $gearActivities[$x]["id"]; ?>">
|
||||
<?php echo $gearActivities[$x]["name"]; ?>
|
||||
</a>
|
||||
<span><strong>
|
||||
<?php echo $translationsGearGear['gear_gear_gearActivities_datelabel']; ?>:
|
||||
</strong>
|
||||
<?php echo (new DateTime($gearActivities[$x]["start_time"]))->format("d/m/y"); ?>@
|
||||
<?php echo (new DateTime($gearActivities[$x]["start_time"]))->format("H:i"); ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<br>
|
||||
<button onclick="window.history.back();" type="button" class="w-100 btn btn-primary d-lg-none">
|
||||
<?php echo $translationsTemplateTop['template_top_global_back']; ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Bottom.php" ?>
|
||||
@@ -1,309 +0,0 @@
|
||||
<?php
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
|
||||
$page = "gears";
|
||||
$addGearAction = -9000;
|
||||
$numGears = 0;
|
||||
$gears = [];
|
||||
$pageNumber = 1;
|
||||
$numRecords = 5;
|
||||
|
||||
if (!isLogged()) {
|
||||
header("Location: ../login.php");
|
||||
die();
|
||||
}
|
||||
|
||||
if (!isTokenValid($_SESSION["token"])) {
|
||||
header("Location: ../logout.php?sessionExpired=1");
|
||||
die();
|
||||
}
|
||||
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsGearGears = include $_SERVER['DOCUMENT_ROOT'] . '/lang/gear/gears/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsGearGears = include $_SERVER['DOCUMENT_ROOT'] . '/lang/gear/gears/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsGearGears = include $_SERVER['DOCUMENT_ROOT'] . '/lang/gear/gears/en.php';
|
||||
}
|
||||
|
||||
/* Add action */
|
||||
if (isset($_POST["addGear"]) && $_GET["addGear"] == 1) {
|
||||
if (empty(trim($_POST["gearBrandAdd"]))) {
|
||||
$_POST["gearBrandAdd"] = NULL;
|
||||
$parsedBrand = NULL;
|
||||
}else{
|
||||
$parsedBrand = urlencode(trim($_POST["gearBrandAdd"]));
|
||||
}
|
||||
if (empty(trim($_POST["gearModelAdd"]))) {
|
||||
$_POST["gearModelAdd"] = NULL;
|
||||
$parsedModel = NULL;
|
||||
}else{
|
||||
$parsedModel = urlencode(trim($_POST["gearModelAdd"]));
|
||||
}
|
||||
|
||||
$addGearAction = newGear($parsedBrand, $parsedModel, urlencode(trim($_POST["gearNicknameAdd"])), $_POST["gearTypeAdd"], $_POST["gearDateAdd"]);
|
||||
}
|
||||
|
||||
if (isset($_GET["pageNumber"])) {
|
||||
$pageNumber = $_GET["pageNumber"];
|
||||
}
|
||||
|
||||
if (!isset($_POST["gearSearch"])) {
|
||||
$gears = getGearPagination($pageNumber, $numRecords);
|
||||
$numGears = numGears();
|
||||
$total_pages = ceil($numGears / $numRecords);
|
||||
} else {
|
||||
$gears = getGearFromNickname(urlencode(trim($_POST["gearNickname"])));
|
||||
if ($gears == NULL) {
|
||||
$numGears = 0;
|
||||
} else {
|
||||
$numGears = 1;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Top.php" ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h1><?php echo $translationsGearGears['gear_title']; ?></h1>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- Error banners -->
|
||||
<?php if ($gears == -1 || $gears == -2 || $addGearAction == -1 || $addGearAction == -2 || $addGearAction == -3) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($gears == -1 || $addGearAction == -1) { ?>
|
||||
API ERROR | <?php echo $translationsGearGears['gear_gear_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($gears == -2 || $addGearAction == -2) { ?>
|
||||
API ERROR | <?php echo $translationsGearGears['gear_gear_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($addGearAction == -3) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_error_addGear_-3']; ?> (-3).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
<?php if ($gears == NULL || (isset($_GET["invalidGear"]) && $_GET["invalidGear"] == 1)) { ?>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($gears == NULL) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_info_searchGear_NULL']; ?> (NULL).
|
||||
<?php } else { ?>
|
||||
<?php if (isset($_GET["invalidGear"]) && $_GET["invalidGear"] == 1) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_info_fromGear_invalidGear']; ?> (1).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($addGearAction == 0 || (isset($_GET["deleteGear"]) && $_GET["deleteGear"] == 1)) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($addGearAction == 0) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_success_gearAdded']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if (isset($_GET["deleteGear"]) && $_GET["deleteGear"] == 1) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_success_gearDeleted']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="row row-gap-3">
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<!-- Add gear zone -->
|
||||
<p><?php echo $translationsGearGears['gear_gear_buttonLabel_addGear']; ?></p>
|
||||
<a class="w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addGearModal"><?php echo $translationsGearGears['gear_gear_button_addGear']; ?></a>
|
||||
|
||||
<!-- Modal add gear -->
|
||||
<div class="modal fade" id="addGearModal" tabindex="-1" aria-labelledby="addGearModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addGearModal"><?php echo $translationsGearGears['gear_gear_modal_addGear_title']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../gear/gears.php?addGear=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- brand fields -->
|
||||
<label for="gearBrandAdd"><b><?php echo $translationsGearGears['gear_gear_modal_addEditGear_brandLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="gearBrandAdd" placeholder="<?php echo $translationsGearGears['gear_gear_modal_addEditGear_brandPlaceholder']; ?>" maxlength="45" value="<?php echo isset($_POST["gearBrandAdd"]) ? htmlspecialchars($_POST["gearBrandAdd"]) : ''; ?>">
|
||||
<!-- model fields -->
|
||||
<label for="gearModelAdd"><b><?php echo $translationsGearGears['gear_gear_modal_addEditGear_modelLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="gearModelAdd" placeholder="<?php echo $translationsGearGears['gear_gear_modal_addEditGear_modelPlaceholder']; ?>" maxlength="45" value="<?php echo isset($_POST["gearModelAdd"]) ? htmlspecialchars($_POST["gearModelAdd"]) : ''; ?>">
|
||||
<!-- nickname fields -->
|
||||
<label for="gearNicknameAdd"><b>* <?php echo $translationsGearGears['gear_gear_modal_addEditGear_nicknameLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="gearNicknameAdd" placeholder="<?php echo htmlspecialchars($translationsGearGears['gear_gear_modal_addEditGear_nicknamePlaceholder']); ?>" maxlength="45" value="<?php echo isset($_POST["gearNicknameAdd"]) ? htmlspecialchars($_POST["gearNicknameAdd"]) : ''; ?>">
|
||||
<!-- gear type fields -->
|
||||
<label for="gearTypeAdd"><b>* <?php echo $translationsGearGears['gear_gear_modal_addEditUser_gearTypeLabel']; ?></b></label>
|
||||
<select class="form-control" name="gearTypeAdd">
|
||||
<option value="1" <?php if (isset($_POST["gearTypeAdd"]) && $_POST["gearTypeAdd"] == 1) {
|
||||
echo 'selected="selected"';
|
||||
} ?>><?php echo htmlspecialchars($translationsGearGears['gear_gear_modal_addEditUser_gearTypeOption1']); ?></option>
|
||||
<option value="2" <?php if (isset($_POST["gearTypeAdd"]) && $_POST["gearTypeAdd"] == 2) {
|
||||
echo 'selected="selected"';
|
||||
} ?>><?php echo htmlspecialchars($translationsGearGears['gear_gear_modal_addEditUser_gearTypeOption2']); ?></option>
|
||||
<option value="3" <?php if (isset($_POST["gearTypeAdd"]) && $_POST["gearTypeAdd"] == 3) {
|
||||
echo 'selected="selected"';
|
||||
} ?>><?php echo htmlspecialchars($translationsGearGears['gear_gear_modal_addEditUser_gearTypeOption3']); ?></option>
|
||||
</select required>
|
||||
<!-- date fields -->
|
||||
<label for="gearDateAdd"><b>* <?php echo $translationsGearGears['gear_gear_modal_addEditGear_dateLabel']; ?></b></label>
|
||||
<input class="form-control" type="date" name="gearDateAdd" placeholder="<?php echo htmlspecialchars($translationsGearGears['gear_gear_modal_addEditGear_datePlaceholder']); ?>" value="<?php echo isset($_POST["gearDateAdd"]) ? htmlspecialchars($_POST["gearDateAdd"]) : ''; ?>" required>
|
||||
<p>* <?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
<button type="submit" class="btn btn-success" name="addGear"><?php echo $translationsGearGears['gear_gear_modal_addGear_title']; ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<p><?php echo $translationsGearGears['gear_gear_buttonLabel_searchGear']; ?></p>
|
||||
<form action="../gear/gears.php" method="post">
|
||||
<div class="mb-3">
|
||||
<!--<label for="userUsername"><b>Username</b></label>-->
|
||||
<input class="form-control" type="text" name="gearNickname" placeholder="<?php echo $translationsGearGears['gear_gear_form_searchGear_nicknamePlaceholder']; ?>" required>
|
||||
</div>
|
||||
<button class="w-100 btn btn-success" type="submit" name="gearSearch"><?php echo $translationsGearGears['gear_gear_button_searchGear']; ?></button>
|
||||
</form>
|
||||
<?php if (isset($_POST["gearSearch"])) { ?>
|
||||
<br>
|
||||
<a class="w-100 btn btn-primary" href="../gear/gears.php" role="button"><?php echo $translationsTemplateTop['template_top_global_button_listAll']; ?></a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col">
|
||||
<?php if ($gears == -1 || $numGears == -1 || $gears == -2) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<div>
|
||||
<?php if ($gears == -1 || $numGears == -1 || $gears == -2) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_error_listGear_-1-2']; ?> (-1/-2).
|
||||
<?php } ?>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<!-- Info banners -->
|
||||
<?php if ($gears == -3) { ?>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<div>
|
||||
<?php if ($gears == -3) { ?>
|
||||
<?php echo $translationsGearGears['gear_gear_error_listGear_-3']; ?> (-3).
|
||||
<?php } ?>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<p><?php echo $translationsGearGears['gear_gear_list_title1']; ?> <?php echo ($numGears); ?> <?php echo $translationsGearGears['gear_gear_list_title2']; ?> (<?php echo $numRecords; ?> <?php echo $translationsGearGears['gear_gear_list_title3']; ?>:</p>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($gears as $gear) { ?>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src=<?php if ($gear["gear_type"] == 1) {
|
||||
echo ("../img/avatar/bicycle1.png");
|
||||
} else {
|
||||
if ($gear["gear_type"] == 2) {
|
||||
echo ("../img/avatar/running_shoe1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/wetsuit1.png");
|
||||
}
|
||||
} ?> alt="gearPicture" width="55" height="55">
|
||||
<!--<img src=<?php if ($gear["gear_type"] == 1) {
|
||||
echo ("../img/avatar/bicycle1.png");
|
||||
} else {
|
||||
if ($gear["gear_type"] == 2) {
|
||||
echo ("../img/avatar/running_shoe1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/wetsuit1.png");
|
||||
}
|
||||
} ?> alt="gearPicture" class="rounded-circle" width="55" height="55">-->
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
<!--<?php echo ($gear["nickname"]); ?>-->
|
||||
<a href="../gear/gear.php?gearID=<?php echo ($gear["id"]); ?>" class="link-underline-opacity-25 link-underline-opacity-100-hover"><?php echo ($gear["nickname"]); ?></a>
|
||||
</div>
|
||||
<b><?php echo $translationsGearGears['gear_gear_gear_type']; ?></b><?php if ($gear["gear_type"] == 1) {
|
||||
echo $translationsGearGears['gear_gear_modal_addEditUser_gearTypeOption1'];
|
||||
} else {
|
||||
if ($gear["gear_type"] == 2) {
|
||||
echo $translationsGearGears['gear_gear_modal_addEditUser_gearTypeOption2'];
|
||||
} else {
|
||||
if ($gear["gear_type"] == 3) {
|
||||
echo $translationsGearGears['gear_gear_modal_addEditUser_gearTypeOption3'];
|
||||
}
|
||||
}
|
||||
} ?>
|
||||
</br>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<?php if ($gear["is_active"] == 1) { ?>
|
||||
<span class="badge bg-success-subtle border border-success-subtle text-success-emphasis align-middle"><?php echo $translationsGearGears['gear_gear_list_isactive']; ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis align-middle"><?php echo $translationsGearGears['gear_gear_list_isinactive']; ?></span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<!--<a class="btn btn-link btn-rounded btn-lg" href="../gear/gear.php?gearID=<?php echo ($gear["id"]); ?>" role="button">View</a>
|
||||
<a class="btn btn-link btn-lg" href="../gear/gear.php?gearID=<?php echo ($gear["id"]); ?>" role="button" ><i class="fa-solid fa-arrow-up-right-from-square"></i></a>-->
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php if (!isset($_POST["gearSearch"])) { ?>
|
||||
<br>
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <?php if ($pageNumber == 1) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?pageNumber=1">«</a></li>
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++) { ?>
|
||||
<li class="page-item <?php if ($i == $pageNumber) {
|
||||
echo "active";
|
||||
} ?>"><a class="page-link" href="?pageNumber=<?php echo ($i); ?>"><?php echo ($i); ?></a></li>
|
||||
<?php } ?>
|
||||
<li class="page-item <?php if ($pageNumber == $total_pages) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?pageNumber=<?php echo ($total_pages); ?>">»</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<button onclick="window.history.back();" type="button" class="w-100 btn btn-primary d-lg-none"><?php echo $translationsTemplateTop['template_top_global_back']; ?></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Bottom.php" ?>
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="strava_outlined" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95.3 41.39"><defs><style>.cls-1{fill:#fc4c02;}.cls-2{fill:#999;}</style></defs><title>api_logo</title><path class="cls-1" d="M14.62,34.11a14.15,14.15,0,0,1-4.31-.64,9.54,9.54,0,0,1-3.44-1.91l2.7-3.21a8,8,0,0,0,2.59,1.36,9.31,9.31,0,0,0,2.7.41,2.13,2.13,0,0,0,1-.17,0.53,0.53,0,0,0,.3-0.47v0a0.63,0.63,0,0,0-.44-0.54,7.69,7.69,0,0,0-1.65-.45q-1.27-.26-2.43-0.61a8.35,8.35,0,0,1-2-.88,4.27,4.27,0,0,1-1.39-1.36,3.69,3.69,0,0,1-.52-2v0a4.78,4.78,0,0,1,.42-2,4.57,4.57,0,0,1,1.23-1.62,5.85,5.85,0,0,1,2-1.08,8.9,8.9,0,0,1,2.75-.39A12.87,12.87,0,0,1,18,19a9.18,9.18,0,0,1,3,1.55l-2.46,3.41a7.57,7.57,0,0,0-2.28-1.13,7.93,7.93,0,0,0-2.26-.36,1.56,1.56,0,0,0-.83.17,0.51,0.51,0,0,0-.27.45v0a0.62,0.62,0,0,0,.41.52,7,7,0,0,0,1.6.45,22.37,22.37,0,0,1,2.64.62,7.8,7.8,0,0,1,2,.94A4.16,4.16,0,0,1,20.83,27a3.81,3.81,0,0,1,.46,1.94v0a4.69,4.69,0,0,1-.48,2.14,4.57,4.57,0,0,1-1.34,1.61,6.35,6.35,0,0,1-2.09,1A9.87,9.87,0,0,1,14.62,34.11Z"/><path class="cls-1" d="M25.46,23H21V18.72H35V23H30.51V33.83H25.46V23Z"/><path class="cls-1" d="M35.67,18.72H43a10.1,10.1,0,0,1,3.33.46,5.54,5.54,0,0,1,2.1,1.26,4.61,4.61,0,0,1,1,1.55,5.48,5.48,0,0,1,.35,2v0a4.77,4.77,0,0,1-.8,2.8,5.5,5.5,0,0,1-2.18,1.81l3.52,5.14H44.64l-2.85-4.32H40.72v4.32H35.67V18.72Zm7.23,7.19a2.32,2.32,0,0,0,1.42-.39,1.28,1.28,0,0,0,.52-1.08v0a1.23,1.23,0,0,0-.52-1.09,2.44,2.44,0,0,0-1.4-.36h-2.2v3H42.9Z"/><polygon class="cls-1" points="79.61 27.33 82.91 33.83 87.75 33.83 79.61 17.77 71.48 33.83 76.32 33.83 79.61 27.33"/><polygon class="cls-1" points="56.98 27.33 60.27 33.83 65.11 33.83 56.98 17.77 48.85 33.83 53.69 33.83 56.98 27.33"/><polygon class="cls-1" points="68.3 25.22 65.01 18.72 60.17 18.72 68.3 34.77 76.43 18.72 71.59 18.72 68.3 25.22"/><path class="cls-2" d="M10.87,10.95l1,0.2a2.42,2.42,0,0,1-.69,1.53,2.08,2.08,0,0,1-1.53.58,2.24,2.24,0,0,1-1.74-.74,3.21,3.21,0,0,1-.68-2.22,3,3,0,0,1,.71-2.12,2.26,2.26,0,0,1,1.7-.75A2.1,2.1,0,0,1,11.11,8a2.23,2.23,0,0,1,.7,1.44l-1,.15A1.23,1.23,0,0,0,9.65,8.29a1.28,1.28,0,0,0-1,.5,2.35,2.35,0,0,0-.41,1.52,2.51,2.51,0,0,0,.4,1.56,1.26,1.26,0,0,0,1,.52Q10.7,12.39,10.87,10.95Z"/><path class="cls-2" d="M16.46,13.09a2.51,2.51,0,0,1-1.8,0,2.23,2.23,0,0,1-1.32-1.44,4.25,4.25,0,0,1-.2-1.36,3,3,0,0,1,.71-2.12,2.26,2.26,0,0,1,1.7-.75,2.24,2.24,0,0,1,1.69.76A3,3,0,0,1,18,10.3a4.22,4.22,0,0,1-.2,1.36,2.33,2.33,0,0,1-.54.93A2.25,2.25,0,0,1,16.46,13.09Zm-1.93-1.22a1.29,1.29,0,0,0,2.06,0A2.56,2.56,0,0,0,17,10.3a2.34,2.34,0,0,0-.41-1.49,1.28,1.28,0,0,0-2,0,2.33,2.33,0,0,0-.41,1.48A2.56,2.56,0,0,0,14.53,11.87Z"/><path class="cls-2" d="M19.6,13.14V7.54h1.1l1.68,3.9h0l1.66-3.9h1.09v5.6H24.2V9.29h0L22.5,13.14H22.26L20.57,9.29h0v3.85H19.6Z"/><path class="cls-2" d="M27.1,13.14V7.54h2.23A2,2,0,0,1,30.72,8a1.65,1.65,0,0,1,.53,1.3,1.65,1.65,0,0,1-.51,1.28,2,2,0,0,1-1.39.47H28v2.08H27.1Zm0.94-3h1.27A1,1,0,0,0,30,9.95a0.81,0.81,0,0,0,.27-0.64A0.81,0.81,0,0,0,30,8.63a1.1,1.1,0,0,0-.69-0.22H28v1.78Z"/><path class="cls-2" d="M35.47,13.14l-0.34-1H32.87l-0.33,1H31.61l2-5.6h0.77l2,5.6H35.47ZM34,8.91l-0.85,2.46h1.7L34,8.91h0Z"/><path class="cls-2" d="M38.24,13.14V8.41H36.57V7.54h4.28V8.41H39.19v4.73H38.24Z"/><path class="cls-2" d="M42.29,13.14V7.54h0.94v5.6H42.29Z"/><path class="cls-2" d="M45.23,13.14V7.54h2.3A1.65,1.65,0,0,1,48.73,8,1.41,1.41,0,0,1,49.18,9a1.17,1.17,0,0,1-.65,1.08v0a1.6,1.6,0,0,1,.6.48,1.3,1.3,0,0,1,.25.82q0,1.71-2.06,1.71h-2.1Zm0.94-3.3h1.23A0.93,0.93,0,0,0,48,9.65a0.64,0.64,0,0,0,.22-0.52A0.66,0.66,0,0,0,48,8.59a0.94,0.94,0,0,0-.62-0.18H46.18V9.83Zm0,2.43h1.4a1.7,1.7,0,0,0,.34-0.06,0.7,0.7,0,0,0,.28-0.14,0.72,0.72,0,0,0,.17-0.25,1,1,0,0,0,.07-0.39,0.66,0.66,0,0,0-.31-0.65,2.16,2.16,0,0,0-1-.16h-1v1.65Z"/><path class="cls-2" d="M51,13.14V7.54H52v4.73h2.35v0.87H51Z"/><path class="cls-2" d="M55.76,13.14V7.54h3.49V8.41H56.7V9.85H59v0.84H56.7v1.58h2.54v0.87H55.76Z"/><path class="cls-2" d="M64.28,13.14l-1.07-5.6h1l0.76,3.94h0l1.33-3.94h0.54l1.32,3.94h0L68.9,7.54h0.94l-1.07,5.6h-1L66.53,9.3h0l-1.24,3.84h-1Z"/><path class="cls-2" d="M71.45,13.14V7.54h0.94v5.6H71.45Z"/><path class="cls-2" d="M75.5,13.14V8.41H73.83V7.54h4.28V8.41H76.45v4.73H75.5Z"/><path class="cls-2" d="M79.51,13.14V7.54h0.94v2.3h2.11V7.54h0.94v5.6H82.57V10.66H80.46v2.48H79.51Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,15 +0,0 @@
|
||||
<!-- Footer -->
|
||||
<footer class="border-top py-3 my-4 bg-body-tertiar">
|
||||
<p class="text-center text-muted">©
|
||||
<?php if (date("Y") == 2023) {
|
||||
echo date("Y");
|
||||
} else { ?>2023 -
|
||||
<?php echo date("Y");
|
||||
} ?> Endurain • <a href="https://github.com/joaovitoriasilva/endurain"
|
||||
role="button"><i class="fa-brands fa-github"></i></a> • <a href="https://fosstodon.org/@endurain"><i class="fa-brands fa-mastodon"></i></a> • v0.1.5
|
||||
</p>
|
||||
<p class="text-center text-muted"><img src="../img/strava/api_logo_cptblWith_strava_horiz_light.png"
|
||||
alt="Compatible with STRAVA image" height="25" /></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,165 +0,0 @@
|
||||
<?php
|
||||
// Check if the "preferred_language" key is set in the session
|
||||
if (isset($_SESSION["preferred_language"])) {
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsTemplateTop = include $_SERVER['DOCUMENT_ROOT'] . '/lang/inc/Template-Top/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsTemplateTop = include $_SERVER['DOCUMENT_ROOT'] . '/lang/inc/Template-Top/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsTemplateTop = include $_SERVER['DOCUMENT_ROOT'] . '/lang/inc/Template-Top/en.php';
|
||||
}
|
||||
} else {
|
||||
// Set a default language or handle the case when "preferred_language" is not set
|
||||
$translationsTemplateTop = include $_SERVER['DOCUMENT_ROOT'] . '/lang/inc/Template-Top/en.php';
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="default" lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Endurain</title>
|
||||
<!--<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.2.0/chart.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script>-->
|
||||
<script src="https://kit.fontawesome.com/8c44ee63d9.js"></script>
|
||||
<link rel="shortcut icon" href="../img/logo/logo.png">
|
||||
<link rel="apple-touch-icon" href="../img/logo/logo.png">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Used to toggle the menu on small screens when clicking on the menu button
|
||||
function showNav() {
|
||||
var x = document.getElementById("navSmallScreens");
|
||||
if (x.className.indexOf("w3-show") == -1) {
|
||||
x.className += " w3-show";
|
||||
} else {
|
||||
x.className = x.className.replace(" w3-show", "");
|
||||
}
|
||||
}
|
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const html = document.querySelector('html');
|
||||
if (prefersDark) {
|
||||
html.setAttribute('data-bs-theme', 'dark');
|
||||
} else {
|
||||
html.setAttribute('data-bs-theme', 'default');
|
||||
}
|
||||
</script>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<!--<img src="../img/logo/logo.png" alt="Logo" width="24" height="24" class="rounded-circle d-inline-block align-text-top m-2">-->
|
||||
<!--<img src="../img/logo/logo.png" alt="Logo" width="30" class="d-inline-block align-text-top m-2 rounded-1">-->
|
||||
<a class="navbar-brand" href="../index.php">Endurain</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<?php if (isLogged()) { ?>
|
||||
<?php if($page == "index"){ ?>
|
||||
<a class="nav-link" href="#" role="button" data-bs-toggle="modal" data-bs-target="#searchUserModal"><i class="fa-solid fa-magnifying-glass"></i> <?php echo $translationsTemplateTop['template_top_navbar_search']; ?></a>
|
||||
|
||||
<!-- Modal add actvity -->
|
||||
<div class="modal fade" id="searchUserModal" tabindex="-1" aria-labelledby="searchUserModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="searchUserModal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_searchUserModal_title']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../index.php?searchUser=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- username search fields -->
|
||||
<label for="templateTopSeachUser"><b>*
|
||||
<?php echo $translationsTemplateTop['template_top_global_searchUserModal_usernameLabel']; ?>
|
||||
</b>
|
||||
</label>
|
||||
<input class="form-control" type="text" name="templateTopSeachUser"
|
||||
placeholder="<?php echo $translationsTemplateTop['template_top_global_searchUserModal_usernamePlaceholder']; ?>"
|
||||
maxlength="45" value="<?php echo isset($_POST["templateTopSeachUser"]) ? htmlspecialchars($_POST["templateTopSeachUser"]) : ''; ?>">
|
||||
<p>*
|
||||
<?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" name="searchUser">
|
||||
<?php echo $translationsTemplateTop['template_top_global_searchUserModal_title']; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<a class="nav-link <?php if (str_contains($page, "gear")) {
|
||||
echo ("active");
|
||||
} ?>" href="../gear/gears.php"><i class="fa-solid fa-bicycle"></i>
|
||||
<?php echo $translationsTemplateTop['template_top_navbar_gear']; ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="navbar-nav">
|
||||
<?php if (isLogged()) { ?>
|
||||
<span class="border-top d-sm-none d-block mb-2"></span>
|
||||
<a class="nav-link" href="../users/user.php?userID=<?php echo ($_SESSION["id"]); ?>">
|
||||
<img src="<?php if (is_null($_SESSION["photo_path"])) {
|
||||
if ($_SESSION["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($_SESSION["photo_path"]);
|
||||
} ?>" alt="userPhoto" width="24" height="24" class="rounded-circle align-top"><span
|
||||
class="ms-2">
|
||||
<?php echo $translationsTemplateTop['template_top_navbar_profile']; ?>
|
||||
</span>
|
||||
</a>
|
||||
<span class="border-top d-sm-none d-block mb-2"></span>
|
||||
<a class="nav-link d-none d-sm-block">|</a>
|
||||
<a class="nav-link" href="../settings/settings.php"><i class="fa-solid fa-gear"></i>
|
||||
<?php echo $translationsTemplateTop['template_top_navbar_settings']; ?>
|
||||
</a>
|
||||
<a class="nav-link" href="../logout.php"><i class="fas fa-sign-out-alt"></i>
|
||||
<?php echo $translationsTemplateTop['template_top_navbar_logout']; ?>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a class="nav-link" href="../login.php"><i class="fas fa-sign-in-alt"></i>
|
||||
<?php echo $translationsTemplateTop['template_top_navbar_login']; ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center mx-2 my-2 justify-content-center"
|
||||
role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation me-1"></i>
|
||||
<div>
|
||||
<span>
|
||||
<?php echo $translationsTemplateTop['template_top_warning_zone']; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,273 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Activies */
|
||||
/* ************************************************************************** */
|
||||
/* Get all activities */
|
||||
function getActivities()
|
||||
{
|
||||
$response = callAPIRoute("/activities/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user activities */
|
||||
function getUserActivities()
|
||||
{
|
||||
$response = callAPIRoute("/activities/user", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user activities for provided week */
|
||||
function getUserActivitiesWeek($userID, $week)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$userID/week/$week", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user activities for this week */
|
||||
function getUserActivitiesThisWeekDistances($userID)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$userID/thisweek/distances", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user activities for this month */
|
||||
function getUserActivitiesThisMonthDistances($userID)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$userID/thismonth/distances", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user activities count for this month */
|
||||
function getUserActivitiesThisMonthNumber($userID)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$userID/thismonth/number", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get all gear activities */
|
||||
function getGearActivities($user_id, $gearID)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$user_id/gear/$gearID", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Get all activities with pagination */
|
||||
function getActivitiesPagination($pageNumber, $numRecords)
|
||||
{
|
||||
$response = callAPIRoute("/activities/all/pagenumber/$pageNumber/numRecords/$numRecords", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user activities with pagination */
|
||||
function getUserActivitiesPagination($userID, $pageNumber, $numRecords)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$userID/page_number/$pageNumber/num_records/$numRecords", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user following activities with pagination */
|
||||
function getFollowedUserActivitiesPagination($user_id, $pageNumber, $numRecords)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$user_id/followed/page_number/$pageNumber/num_records/$numRecords", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get number of activities */
|
||||
function numActivities()
|
||||
{
|
||||
$response = callAPIRoute("/activities/all/number", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user number of activities */
|
||||
function numUserActivities($userID)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$userID/number", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user following number of activities */
|
||||
function numFollowedUserActivities($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/activities/user/$user_id/followed/number", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get gear from id */
|
||||
function getActivityFromId($id)
|
||||
{
|
||||
$response = callAPIRoute("/activities/$id", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* adds gear_id to activity */
|
||||
function addGearToActivity($activityID, $gearID)
|
||||
{
|
||||
$response = callAPIRoute("/activities/$activityID/addgear/$gearID", 0, 3, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Uploads activity file */
|
||||
function uploadActivityFile($user_id, $file)
|
||||
{
|
||||
$response = callAPIRoute("/activities/$user_id/create/upload", 1, 6, $file);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 201) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Unset activity gear */
|
||||
function unsetActivityGear($id)
|
||||
{
|
||||
$response = callAPIRoute("/activities/$id/deletegear", 0, 3, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Deletes an activity based on its ID */
|
||||
function deleteActivity($id)
|
||||
{
|
||||
$response = callAPIRoute("/activities/$id/delete", 1, 1, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Activies streams */
|
||||
/* ************************************************************************** */
|
||||
/* Get all activity activity streams */
|
||||
function getActivityActivitiesStream($activity_id)
|
||||
{
|
||||
$response = callAPIRoute("/activities/streams/activity_id/$activity_id/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
#echo $response[0];
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get activity activity streams based on stream_type */
|
||||
function getActivityActivitiesStreamByStreamType($activity_id, $stream_type)
|
||||
{
|
||||
$response = callAPIRoute("/activities/streams/activity_id/$activity_id/stream_type/$stream_type", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
#echo $response[0];
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Creates a new activity stream */
|
||||
function newActivityStream($activity_id, $stream_type, $stream_waypoints, $strava_activity_stream_id)
|
||||
{
|
||||
$response = callAPIRoute("/activities/streams/create", 0, 4, json_encode(array(
|
||||
'activity_id' => $activity_id,
|
||||
'stream_type' => $stream_type,
|
||||
'stream_waypoints' => $stream_waypoints,
|
||||
'strava_activity_stream_id' => $strava_activity_stream_id,
|
||||
)));
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 201) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Followers */
|
||||
/* ************************************************************************** */
|
||||
/* Get if user follows specific user */
|
||||
function getStatusUserFollowsSpecificUser($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/".$_SESSION["id"]."/targetUser/$user_id", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user followers count all */
|
||||
function getUserFollowersCountAll($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/$user_id/followers/count/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user followers count */
|
||||
function getUserFollowersCount($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/$user_id/followers/count/accepted", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user followers all */
|
||||
function getUserFollowersAll($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/$user_id/followers/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user following count all */
|
||||
function getUserFollowingCountAll($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/$user_id/following/count/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user following count */
|
||||
function getUserFollowingCount($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/$user_id/following/count/accepted", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user following all */
|
||||
function getUserFollowingAll($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/user/$user_id/following/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Create user follows specific user */
|
||||
function createUserFollowsSpecificUser($user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/create/user/".$_SESSION["id"]."/targetUser/$user_id", 1, 2, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 201) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Accept user follows specific user */
|
||||
function acceptUserFollowsSpecificUser($user_id, $taget_user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/accept/user/$user_id/targetUser/$taget_user_id", 0, 3, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteUserFollowsSpecificUser($user_id, $taget_user_id)
|
||||
{
|
||||
$response = callAPIRoute("/followers/delete/user/$user_id/targetUser/$taget_user_id", 1, 1, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Gear */
|
||||
/* ************************************************************************** */
|
||||
/* Get all gear */
|
||||
function getGear()
|
||||
{
|
||||
$response = callAPIRoute("/gear/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get all gear from type*/
|
||||
function getGearFromType($gear_type)
|
||||
{
|
||||
$response = callAPIRoute("/gear/type/$gear_type", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get all gear with pagination */
|
||||
function getGearPagination($pageNumber, $numRecords)
|
||||
{
|
||||
$response = callAPIRoute("/gear/page_number/$pageNumber/num_records/$numRecords", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get number of gear */
|
||||
function numGears()
|
||||
{
|
||||
$response = callAPIRoute("/gear/number", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get gear from nickname */
|
||||
function getGearFromNickname($nickname)
|
||||
{
|
||||
$response = callAPIRoute("/gear/nickname/$nickname", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get gear from id */
|
||||
function getGearFromId($gear_id)
|
||||
{
|
||||
$response = callAPIRoute("/gear/id/$gear_id", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Creates a new gear */
|
||||
function newGear($brand, $model, $nickname, $gear_type, $date)
|
||||
{
|
||||
#if (getGearFromNickname($nickname) != NULL) {
|
||||
# return -3;
|
||||
#}
|
||||
|
||||
$response = callAPIRoute("/gear/create", 0, 4, json_encode(array(
|
||||
'brand' => $brand,
|
||||
'model' => $model,
|
||||
'nickname' => $nickname,
|
||||
'gear_type' => $gear_type,
|
||||
'created_at' => $date,
|
||||
)));
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 201) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit gear */
|
||||
function editGear($id, $brand, $model, $nickname, $gear_type, $date, $is_active)
|
||||
{
|
||||
$response = callAPIRoute("/gear/$id/edit", 0, 3, json_encode(array(
|
||||
'brand' => $brand,
|
||||
'model' => $model,
|
||||
'nickname' => $nickname,
|
||||
'gear_type' => $gear_type,
|
||||
'created_at' => $date,
|
||||
'is_active' => $is_active,
|
||||
)));
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Deletes a gear based on its ID */
|
||||
function deleteGear($id)
|
||||
{
|
||||
$response = callAPIRoute("/gear/$id/delete", 1, 1, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Main API Funcs */
|
||||
/* ************************************************************************** */
|
||||
/* Get to call a API route */
|
||||
function callAPIRoute($endpoint, $multipleReturns, $callType, $dataFields)
|
||||
{
|
||||
$api_url = getenv('BACKEND_PROTOCOL').'://'.getenv('BACKEND_HOST');
|
||||
$responseArray = [];
|
||||
|
||||
// Initialize a new cURL session
|
||||
$ch = curl_init();
|
||||
|
||||
// Set the cURL options to make an HTTP GET request to the API endpoint with an OAuth2 bearer token in the Authorization header
|
||||
curl_setopt($ch, CURLOPT_URL, $api_url . $endpoint);
|
||||
|
||||
// 0 GET, 1 DELETE, 2 POST, 3 PUT, 4 and 5 POST with $dataFields already JSON Encoded, 6 POST that will send a file
|
||||
if ($callType == 1) {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
|
||||
} else {
|
||||
if ($callType == 2) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if ($dataFields != NULL) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($dataFields));
|
||||
}
|
||||
} else {
|
||||
if ($callType == 3) {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
|
||||
if ($dataFields != NULL) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataFields);
|
||||
}
|
||||
} else {
|
||||
if ($callType == 4 || $callType == 5) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if ($dataFields != NULL) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataFields);
|
||||
}
|
||||
}else{
|
||||
if ($callType == 6) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if ($dataFields != NULL) {
|
||||
$fileField = 'file';
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, [
|
||||
$fileField => new CURLFile($dataFields),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($multipleReturns == 1) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
"Authorization: Bearer {$_SESSION["token"]}"
|
||||
));
|
||||
} else {
|
||||
if ($callType == 4 || $callType == 3) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer {$_SESSION["token"]}"
|
||||
));
|
||||
} else {
|
||||
if ($callType == 5) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
"Content-Type: application/json"
|
||||
));
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
"Content-Type: application/x-www-form-urlencoded"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// Execute the cURL request and store the response in a variable
|
||||
$response = curl_exec($ch);
|
||||
|
||||
$responseArray[] = $response;
|
||||
$responseArray[] = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $responseArray;
|
||||
}
|
||||
|
||||
function parseResponse($response, $success_code)
|
||||
{
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === $success_code) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
if ($response[1] === 401) {
|
||||
if (json_decode($response[0], true)["detail"] === "Token no longer valid"){
|
||||
#clearUserRelatedInfoSession();
|
||||
#header("location: ../logout.php?sessionExpired=1");
|
||||
}
|
||||
}else{
|
||||
if ($response[1] === 403) {
|
||||
return -2;
|
||||
}else{
|
||||
if ($response[1] === 409) {
|
||||
#return {
|
||||
# "error_code" => $response[1],
|
||||
# "error_message" => json_decode($response[0], true)["detail"],
|
||||
#};
|
||||
}else{
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Session info */
|
||||
/* ************************************************************************** */
|
||||
|
||||
/* Check if a user is logged */
|
||||
function isLogged()
|
||||
{
|
||||
if (isset($_SESSION["id"])) {
|
||||
if ($_SESSION["id"] >= 0) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Check if user token is valid */
|
||||
function isTokenValid($token)
|
||||
{
|
||||
$response = callAPIRoute("/validate_token", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Do a login */
|
||||
function loginUser($username, $password, $neverExpires)
|
||||
{
|
||||
$response = callAPIRoute("/token", 0, 2, array(
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'do_not_expire' => $neverExpires,
|
||||
));
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/* Unset user info */
|
||||
function clearUserRelatedInfoSession()
|
||||
{
|
||||
unset($_SESSION["token"]);
|
||||
unset($_SESSION["id"]);
|
||||
unset($_SESSION["username"]);
|
||||
unset($_SESSION["email"]);
|
||||
unset($_SESSION["name"]);
|
||||
unset($_SESSION["city"]);
|
||||
unset($_SESSION["birthdate"]);
|
||||
unset($_SESSION["preferred_language"]);
|
||||
unset($_SESSION["gender"]);
|
||||
unset($_SESSION["access_type"]);
|
||||
unset($_SESSION["photo_path"]);
|
||||
unset($_SESSION["photo_path_aux"]);
|
||||
}
|
||||
|
||||
/* Set user info */
|
||||
function setUserRelatedInfoSession($token)
|
||||
{
|
||||
clearUserRelatedInfoSession();
|
||||
|
||||
$_SESSION["token"] = $token;
|
||||
|
||||
$response = callAPIRoute("/users/me", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
$user = json_decode($response[0], true);
|
||||
if ($user['is_active'] == 0) {
|
||||
clearUserRelatedInfoSession();
|
||||
return -3;
|
||||
}
|
||||
// Populate the $_SESSION variable with user information
|
||||
// $_SESSION["token"] = $token;
|
||||
$_SESSION["id"] = $user['id'];
|
||||
$_SESSION["name"] = $user['name'];
|
||||
$_SESSION["username"] = $user['username'];
|
||||
$_SESSION["email"] = $user['email'];
|
||||
$_SESSION["city"] = $user['city'];
|
||||
#$_SESSION["birthdate"] = date("d/m/Y", strtotime($user['birthdate']));
|
||||
$_SESSION["birthdate"] = $user['birthdate'];
|
||||
$_SESSION["preferred_language"] = $user['preferred_language'];
|
||||
$_SESSION["gender"] = $user['gender'];
|
||||
$_SESSION["access_type"] = $user['access_type'];
|
||||
$_SESSION["photo_path"] = $user['photo_path'];
|
||||
$_SESSION["photo_path_aux"] = $user['photo_path_aux'];
|
||||
$_SESSION["is_strava_linked"] = $user['is_strava_linked'];
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Strava */
|
||||
/* ************************************************************************** */
|
||||
/* Generate unique user state for strava link */
|
||||
function setUniqueUserStateStravaLink($state)
|
||||
{
|
||||
$response = callAPIRoute("/strava/set-user-unique-state/$state", 0, 3, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsetUniqueUserStateStravaLink()
|
||||
{
|
||||
$response = callAPIRoute("/strava/unset-user-unique-state", 0, 3, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function linkStrava($state)
|
||||
{
|
||||
$client_id = '115321';
|
||||
|
||||
$redirect_uri = urlencode(getenv('BACKEND_PROTOCOL').'://'.getenv('BACKEND_HOST').'/strava/link');
|
||||
$scope = 'read,read_all,profile:read_all,activity:read,activity:read_all';
|
||||
|
||||
$strava_auth_url = "http://www.strava.com/oauth/authorize?client_id={$client_id}&response_type=code&redirect_uri={$redirect_uri}&approval_prompt=force&scope={$scope}&state={$state}";
|
||||
|
||||
|
||||
#header("Location: " . $strava_auth_url);
|
||||
echo "<script>location.href = '$strava_auth_url';</script>";
|
||||
#<meta http-equiv="Location" content=$strava_auth_url>
|
||||
}
|
||||
|
||||
function getStravaActivitiesLastDays($days)
|
||||
{
|
||||
$response = callAPIRoute("/strava/activities/days/$days", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 202) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStravaGear()
|
||||
{
|
||||
$response = callAPIRoute("/strava/gear", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 202) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
<?php
|
||||
/* ************************************************************************** */
|
||||
/* Users */
|
||||
/* ************************************************************************** */
|
||||
/* Get all users */
|
||||
function getUsers()
|
||||
{
|
||||
$response = callAPIRoute("/users/all", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get all users with pagination */
|
||||
function getUsersPagination($pageNumber, $numRecords)
|
||||
{
|
||||
$response = callAPIRoute("/users/all/page_number/$pageNumber/num_records/$numRecords", 1, 0, NULL);
|
||||
return parseResponse($response, 200);
|
||||
}
|
||||
|
||||
/* Get number of users */
|
||||
function numUsers()
|
||||
{
|
||||
$response = callAPIRoute("/users/number", 1, 0, NULL);
|
||||
return parseResponse($response, 200);
|
||||
}
|
||||
|
||||
/* Get user from username */
|
||||
function getUsersIfContainsUsername($usernameUser)
|
||||
{
|
||||
$response = callAPIRoute("/users/username/contains/$usernameUser", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user from username */
|
||||
function getUserFromUsername($usernameUser)
|
||||
{
|
||||
$response = callAPIRoute("/users/username/$usernameUser", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user from ID */
|
||||
function getUserFromId($id)
|
||||
{
|
||||
$response = callAPIRoute("/users/id/$id", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user from ID */
|
||||
function getUserIdFromUsername($usernameUser)
|
||||
{
|
||||
$response = callAPIRoute("/users/$usernameUser/id", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user photo path from ID */
|
||||
function getUserPhotoFromID($id)
|
||||
{
|
||||
$response = callAPIRoute("/users/$id/photo_path", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get user photo path aux from ID */
|
||||
function getUserPhotoAuxFromID($id)
|
||||
{
|
||||
$response = callAPIRoute("/users/$id/photo_path_aux", 1, 0, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return json_decode($response[0], true);
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Creates a new user */
|
||||
function newUser($name, $username, $email, $password, $gender, $preferred_language, $city, $birthdate, $access_type, $photo_path, $photo_path_aux, $is_active)
|
||||
{
|
||||
if (getUserFromUsername($username) != NULL) {
|
||||
return -3;
|
||||
}
|
||||
|
||||
$response = callAPIRoute("/users/create", 0, 4, json_encode(array(
|
||||
'name' => $name,
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'password' => hash("sha256", $password),
|
||||
'preferred_language' => $preferred_language,
|
||||
'city' => $city,
|
||||
'birthdate' => $birthdate,
|
||||
'gender' => $gender,
|
||||
'access_type' => $access_type,
|
||||
'photo_path' => $photo_path,
|
||||
'photo_path_aux' => $photo_path_aux,
|
||||
'is_active' => $is_active,
|
||||
)));
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 201) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit user */
|
||||
function editUser($name, $username, $email, $id, $preferred_language, $city, $birthdate, $gender, $access_type, $photo_path, $photo_path_aux, $is_active)
|
||||
{
|
||||
$response = callAPIRoute("/users/edit", 0, 3, json_encode(array(
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'preferred_language' => $preferred_language,
|
||||
'city' => $city,
|
||||
'birthdate' => $birthdate,
|
||||
'gender' => $gender,
|
||||
'access_type' => $access_type,
|
||||
'photo_path' => $photo_path,
|
||||
'photo_path_aux' => $photo_path_aux,
|
||||
'is_active' => $is_active,
|
||||
)));
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit user password */
|
||||
function editUserPassword($user_id, $password){
|
||||
$response = callAPIRoute("/users/edit/password", 0, 3, json_encode(array(
|
||||
'id' => $user_id,
|
||||
'password' => hash("sha256", $password),
|
||||
)));
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Unset user photo */
|
||||
function unsetUserPhoto($id)
|
||||
{
|
||||
$response = callAPIRoute("/users/$id/delete-photo", 0, 3, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Deletes a user based on its ID */
|
||||
function deleteUser($id)
|
||||
{
|
||||
$response = callAPIRoute("/users/$id/delete", 1, 1, NULL);
|
||||
if ($response[0] === false) {
|
||||
return -1;
|
||||
} else {
|
||||
if ($response[1] === 200) {
|
||||
return 0;
|
||||
} else {
|
||||
if ($response[1] === 409) {
|
||||
return -409;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/main-api-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/session-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/users-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/gear-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/activities-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/activities-streams-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/followers-funcs.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT']."/inc/func/strava-funcs.php";
|
||||
30
frontend/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="bg-body-tertiary" data-bs-theme="dark" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/logo/logo.png">
|
||||
<link rel="apple-touch-icon" href="/logo/logo.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Endurain</title>
|
||||
</head>
|
||||
<body >
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script>
|
||||
// Detects if the user has set their system to use dark mode
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
// Applies dark theme by setting data-bs-theme="dark"
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
} else {
|
||||
// Applies light theme or removes the attribute for default behavior
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
}
|
||||
|
||||
// Optional: Listen for changes in the preference and adjust on the fly
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||
const newColorScheme = event.matches ? "dark" : "light";
|
||||
document.documentElement.setAttribute('data-bs-theme', newColorScheme);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,802 +0,0 @@
|
||||
<?php
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
|
||||
$page = "index";
|
||||
|
||||
if (!isLogged()) {
|
||||
header("Location: ../login.php");
|
||||
die();
|
||||
}
|
||||
|
||||
if (!isTokenValid($_SESSION["token"])) {
|
||||
header("Location: ../logout.php?sessionExpired=1");
|
||||
die();
|
||||
}
|
||||
|
||||
#header("Location: ../gear/gears.php");
|
||||
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsIndex = include $_SERVER['DOCUMENT_ROOT'] . '/lang/index/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsIndex = include $_SERVER['DOCUMENT_ROOT'] . '/lang/index/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsIndex = include $_SERVER['DOCUMENT_ROOT'] . '/lang/index/en.php';
|
||||
}
|
||||
|
||||
// activities
|
||||
$numUserActivities = 0;
|
||||
$numFollowedUserActivities = 0;
|
||||
$userActivities = [];
|
||||
$pageNumberUserActivities = 1;
|
||||
$pageNumberFollowedUserActivities = 1;
|
||||
$addActivityAction = -9000;
|
||||
$editActivityAction = -9000;
|
||||
$deleteActivityAction = -9000;
|
||||
$users = [];
|
||||
|
||||
// general
|
||||
$numRecords = 5;
|
||||
|
||||
// search user
|
||||
if (isset($_POST["searchUser"]) && (isset($_GET["searchUser"]) && $_GET["searchUser"] == 1)) {
|
||||
$users = getUserFromUsername(urlencode(trim($_POST["templateTopSeachUser"])));
|
||||
if ($users != NULL) {
|
||||
header("Location: ../users/user.php?userID=" . $users["id"]);
|
||||
die();
|
||||
}else{
|
||||
$users = -3;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add action */
|
||||
if (isset($_POST["addActivity"]) && $_GET["addActivity"] == 1) {
|
||||
$fileExtension = pathinfo($_FILES["activityGpxFileAdd"]["name"], PATHINFO_EXTENSION);
|
||||
if ($fileExtension == "gpx") {
|
||||
$uploadDir = 'uploads/';
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$uploadedFile = $uploadDir . mt_rand(100, 999) . chr(mt_rand(97, 122)) . chr(mt_rand(97, 122)) . chr(mt_rand(97, 122)) . mt_rand(100, 999) . "-" . basename($_FILES["activityGpxFileAdd"]["name"]);
|
||||
|
||||
// Move the uploaded file to the specified directory
|
||||
if (move_uploaded_file($_FILES["activityGpxFileAdd"]["tmp_name"], $uploadedFile)) {
|
||||
$addActivityAction = uploadActivityFile($_SESSION["id"], $uploadedFile);
|
||||
unlink($uploadedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["pageNumberActivities"])) {
|
||||
$pageNumberUserActivities = $_GET["pageNumberActivities"];
|
||||
}
|
||||
|
||||
if (isset($_GET["pageNumberFollowedUserActivities"])) {
|
||||
$pageNumberFollowedUserActivities = $_GET["pageNumberFollowedUserActivities"];
|
||||
}
|
||||
|
||||
$userActivities = getUserActivitiesPagination($_SESSION["id"], $pageNumberUserActivities, $numRecords);
|
||||
$numUserActivities = numUserActivities($_SESSION["id"]);
|
||||
$total_pages_user = ceil($numUserActivities / $numRecords);
|
||||
|
||||
$followedUserActivities = getFollowedUserActivitiesPagination($_SESSION["id"], $pageNumberFollowedUserActivities, $numRecords);
|
||||
$numFollowedUserActivities = numFollowedUserActivities($_SESSION["id"]);
|
||||
$total_pages_followed = ceil($numFollowedUserActivities / $numRecords);
|
||||
|
||||
$thisWeekDistances = getUserActivitiesThisWeekDistances($_SESSION["id"]);
|
||||
$thisMonthDistances = getUserActivitiesThisMonthDistances($_SESSION["id"]);
|
||||
?>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Top.php" ?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!--<h1><?php echo $translationsIndex['index_title']; ?></h1>-->
|
||||
</div>
|
||||
|
||||
<!-- Page Container -->
|
||||
<div class="container mt-4">
|
||||
<div class="row row-gap-3">
|
||||
<!-- sidebar zone -->
|
||||
<div class="col-lg-3 col-md-12">
|
||||
<div class="d-none d-lg-block mt-3 mb-3 d-flex justify-content-center">
|
||||
<div class="justify-content-center d-flex">
|
||||
<img src=<?php if (is_null($_SESSION["photo_path"])) {
|
||||
if ($_SESSION["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($_SESSION["photo_path"]);
|
||||
} ?> alt="userPicture" class="rounded-circle" width="120" height="120">
|
||||
</div>
|
||||
<div class="text-center mt-3 mb-3 fw-bold">
|
||||
<a href="../users/user.php?userID=<?php echo ($_SESSION["id"]); ?>">
|
||||
<?php echo $_SESSION["name"]; ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- this week distances zone -->
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_title']; ?>
|
||||
</span>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_run']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if (isset($thisWeekDistances)) { ?>
|
||||
<?php echo number_format(($thisWeekDistances["run"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
0 km
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_bike']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if (isset($thisWeekDistances)) { ?>
|
||||
<?php echo number_format(($thisWeekDistances["bike"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
0 km
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_swim']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if (isset($thisWeekDistances)) { ?>
|
||||
<?php if ($thisWeekDistances["swim"] > 10000) { ?>
|
||||
<?php echo number_format(($thisWeekDistances["swim"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
<?php echo $thisWeekDistances["swim"]; ?> m
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
0 m
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- this month distances zone -->
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisMonthDistances_title']; ?>
|
||||
</span>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_run']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if (isset($thisMonthDistances)) { ?>
|
||||
<?php echo number_format(($thisMonthDistances["run"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
0 km
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_bike']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if (isset($thisMonthDistances)) { ?>
|
||||
<?php echo number_format(($thisMonthDistances["bike"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
0 km
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_userZone_thisWeekDistances_swim']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if (isset($thisMonthDistances)) { ?>
|
||||
<?php if ($thisMonthDistances["swim"] > 10000) { ?>
|
||||
<?php echo number_format(($thisMonthDistances["swim"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
<?php echo $thisMonthDistances["swim"]; ?> m
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
0 m
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addActivityModal">
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity']; ?>
|
||||
</a>
|
||||
|
||||
<!-- Modal add actvity -->
|
||||
<div class="modal fade" id="addActivityModal" tabindex="-1" aria-labelledby="addActivityModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addActivityModal">
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity']; ?>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../index.php?addActivity=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- date fields -->
|
||||
<label for="activityGpxFileAdd"><b>*
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_modal_addGpxFile_placeholder']; ?>
|
||||
</b></label>
|
||||
<input class="form-control" type="file" name="activityGpxFileAdd" accept=".gpx" required>
|
||||
<p>*
|
||||
<?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?php echo $translationsTemplateTop['template_top_global_close']; ?>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" name="addActivity">
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity']; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!-- Error banners -->
|
||||
<?php if ($userActivities == -1 || $userActivities == -2 || $addActivityAction == -1 || $addActivityAction == -2 || $addActivityAction == -3 || $addActivityAction == -4 || $addActivityAction == -5 || $addActivityAction == -6 || $addActivityAction == -7 || $addActivityAction == -8 || $addActivityAction == -9 || $addActivityAction == -10 || $addActivityAction == -11 || (isset($_GET["invalidActivity"]) && $_GET["invalidActivity"] == 1) || $users == -1 || $users == -2 || $users == -3) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($userActivities == -1 || $addActivityAction == -1 || $users == -1) { ?>
|
||||
API ERROR |
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($userActivities == -2 || $addActivityAction == -2 || $users == -2) { ?>
|
||||
API ERROR |
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($addActivityAction == -3) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_fileExtensionNotSupported_-3']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($addActivityAction == -4) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_GPXError_-4']; ?> (-4).
|
||||
<?php } else { ?>
|
||||
<?php if ($addActivityAction == -5 || $addActivityAction == -6 || $addActivityAction == -7 || $addActivityAction == -8 || $addActivityAction == -9 || $addActivityAction == -10 || $addActivityAction == -11) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_createStreams_error_-5-6-7-8-9-10-11']; ?> (-5/-6/-7/-8/-9/-10/-11).
|
||||
<?php } else { ?>
|
||||
<?php if (isset($_GET["invalidActivity"]) && $_GET["invalidActivity"] == 1) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_invalidActivity']; ?>.
|
||||
<?php }else{ ?>
|
||||
<?php if ($users == -3) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_noUsersFound']; ?>.
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
<?php if ($userActivities == NULL || (isset($_GET["userNotFound"]) && $_GET["userNotFound"] == 1)) { ?>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($userActivities == NULL) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_info_searchActivity_NULL']; ?> (NULL).
|
||||
<?php }else{ ?>
|
||||
<?php if (isset($_GET["userNotFound"]) && $_GET["userNotFound"] == 1) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_info_userNotFound']; ?>.
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($addActivityAction == 0 || (isset($_GET["deleteActivity"]) && $_GET["deleteActivity"] == 1)) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($addActivityAction == 0) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_success_activityAdded']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if (isset($_GET["deleteActivity"]) && $_GET["deleteActivity"] == 1) { ?>
|
||||
<?php echo $translationsIndex['index_sidebar_addActivity_success_activityDeleted']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="btn-group mb-3 d-flex" role="group" aria-label="Activities radio toggle button group">
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnRadioUserActivities" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary w-100" for="btnRadioUserActivities"><?php echo $translationsIndex['activity_radio_userActivities']; ?></label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnRadioFollowersActivities" autocomplete="off">
|
||||
<label class="btn btn-outline-primary w-100" for="btnRadioFollowersActivities"><?php echo $translationsIndex['activity_radio_followersActivities']; ?></label>
|
||||
</div>
|
||||
|
||||
<div id="userActivitiesDiv" style="display: block;">
|
||||
<!-- user activities list -->
|
||||
<?php if (isset($userActivities)) { ?>
|
||||
<?php foreach ($userActivities as $activity) { ?>
|
||||
<?php
|
||||
$activityStream = getActivityActivitiesStreamByStreamType($activity["id"],7);
|
||||
if(isset($activityStream)){
|
||||
if($activityStream["stream_type"] == 7){
|
||||
$latlonStream = $activityStream["stream_waypoints"];
|
||||
}
|
||||
}else{
|
||||
$latlonStream = NULL;
|
||||
}
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src=<?php if (is_null($_SESSION["photo_path"])) {
|
||||
if ($_SESSION["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($_SESSION["photo_path"]);
|
||||
} ?> alt="userPicture" class="rounded-circle" width="55" height="55">
|
||||
<div class="ms-3 me-3">
|
||||
<div class="fw-bold">
|
||||
<a href="activities/activity.php?activityID=<?php echo ($activity["id"]); ?>"
|
||||
class="link-underline-opacity-25 link-underline-opacity-100-hover">
|
||||
<?php echo ($activity["name"]); ?>
|
||||
</a>
|
||||
</div>
|
||||
<h7>
|
||||
<?php if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2) {
|
||||
echo '<i class="fa-solid fa-person-running"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 3) {
|
||||
echo '<i class="fa-solid fa-person-running"></i> (Virtual)';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6) {
|
||||
echo '<i class="fa-solid fa-person-biking"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 7) {
|
||||
echo '<i class="fa-solid fa-person-biking"></i> (Virtual)';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 8 || $activity["activity_type"] == 9) {
|
||||
echo '<i class="fa-solid fa-person-swimming"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 10) {
|
||||
echo '<i class="fa-solid fa-dumbbell"></i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?>
|
||||
<?php echo (new DateTime($activity["start_time"]))->format("d/m/y"); ?>@
|
||||
<?php echo (new DateTime($activity["start_time"]))->format("H:i"); ?>
|
||||
<?php if (isset($activity["city"]) || isset($activity["country"])) {
|
||||
echo " - ";
|
||||
} ?>
|
||||
<?php if (isset($activity["city"]) && !empty($activity["city"])) {
|
||||
echo $activity["city"] . ", ";
|
||||
} ?>
|
||||
<?php if (isset($activity["country"]) && !empty($activity["country"])) {
|
||||
echo $activity["country"];
|
||||
} ?>
|
||||
</h7>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-flex">
|
||||
<?php if (isset($activity['strava_activity_id'])) { ?>
|
||||
<a class="btn btn-link btn-lg mt-1" href="https://www.strava.com/activities/<?php echo $activity['strava_activity_id']; ?>" role="button">
|
||||
<i class="fa-brands fa-strava"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-flex mt-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_distance']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if ($activity["activity_type"] != 9) { ?>
|
||||
<?php echo number_format(($activity["distance"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
<?php echo ($activity["distance"]); ?> m
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_time']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php
|
||||
$startDateTime = new DateTime($activity["start_time"]);
|
||||
$endDateTime = new DateTime($activity["end_time"]);
|
||||
$interval = $startDateTime->diff($endDateTime);
|
||||
|
||||
if ($interval->h < 1) {
|
||||
// If the difference is less than one hour
|
||||
echo $interval->i . "m " . $interval->s . "s";
|
||||
} else {
|
||||
// If the difference is one hour or more
|
||||
echo $interval->h . "h " . $interval->i . "m";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<?php if ($activity["activity_type"] != 9 && $activity["activity_type"] != 1) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_elevation_gain']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo ($activity["elevation_gain"]); ?> m
|
||||
<?php } else { ?>
|
||||
<?php if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_pace']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo floor(($activity["pace"] * 1000) / 60) . ":" . number_format((((($activity["pace"] * 1000) / 60) - floor(($activity["pace"] * 1000) / 60)) * 60), 0); ?>
|
||||
min/km
|
||||
<?php } else { ?>
|
||||
<?php if ($activity["activity_type"] == 9) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_pace']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo floor(($activity["pace"] * 100) / 60) . ":" . number_format((((($activity["pace"] * 100) / 60) - floor(($activity["pace"] * 100) / 60)) * 60), 0); ?>
|
||||
min/km
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (isset($latlonStream)) { ?>
|
||||
<div class="ms-3 me-3 <?php if ($activity['strava_activity_id'] == null) {
|
||||
echo "mb-3";
|
||||
} ?>" id="map_<?php echo $activity['id']; ?>" style="height: 300px"></div>
|
||||
<?php } ?>
|
||||
<!--<?php if ($activity['strava_activity_id'] != null) { ?>
|
||||
<div class="mb-3">
|
||||
<span class="fw-lighter ms-3 me-3">
|
||||
<?php echo $translationsIndex['index_activities_stravaText1']; ?><a
|
||||
href="https://www.strava.com/activities/<?php echo $activity['strava_activity_id']; ?>" target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<?php echo $translationsIndex['index_activities_stravaText2']; ?>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php } ?>-->
|
||||
</div>
|
||||
<br>
|
||||
<script>
|
||||
// JavaScript code to create the map for this activity
|
||||
var waypoints = <?php echo json_encode($latlonStream); ?>;
|
||||
var mapId = "map_<?php echo $activity['id']; ?>";
|
||||
|
||||
var map = L.map(mapId, {
|
||||
dragging: false, // Disable panning
|
||||
touchZoom: false, // Disable touch zoom
|
||||
scrollWheelZoom: false, // Disable scroll wheel zoom
|
||||
zoomControl: false // Remove zoom control buttons
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
var latlngs = waypoints.map(function (waypoint) {
|
||||
return [waypoint.lat, waypoint.lon];
|
||||
});
|
||||
|
||||
L.polyline(latlngs, {
|
||||
color: 'blue'
|
||||
}).addTo(map);
|
||||
|
||||
// Calculate the bounds of the polyline and fit the map to those bounds
|
||||
var bounds = L.latLngBounds(latlngs);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
// Add green dot for the first waypoint
|
||||
L.marker([waypoints[0].lat, waypoints[0].lon], {
|
||||
icon: L.divIcon({
|
||||
className: 'bg-success dot'
|
||||
})
|
||||
}).addTo(map);
|
||||
|
||||
// Add red dot for the last waypoint
|
||||
L.marker([waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].lon], {
|
||||
icon: L.divIcon({
|
||||
className: 'bg-danger dot'
|
||||
})
|
||||
}).addTo(map);
|
||||
</script>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
||||
<br>
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <?php if ($pageNumberUserActivities == 1) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?pageNumberActivities=1">«</a></li>
|
||||
<?php for ($i = 1; $i <= $total_pages_user; $i++) { ?>
|
||||
<li class="page-item <?php if ($i == $pageNumberUserActivities) {
|
||||
echo "active";
|
||||
} ?>"><a class="page-link" href="?pageNumberActivities=<?php echo ($i); ?>">
|
||||
<?php echo ($i); ?>
|
||||
</a></li>
|
||||
<?php } ?>
|
||||
<li class="page-item <?php if ($pageNumberUserActivities == $total_pages_user) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?pageNumberActivities=<?php echo ($total_pages_user); ?>">»</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="followersActivitiesDiv" style="display: none;">
|
||||
<!-- user activities list -->
|
||||
<?php if(isset($followedUserActivities)) {
|
||||
foreach ($followedUserActivities as $activity) { ?>
|
||||
<?php $userActivity = getUserFromId($activity["user_id"]); ?>
|
||||
<?php
|
||||
$activityStream = getActivityActivitiesStreamByStreamType($activity["id"],7);
|
||||
if($activityStream["stream_type"] == 7){
|
||||
$latlonStream = $activityStream["stream_waypoints"];
|
||||
}
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src=<?php if (is_null($userActivity["photo_path"])) {
|
||||
if ($userActivity["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($userActivity["photo_path"]);
|
||||
} ?> alt="userPicture" class="rounded-circle" width="55" height="55">
|
||||
<div class="ms-3 me-3">
|
||||
<div class="fw-bold">
|
||||
<a href="users/user.php?userID=<?php echo ($userActivity["id"]); ?>"
|
||||
class="link-underline-opacity-25 link-underline-opacity-100-hover">
|
||||
<?php echo ($userActivity["name"]); ?>
|
||||
</a>
|
||||
<?php echo " | "; ?>
|
||||
<a href="activities/activity.php?activityID=<?php echo ($activity["id"]); ?>"
|
||||
class="link-underline-opacity-25 link-underline-opacity-100-hover">
|
||||
<?php echo ($activity["name"]); ?>
|
||||
</a>
|
||||
</div>
|
||||
<h7>
|
||||
<?php if ($activity["activity_type"] == 1) {
|
||||
echo '<i class="fa-solid fa-person-running"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 8) {
|
||||
echo '<i class="fa-solid fa-person-biking"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 7) {
|
||||
echo '<i class="fa-solid fa-person-biking"></i> (Virtual)';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 9) {
|
||||
echo '<i class="fa-solid fa-person-swimming"></i>';
|
||||
} else {
|
||||
if ($activity["activity_type"] == 10) {
|
||||
echo '<i class="fa-solid fa-dumbbell"></i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?>
|
||||
<?php echo (new DateTime($activity["start_time"]))->format("d/m/y"); ?>@
|
||||
<?php echo (new DateTime($activity["start_time"]))->format("H:i"); ?>
|
||||
<?php if (isset($activity["city"]) || isset($activity["country"])) {
|
||||
echo " - ";
|
||||
} ?>
|
||||
<?php if (isset($activity["city"]) && !empty($activity["city"])) {
|
||||
echo $activity["city"] . ", ";
|
||||
} ?>
|
||||
<?php if (isset($activity["country"]) && !empty($activity["country"])) {
|
||||
echo $activity["country"];
|
||||
} ?>
|
||||
</h7>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-flex mt-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_distance']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php if ($activity["activity_type"] != 9) { ?>
|
||||
<?php echo number_format(($activity["distance"] / 1000), 2); ?> km
|
||||
<?php } else { ?>
|
||||
<?php echo ($activity["distance"]); ?> m
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_time']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php
|
||||
#echo $activity["start_time"];
|
||||
#echo $activity["end_time"];
|
||||
$startDateTime = new DateTime($activity["start_time"]);
|
||||
$endDateTime = new DateTime($activity["end_time"]);
|
||||
$interval = $startDateTime->diff($endDateTime);
|
||||
|
||||
if ($interval->h < 1) {
|
||||
// If the difference is less than one hour
|
||||
echo $interval->i . "m " . $interval->s . "s";
|
||||
} else {
|
||||
// If the difference is one hour or more
|
||||
echo $interval->h . "h " . $interval->i . "m";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<?php if ($activity["activity_type"] != 9 && $activity["activity_type"] != 1) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_elevation_gain']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo ($activity["elevation_gain"]); ?> m
|
||||
<?php } else { ?>
|
||||
<?php if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_pace']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo floor(($activity["pace"] * 1000) / 60) . ":" . number_format((((($activity["pace"] * 1000) / 60) - floor(($activity["pace"] * 1000) / 60)) * 60), 0); ?>
|
||||
min/km
|
||||
<?php } else { ?>
|
||||
<?php if ($activity["activity_type"] == 9) { ?>
|
||||
<span class="fw-lighter">
|
||||
<?php echo $translationsIndex['index_activities_detail_pace']; ?>
|
||||
</span>
|
||||
<br>
|
||||
<?php echo floor(($activity["pace"] * 100) / 60) . ":" . number_format((((($activity["pace"] * 100) / 60) - floor(($activity["pace"] * 100) / 60)) * 60), 0); ?>
|
||||
min/km
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (isset($latlonStream)) { ?>
|
||||
<div class="ms-3 me-3 <?php if ($activity['strava_activity_id'] == null) {
|
||||
echo "mb-3";
|
||||
} ?>" id="map_<?php echo $activity['id']; ?>" style="height: 300px"></div>
|
||||
<?php } ?>
|
||||
<?php if ($activity['strava_activity_id'] != null) { ?>
|
||||
<div class="mb-3">
|
||||
<span class="fw-lighter ms-3 me-3">
|
||||
<?php echo $translationsIndex['index_activities_stravaText1']; ?><a
|
||||
href="https://www.strava.com/activities/<?php echo $activity['strava_activity_id']; ?>" target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<?php echo $translationsIndex['index_activities_stravaText2']; ?>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<br>
|
||||
<script>
|
||||
// JavaScript code to create the map for this activity
|
||||
var waypoints = <?php echo json_encode($latlonStream); ?>;
|
||||
var mapId = "map_<?php echo $activity['id']; ?>";
|
||||
|
||||
var map = L.map(mapId, {
|
||||
dragging: false, // Disable panning
|
||||
touchZoom: false, // Disable touch zoom
|
||||
scrollWheelZoom: false, // Disable scroll wheel zoom
|
||||
zoomControl: false // Remove zoom control buttons
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
var latlngs = waypoints.map(function (waypoint) {
|
||||
return [waypoint.lat, waypoint.lon];
|
||||
});
|
||||
|
||||
L.polyline(latlngs, {
|
||||
color: 'blue'
|
||||
}).addTo(map);
|
||||
|
||||
// Calculate the bounds of the polyline and fit the map to those bounds
|
||||
var bounds = L.latLngBounds(latlngs);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
// Add green dot for the first waypoint
|
||||
L.marker([waypoints[0].lat, waypoints[0].lon], {
|
||||
icon: L.divIcon({
|
||||
className: 'bg-success dot'
|
||||
})
|
||||
}).addTo(map);
|
||||
|
||||
// Add red dot for the last waypoint
|
||||
L.marker([waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].lon], {
|
||||
icon: L.divIcon({
|
||||
className: 'bg-danger dot'
|
||||
})
|
||||
}).addTo(map);
|
||||
</script>
|
||||
<?php }
|
||||
} ?>
|
||||
|
||||
<br>
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <?php if ($pageNumberFollowedUserActivities == 1) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?$pageNumberFollowedUserActivities=1">«</a></li>
|
||||
<?php for ($i = 1; $i <= $total_pages_followed; $i++) { ?>
|
||||
<li class="page-item <?php if ($i == $pageNumberFollowedUserActivities) {
|
||||
echo "active";
|
||||
} ?>"><a class="page-link" href="?$pageNumberFollowedUserActivities=<?php echo ($i); ?>">
|
||||
<?php echo ($i); ?>
|
||||
</a></li>
|
||||
<?php } ?>
|
||||
<li class="page-item <?php if ($pageNumberFollowedUserActivities == $total_pages_followed) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?$pageNumberFollowedUserActivities=<?php echo ($total_pages_followed); ?>">»</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Get references to the radio buttons and the div elements
|
||||
const userActivitiesRadio = document.getElementById('btnRadioUserActivities');
|
||||
const followersActivitiesRadio = document.getElementById('btnRadioFollowersActivities');
|
||||
const userActivitiesDiv = document.getElementById('userActivitiesDiv');
|
||||
const followersActivitiesDiv = document.getElementById('followersActivitiesDiv');
|
||||
|
||||
// Add a change event listener to the radio buttons
|
||||
userActivitiesRadio.addEventListener('change', () => {
|
||||
userActivitiesDiv.style.display = 'block';
|
||||
followersActivitiesDiv.style.display = 'none';
|
||||
});
|
||||
|
||||
followersActivitiesRadio.addEventListener('change', () => {
|
||||
userActivitiesDiv.style.display = 'none';
|
||||
followersActivitiesDiv.style.display = 'block';
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Page Container -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Bottom.php" ?>
|
||||
8
frontend/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
//activity.php translations
|
||||
return [
|
||||
// title zone
|
||||
"activity_title" => "Activity",
|
||||
// error banner zone
|
||||
"activity_addEditGear_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"activity_addEditGear_API_error_-2" => "API return code was not 200",
|
||||
// success banner zone
|
||||
"activity_success_gearAdded" => "Gear added to activity",
|
||||
"activity_success_gearEdited" => "Activity gear edited",
|
||||
"activity_success_gearDeleted" => "Activity gear deleted",
|
||||
// activity title
|
||||
"activity_title_dropdown_seeItOnStrava" => "See it on Strava",
|
||||
"activity_title_dropdown_deleteActivity" => "Delete Activity",
|
||||
"activity_title_dropdown_deleteActivity_modal_body" => "Are you sure you want to delete activity",
|
||||
// activity details
|
||||
"activity_detail_distance" => "Distance",
|
||||
"activity_detail_time" => "Time",
|
||||
"activity_detail_elevationGain" => "Elevation gain",
|
||||
"activity_detail_elevationLoss" => "Elevation loss",
|
||||
"activity_detail_pace" => "pace",
|
||||
"activity_detail_avgSpeed" => "Avg speed",
|
||||
"activity_detail_avgPower" => "Avg power",
|
||||
// other gear
|
||||
"activity_gear_title" => "Gear",
|
||||
"activity_gear_notset" => "Gear not set for this activity",
|
||||
// add gear
|
||||
"activity_gear_addGear_title" => "Add gear to activity",
|
||||
"activity_gear_addGear_label" => "Select gear",
|
||||
"activity_gear_addGear_submit" => "Add gear",
|
||||
// edit gear
|
||||
"activity_gear_editGear_title" => "Edit activity gear",
|
||||
"activity_gear_editGear_submit" => "Edit gear",
|
||||
// delete gear zone
|
||||
"activity_gear_deleteGear_title" => "Delete activity gear",
|
||||
"activity_gear_deleteGear_body" => "Are you sure you want to delete activity gear",
|
||||
// data graph
|
||||
"activity_dataGraph_dataSelection" => "Data selection",
|
||||
"activity_dataGraph_hr" => "Heart Rate",
|
||||
"activity_dataGraph_cad" => "Cadence",
|
||||
"activity_dataGraph_power" => "Power",
|
||||
"activity_dataGraph_ele" => "Elevation",
|
||||
"activity_dataGraph_vel" => "Velocity",
|
||||
"activity_dataGraph_pace" => "Pace",
|
||||
"activity_dataGraph_title" => "Data graph",
|
||||
"activity_dataGraph_downsampleDataInfo" => "Downsample data (200 points)"
|
||||
];
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
//gear.php translations
|
||||
return [
|
||||
// error banner zone
|
||||
"gear_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"gear_API_error_-2" => "API return code was not 200",
|
||||
// success banner zone
|
||||
"gear_success_gearEdited" => "Gear edited.",
|
||||
// gear info zone
|
||||
"gear_gear_infoZone_isactive" => "Active",
|
||||
"gear_gear_infoZone_isinactive" => "Inactive",
|
||||
"gear_gear_infoZone_gearisbike" => "Bike",
|
||||
"gear_gear_infoZone_gearisshoe" => "Shoes",
|
||||
"gear_gear_infoZone_geariswetsuit" => "Wetsuit",
|
||||
"gear_gear_infoZone_strava_gear" => "Synced from Strava",
|
||||
// edit zone
|
||||
"gear_gear_infoZone_editbutton" => "Edit gear",
|
||||
"gear_gear_infoZone_modal_editGear_brandLabel" => "Brand",
|
||||
"gear_gear_infoZone_modal_editGear_brandPlaceholder" => "Brand (max 45 characters)",
|
||||
"gear_gear_infoZone_modal_editGear_modelLabel" => "Model",
|
||||
"gear_gear_infoZone_modal_editGear_modelPlaceholder" => "Model (max 45 characters)",
|
||||
"gear_gear_infoZone_modal_editGear_nicknameLabel" => "Nickname",
|
||||
"gear_gear_infoZone_modal_editGear_nicknamePlaceholder" => "Nickname (max 45 characters)",
|
||||
"gear_gear_infoZone_modal_editGear_gearTypeLabel" => "Gear type",
|
||||
"gear_gear_infoZone_modal_editGear_gearTypeOption1" => "Bike",
|
||||
"gear_gear_infoZone_modal_editGear_gearTypeOption2" => "Shoes",
|
||||
"gear_gear_infoZone_modal_editGear_gearTypeOption3" => "Wetsuit",
|
||||
"gear_gear_infoZone_modal_editGear_dateLabel" => "Created date",
|
||||
"gear_gear_infoZone_modal_editGear_datePlaceholder" => "Date",
|
||||
"gear_gear_infoZone_modal_editGear_gearIsActiveLabel" => "Is active",
|
||||
"gear_gear_infoZone_modal_editGear_gearIsActiveOption1" => "Yes",
|
||||
"gear_gear_infoZone_modal_editGear_gearIsActiveOption2" => "No",
|
||||
// delete zone
|
||||
"gear_gear_infoZone_modal_deleteGear_body" => "Are you sure you want to delete gear",
|
||||
"gear_gear_infoZone_deletebutton" => "Delete gear",
|
||||
// details
|
||||
"gear_gear_infoZone_distance" => "Distance",
|
||||
"gear_gear_infoZone_brand" => "Brand",
|
||||
"gear_gear_infoZone_model" => "Model",
|
||||
// gear activities
|
||||
"gear_gear_gearActivities_noactivities" => "Gear is not linked to any activity",
|
||||
"gear_gear_gearActivities_title" => "Gear activities",
|
||||
"gear_gear_gearActivities_number" => "(last 10 activities)",
|
||||
"gear_gear_gearActivities_datelabel" => "Date"
|
||||
];
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
//gears.php translations
|
||||
return [
|
||||
// title zone
|
||||
"gear_title" => "Gear",
|
||||
// error banner zone
|
||||
"gear_gear_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"gear_gear_API_error_-2" => "API return code was not 200",
|
||||
"gear_gear_error_addGear_-3" => "Nickname already exists. Please verify nickname",
|
||||
"gear_gear_error_editGear_-3" => "Nickname and type field required",
|
||||
// info banner zone
|
||||
"gear_gear_info_searchGear_NULL" => "Gear not found",
|
||||
"gear_gear_info_fromGear_invalidGear" => "Gear not found",
|
||||
// success banner zone
|
||||
"gear_gear_success_gearAdded" => "Gear added.",
|
||||
"gear_gear_success_gearEdited" => "Gear edited.",
|
||||
"gear_gear_success_gearDeleted" => "Gear deleted.",
|
||||
// add gear zone
|
||||
"gear_gear_buttonLabel_addGear" => "Add gear:",
|
||||
"gear_gear_button_addGear" => "Add gear",
|
||||
"gear_gear_modal_addGear_title" => "Add gear",
|
||||
"gear_gear_modal_addEditGear_brandLabel" => "Brand",
|
||||
"gear_gear_modal_addEditGear_brandPlaceholder" => "Brand (max 45 characters)",
|
||||
"gear_gear_modal_addEditGear_modelLabel" => "Model",
|
||||
"gear_gear_modal_addEditGear_modelPlaceholder" => "Model (max 45 characters)",
|
||||
"gear_gear_modal_addEditGear_nicknameLabel" => "Nickname",
|
||||
"gear_gear_modal_addEditGear_nicknamePlaceholder" => "Nickname (max 45 characters)",
|
||||
"gear_gear_modal_addEditUser_gearTypeLabel" => "Gear type",
|
||||
"gear_gear_modal_addEditUser_gearTypeOption1" => "Bike",
|
||||
"gear_gear_modal_addEditUser_gearTypeOption2" => "Shoes",
|
||||
"gear_gear_modal_addEditUser_gearTypeOption3" => "Wetsuit",
|
||||
"gear_gear_modal_addEditGear_dateLabel" => "Created date",
|
||||
"gear_gear_modal_addEditGear_datePlaceholder" => "Date",
|
||||
// search gear zone
|
||||
"gear_gear_buttonLabel_searchGear" => "Search gear by nickname:",
|
||||
"gear_gear_button_searchGear" => "Search",
|
||||
"gear_gear_form_searchGear_nicknamePlaceholder" => "Nickname",
|
||||
// list gears
|
||||
// error banner zone
|
||||
"gear_gear_error_listGear_-1-2" => "It was not possible to list gear",
|
||||
// info banner zone
|
||||
"gear_gear_error_listGear_-3" => "Gear does not exist",
|
||||
// list
|
||||
"gear_gear_list_title1" => "There is a total of",
|
||||
"gear_gear_list_title2" => " gear(s)",
|
||||
"gear_gear_list_title3" => " per page)",
|
||||
"gear_gear_gear_type" => "Type: ",
|
||||
"gear_gear_list_isactive" => "Active",
|
||||
"gear_gear_list_isinactive" => "Inactive",
|
||||
"gear_gear_list_button_components" => "Gear components",
|
||||
// edit gear zone
|
||||
"gear_gear_modal_editGear_title" => "Edit gear",
|
||||
"gear_gear_modal_editUser_gearIsActiveLabel" => "Is active",
|
||||
"gear_gear_modal_editUser_gearIsActiveOption1" => "Yes",
|
||||
"gear_gear_modal_editUser_gearIsActiveOption2" => "No",
|
||||
// delete gear zone
|
||||
"gear_gear_modal_deleteGear_title" => "Delete gear",
|
||||
"gear_gear_modal_deleteGear_body" => "Are you sure you want to delete gear"
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
//Template-Top.php translations
|
||||
return [
|
||||
"template_top_navbar_search" => "Search",
|
||||
"template_top_global_searchUserModal_title" => "Search user",
|
||||
"template_top_global_searchUserModal_usernameLabel" => "Username",
|
||||
"template_top_global_searchUserModal_usernamePlaceholder" => "Username",
|
||||
"template_top_navbar_gear" => "Gear",
|
||||
"template_top_navbar_login" => "Login",
|
||||
"template_top_navbar_profile" => "Profile",
|
||||
"template_top_navbar_settings" => "Settings",
|
||||
"template_top_navbar_logout" => "Logout",
|
||||
// back section
|
||||
"template_top_global_back" => "Back",
|
||||
"template_top_global_requiredFields" => "Required field(s)",
|
||||
"template_top_global_close" => "Close",
|
||||
"template_top_global_button_listAll" => "List all",
|
||||
"template_top_warning_zone" => "Alpha software, some features might not work has expected"
|
||||
];
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
//index.php translations
|
||||
return [
|
||||
// title zone
|
||||
"index_title" => "Feed",
|
||||
"index_userZone_thisWeekDistances_title" => "This week distances",
|
||||
"index_userZone_thisWeekDistances_run" => "Run",
|
||||
"index_userZone_thisWeekDistances_bike" => "Bike",
|
||||
"index_userZone_thisWeekDistances_swim" => "Swim",
|
||||
"index_userZone_thisMonthDistances_title" => "This month distances",
|
||||
// add activity
|
||||
"index_sidebar_addActivity" => "Add Activity",
|
||||
"index_sidebar_addActivity_modal_addGpxFile_placeholder" => "Add GPX activity",
|
||||
// error banner zone
|
||||
"index_sidebar_addActivity_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"index_sidebar_addActivity_API_error_-2" => "API return code was not 200",
|
||||
"index_sidebar_addActivity_GPXError_-3" => "Invalid GPX file or could not load the file",
|
||||
"index_sidebar_addActivity_GPXError_-4" => "Invalid GPX file or could not load the file",
|
||||
"index_sidebar_addActivity_createStreams_error_-5-6-7-8-9-10-11" => "Unable to create activity stream",
|
||||
"index_sidebar_invalidActivity" => "Invalid activity. Activity ID not valid or you might not have access to see it",
|
||||
"index_sidebar_noUsersFound" => "No user found with that username",
|
||||
// info banner zone
|
||||
"index_sidebar_addActivity_info_searchActivity_NULL" => "Activities not found",
|
||||
"index_sidebar_info_userNotFound" => "User not found",
|
||||
// success banner zone
|
||||
"index_sidebar_addActivity_success_activityAdded" => "Activity added.",
|
||||
"index_sidebar_addActivity_success_activityDeleted" => "Activity deleted.",
|
||||
// radio zone
|
||||
"activity_radio_userActivities" => "My activities",
|
||||
"activity_radio_followersActivities" => "Followers activities",
|
||||
// activities zone
|
||||
"index_activities_type_title" => "Activity added.",
|
||||
"index_activities_detail_distance" => "Distance",
|
||||
"index_activities_detail_time" => "Time",
|
||||
"index_activities_detail_elevation_gain" => "Elevation gain",
|
||||
"index_activities_detail_pace" => "Pace",
|
||||
"index_activities_stravaText1" => "Strava activitity, see it ",
|
||||
"index_activities_stravaText2" => "here"
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
//login.php translations
|
||||
return [
|
||||
"login_info_session expired" => "User session expired",
|
||||
"login_error_access_token_not_set" => "Unable to login. Access token not set",
|
||||
"login_error_user_inactive" => "User is inactive. Unable to login",
|
||||
"login_error_incorrect_user_credentials" => "Incorrect user credentials. Check username and password",
|
||||
"login_error_undefined" => "Unable to login. Undefined",
|
||||
"login_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"login_API_error_-2" => "API return code was not 200",
|
||||
"login_API_error_-3" => "User state is inactive. Unable to login",
|
||||
"login_subtitle" => "Sign-in bellow",
|
||||
"login_insert_username" => "Insert username",
|
||||
"login_password" => "Password",
|
||||
"login_neverExpires" => "Remember sign in",
|
||||
"login_login" => "Sign in",
|
||||
"login_signup_text" => "Looking for signing up?",
|
||||
"login_signup_button" => "Sign up",
|
||||
// Add more translations here
|
||||
];
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
//settings.php translations
|
||||
return [
|
||||
// title zone
|
||||
"settings_title" => "Settings",
|
||||
"settings_sidebar_users" => "Users",
|
||||
"settings_sidebar_global" => "Global settings",
|
||||
"settings_sidebar_profileSettings" => "My profile",
|
||||
"settings_sidebar_securitySettings" => "Security",
|
||||
"settings_sidebar_integrationsSettings" => "Integrations",
|
||||
// error banner zone
|
||||
"settings_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"settings_API_error_-2" => "API return code was not 200",
|
||||
// info banner zone
|
||||
|
||||
// success banner zone
|
||||
|
||||
];
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
// integration-settings.php translations
|
||||
return [
|
||||
// success banner zone
|
||||
"settings_integration_settings_success_stravaLinked" => "Strava linked with success",
|
||||
"settings_integration_settings_success_stravaGear" => "Strava gear retrieved",
|
||||
"settings_integration_settings_success_stravaActivities" => "Strava activities retrieval background process started",
|
||||
// card zone
|
||||
"settings_integration_settings_strava_title" => "Strava",
|
||||
"settings_integration_settings_strava_body" => "Strava is an American internet service for tracking physical exercise which incorporates social network features.",
|
||||
"settings_integration_settings_strava_body_linked" => "Strava is linked to your account",
|
||||
// general
|
||||
"settings_integration_settings_connect_button" => "Connect",
|
||||
"settings_integration_settings_retrieve_last_week_activities_button" => "Retrieve last week activities",
|
||||
"settings_integration_settings_retrieve_gear_button" => "Retrieve gear",
|
||||
];
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
// profile-settings.php translations
|
||||
return [
|
||||
// error banner zone
|
||||
"settings_sidebar_profile_error_deleteProfilePhoto_-3" => "It was not possible to delete profile photo on filesystem. Database not updated",
|
||||
"settings_profile_settings_error_editProfile_-3" => "Photo path invalid",
|
||||
"settings_profile_settings_error_editProfile_-4" => "Only photos with extension .jpg, .jpeg e .png are allowed",
|
||||
"settings_profile_settings_error_editProfile_-5" => "It was not possible to upload photo",
|
||||
// info banner zone
|
||||
|
||||
// success banner zone
|
||||
"settings_sidebar_profile_success_profilePhotoDeleted" => "Profile photo deleted.",
|
||||
"settings_sidebar_profile_success_profileEdited" => "Profile edited",
|
||||
|
||||
// edit profile modal zone
|
||||
"settings_sidebar_profile_editProfile_title" => "Edit profile",
|
||||
"settings_sidebar_profile_photo_label" => "Profile photo",
|
||||
"settings_sidebar_profile_username_label" => "Username",
|
||||
"settings_sidebar_profile_username_placeholder" => "Username (max 45 characters)",
|
||||
"settings_sidebar_profile_name_label" => "Name",
|
||||
"settings_sidebar_profile_name_placeholder" => "Name (max 45 characters)",
|
||||
"settings_sidebar_profile_email_label" => "Email",
|
||||
"settings_sidebar_profile_email_placeholder" => "Email (max 45 characters)",
|
||||
"settings_sidebar_profile_city_label" => "Town/city",
|
||||
"settings_sidebar_profile_city_placeholder" => "Town/city (max 45 characters)",
|
||||
"settings_sidebar_profile_birthdate_label" => "Birth date",
|
||||
"settings_sidebar_profile_gender_label" => "Gender",
|
||||
"settings_sidebar_profile_gender_option1" => "Male",
|
||||
"settings_sidebar_profile_gender_option2" => "Female",
|
||||
"settings_sidebar_profile_preferredLanguage_label" => "Prefered language",
|
||||
"settings_sidebar_profile_preferredLanguage_option1" => "English",
|
||||
"settings_sidebar_profile_preferredLanguage_option2" => "Portuguese",
|
||||
// profile zone
|
||||
"settings_sidebar_profile_deleteProfilePhoto" => "Delete profile photo",
|
||||
"settings_sidebar_profile_modal_title_deleteProfilePhoto" => "Delete profile photo",
|
||||
"settings_sidebar_profile_modal_body_deleteProfilePhoto" => "Are you sure you want to delete the profile photo?",
|
||||
"settings_sidebar_profile_button_editprofile" => "Edit profile",
|
||||
"settings_sidebar_profile_gender_male" => "Male",
|
||||
"settings_sidebar_profile_gender_female" => "Female",
|
||||
"settings_sidebar_profile_access_type_regular_user" => "Regular user",
|
||||
"settings_sidebar_profile_access_type_admin" => "Admin",
|
||||
"settings_sidebar_profile_username_subtitle" => "Username: ",
|
||||
"settings_sidebar_profile_email_subtitle" => "Email: ",
|
||||
"settings_sidebar_profile_gender_subtitle" => "Gender: ",
|
||||
"settings_sidebar_profile_birthdate_subtitle" => "Birthdate: ",
|
||||
"settings_sidebar_profile_city_subtitle" => "City: ",
|
||||
"settings_sidebar_profile_access_type_subtitle" => "Access type: ",
|
||||
"settings_sidebar_profile_preferredlanguage_subtitle" => "Preferred language: ",
|
||||
];
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
// security-settings.php translations
|
||||
return [
|
||||
// error banner zone
|
||||
"settings_security_settings_error_passwords_dont_match-3" => "Passwords don't match",
|
||||
"settings_security_settings_error_password_complexity-4" => "Password complexity not met",
|
||||
|
||||
// info banner zone
|
||||
"settings_security_settings_info_password_requirements" => "Password requirements includes:",
|
||||
"settings_security_settings_info_password_requirements_characters" => " - 8 characters;",
|
||||
"settings_security_settings_info_password_requirements_capital_letters" => " - 1 capital letter;",
|
||||
"settings_security_settings_info_password_requirements_numbers" => " - 1 number;",
|
||||
"settings_security_settings_info_password_requirements_special_characters" => " - 1 special character.",
|
||||
|
||||
// success banner zone
|
||||
"settings_security_settings_success_password_edited" => "User password edited",
|
||||
|
||||
// form change password
|
||||
"settings_security_settings_subtitle_change_password" => "Change password",
|
||||
"settings_security_settings_subtitle_change_password_password" => "Password",
|
||||
"settings_security_settings_subtitle_change_password_repeat_password" => "Repeat password",
|
||||
"settings_security_settings_subtitle_change_password_button" => "Change password",
|
||||
];
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
// users-settings.php translations
|
||||
return [
|
||||
// error banner zone
|
||||
"settings_users_settings_error_addUser_-3" => "Username already exists. Please verify username",
|
||||
"settings_users_settings_error_addEditUser_-4" => "Photo path invalid",
|
||||
"settings_users_settings_error_addEditUser_-5" => "Only photos with extension .jpg, .jpeg e .png are allowed",
|
||||
"settings_users_settings_error_addEditUser_-6" => "It was not possible to upload photo",
|
||||
"settings_users_settings_error_editUser_-3" => "Username and type field required",
|
||||
"settings_users_settings_error_deleteUser_-3" => "User cannot delete himself",
|
||||
"settings_users_settings_error_deleteUser_-409" => "User has dependencies. It is not possible to delete",
|
||||
"settings_users_settings_error_deleteUserPhoto_-3" => "It was not possible to delete user photo on filesystem. Database not updated",
|
||||
"settings_users_settings_error_passwords_dont_match-3" => "Passwords don't match",
|
||||
"settings_users_settings_error_password_complexity-4" => "Password complexity not met",
|
||||
"settings_users_settings_error_user_id_not_valid-5" => "User ID not valid",
|
||||
// info banner zone
|
||||
"settings_users_settings_info_userDeleted_photoNotDeleted" => "User deleted. It was not possible to delete user photo from filesystem.",
|
||||
"settings_users_settings_error_searchUser_NULL" => "User not found",
|
||||
// success banner zone
|
||||
"settings_users_settings_success_userAdded" => "User added.",
|
||||
"settings_users_settings_success_userEdited" => "User edited.",
|
||||
"settings_users_settings_success_userDeleted" => "User deleted.",
|
||||
"settings_users_settings_success_userPhotoDeleted" => "User photo deleted.",
|
||||
"settings_users_settings_success_password_edited" => "User password edited",
|
||||
// users zone
|
||||
"settings_users_settings_new" => "New user",
|
||||
"settings_users_settings_search" => "Search",
|
||||
"settings_sidebar_button_addUser" => "Add user",
|
||||
// add user modal
|
||||
"settings_users_settings_modal_addUser_title" => "Add user",
|
||||
"settings_users_settings_modal_addEditUser_photoLabel" => "User photo",
|
||||
"settings_users_settings_modal_addEditUser_usernameLabel" => "Username",
|
||||
"settings_users_settings_modal_addEditUser_usernamePlaceholder" => "Username (max 45 characters)",
|
||||
"settings_users_settings_modal_addEditUser_emailLabel" => "Email",
|
||||
"settings_users_settings_modal_addEditUser_emailPlaceholder" => "Email (max 45 characters)",
|
||||
"settings_users_settings_modal_addEditUser_nameLabel" => "Name",
|
||||
"settings_users_settings_modal_addEditUser_namePlaceholder" => "Name (max 45 characters)",
|
||||
"settings_users_settings_modal_addEditUser_passwordLabel" => "Password",
|
||||
"settings_users_settings_modal_addEditUser_passwordPlaceholder" => "Password",
|
||||
"settings_users_settings_modal_addEditUser_cityLabel" => "Town/city",
|
||||
"settings_users_settings_modal_addEditUser_cityPlaceholder" => "Town/city (max 45 characters)",
|
||||
"settings_users_settings_modal_addEditUser_birthdateLabel" => "Birth date",
|
||||
"settings_users_settings_modal_addEditUser_genderLabel" => "User gender",
|
||||
"settings_users_settings_modal_addEditUser_genderOption1" => "Male",
|
||||
"settings_users_settings_modal_addEditUser_genderOption2" => "Female",
|
||||
"settings_users_settings_modal_addEditUser_preferredLanguageLabel" => "User prefered language",
|
||||
"settings_users_settings_modal_addEditUser_preferredLanguageOption1" => "English",
|
||||
"settings_users_settings_modal_addEditUser_preferredLanguageOption2" => "Portuguese",
|
||||
"settings_users_settings_modal_addEditUser_typeLabel" => "User type",
|
||||
"settings_users_settings_modal_addEditUser_typeOption1" => "Regular user",
|
||||
"settings_users_settings_modal_addEditUser_typeOption2" => "Administrator",
|
||||
"settings_users_settings_modal_addEditUser_notesLabel" => "Notes",
|
||||
"settings_users_settings_modal_addEditUser_notesPlaceholder" => "Notes (max 250 characters)",
|
||||
"settings_users_settings_modal_addEditUser_isActiveLabel" => "Is active",
|
||||
"settings_users_settings_modal_addEditUser_isActiveOption1" => "Yes",
|
||||
"settings_users_settings_modal_addEditUser_isActiveOption2" => "No",
|
||||
// search user zone
|
||||
"settings_sidebar_form_searchUser_usernamePlaceholder" => "Username",
|
||||
// users list
|
||||
"settings_users_settings_list_title1" => "There is a total of",
|
||||
"settings_users_settings_list_title2" => " user(s)",
|
||||
"settings_users_settings_list_title3" => " per page)",
|
||||
"settings_users_settings_list_user_accesstype" => "Access type: ",
|
||||
"settings_users_settings_list_isactive" => "Active",
|
||||
"settings_users_settings_list_isinactive" => "Inactive",
|
||||
// edit user zone
|
||||
"settings_users_settings_modal_editUser_title" => "Edit user",
|
||||
// delete user photo zone
|
||||
"settings_users_settings_modal_editUser_deleteUserPhoto" => "Delete photo",
|
||||
// delete user zone
|
||||
"settings_users_settings_modal_deleteUser_title" => "Delete user",
|
||||
"settings_users_settings_modal_deleteUser_body" => "Are you sure you want to delete user",
|
||||
// edit user password zone
|
||||
// info banner zone
|
||||
"settings_users_settings_info_password_requirements" => "Password requirements includes:",
|
||||
"settings_users_settings_info_password_requirements_characters" => " - 8 characters;",
|
||||
"settings_users_settings_info_password_requirements_capital_letters" => " - 1 capital letter;",
|
||||
"settings_users_settings_info_password_requirements_numbers" => " - 1 number;",
|
||||
"settings_users_settings_info_password_requirements_special_characters" => " - 1 special character.",
|
||||
// modal
|
||||
"settings_users_settings_modal_changeUserPassword_title" => "Change password",
|
||||
"settings_users_settings_modal_changeUserPassword_body" => "Change password for user ",
|
||||
"settings_users_settings_modal_change_password_password" => "Password",
|
||||
"settings_users_settings_modal_change_password_repeat_password" => "Repeat password",
|
||||
];
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
//user.php translations
|
||||
return [
|
||||
// title zone
|
||||
"user_title" => "User",
|
||||
// error banner zone
|
||||
"user_follow_API_error_-1" => "Connection to API not possible. Failed to execute cURL request",
|
||||
"user_follow_API_error_-2" => "API return code was not 200",
|
||||
// success banner zone
|
||||
"user_follow_success" => "Request sent to follow user",
|
||||
"user_unfollow_success" => "User unfollowed successfully",
|
||||
"user_follow_request_declined" => "Follow request declined successfully",
|
||||
"user_follow_request_accepted" => "Follow request accepted successfully",
|
||||
// Stats zone
|
||||
"user_stats_numberActivitiesMonth" => "This month activities",
|
||||
// distances zone
|
||||
"user_distances_zone_thisWeekDistances_title" => "Current week distances",
|
||||
"user_distances_zone_thisWeekDistances_run" => "Run",
|
||||
"user_distances_zone_thisWeekDistances_bike" => "Bike",
|
||||
"user_distances_zone_thisWeekDistances_swim" => "Swim",
|
||||
"user_distances_zone_thisMonthDistances_title" => "Current month distances",
|
||||
// main zone
|
||||
"user_main_nav_activities" => "Activities",
|
||||
"user_main_nav_following" => "Following",
|
||||
"user_main_nav_followers" => "Followers",
|
||||
"user_main_nav_user_settings" => "User settings",
|
||||
"user_main_nav_followStatus_follow" => "Follow",
|
||||
"user_main_nav_followStatus_requestSent" => "Request sent",
|
||||
"user_main_nav_followStatus_unfollow" => "Unfollow",
|
||||
// follow modal
|
||||
"user_follow_modal_title" => "Follow user",
|
||||
"user_follow_modal_body" => "Are you sure you want to follow user ",
|
||||
// cancel follow modal
|
||||
"user_cancelFollow_modal_title" => "Cancel follow request",
|
||||
"user_cancelFollow_modal_body" => "Are you sure you want to cancel follow request for user ",
|
||||
// unfollow modal
|
||||
"user_unfollow_modal_title" => "Unfollow user",
|
||||
"user_unfollow_modal_body" => "Are you sure you want to unfollow user ",
|
||||
// activities zone
|
||||
"user_activities_zone_ops" => "Ops...",
|
||||
"user_activities_zone_noActivitiesFound" => "No activities found for this period",
|
||||
"user_activities_zone_dateRange1" => "Activities from ",
|
||||
"user_activities_zone_dateRange2" => " to ",
|
||||
"user_activities_zone_date_current" => "This week",
|
||||
"user_activities_zone_date_oneYearAgo" => "One year ago",
|
||||
"user_activities_zone_detail_distance" => "Distance",
|
||||
"user_activities_zone_detail_time" => "Time",
|
||||
"user_activities_zone_detail_elevation_gain" => "Elevation gain",
|
||||
"user_activities_zone_detail_pace" => "Pace",
|
||||
"user_activities_zone_stravaText1" => "Strava activitity, see it ",
|
||||
"user_activities_zone_stravaText2" => "here",
|
||||
// following zone
|
||||
"user_following_zone_noFollowers" => "No followers found for this user",
|
||||
"user_following_zone_noFollowing" => "This user is not following anyone",
|
||||
"user_following_zone_requestAccepted" => "Accepted",
|
||||
"user_following_zone_requestPending" => "Request pending",
|
||||
// modal delete following
|
||||
"user_deleteFollowing_modal_title" => "Delete following",
|
||||
"user_deleteFollowing_modal_body" => "Are you sure you want to delete following user",
|
||||
// modal delete follower
|
||||
"user_deleteFollower_modal_title" => "Delete follower",
|
||||
"user_deleteFollower_modal_body" => "Are you sure you want to delete follower user",
|
||||
// modal accept user request
|
||||
"user_acceptUserRequest_modal_title" => "Accept user request",
|
||||
"user_acceptUserRequest_modal_body" => "Are you sure you want to accept follow request from user",
|
||||
// modal decline user request
|
||||
"user_declineUserRequest_modal_title" => "Decline user request",
|
||||
"user_declineUserRequest_modal_body" => "Are you sure you want to decline follow request from user",
|
||||
];
|
||||
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
|
||||
$page = "login";
|
||||
|
||||
if (isLogged()) {
|
||||
header("Location: ../index.php");
|
||||
die();
|
||||
}
|
||||
|
||||
// Load the en language file
|
||||
$translationsLogin = include $_SERVER['DOCUMENT_ROOT'] . '/lang/login/en.php';
|
||||
|
||||
$error = 0;
|
||||
|
||||
if (isset($_POST["loginUsername"]) && isset($_POST["loginPassword"])) {
|
||||
clearUserRelatedInfoSession();
|
||||
$hashPassword = hash("sha256", $_POST["loginPassword"]);
|
||||
$neverExpires = false;
|
||||
if (isset($_POST["loginNeverExpires"]) && $_POST["loginNeverExpires"] === "on") {
|
||||
$neverExpires = true;
|
||||
}
|
||||
$result = loginUser($_POST["loginUsername"], $hashPassword, $neverExpires);
|
||||
|
||||
if ($result[1] === 200) {
|
||||
$responseContent = json_decode($result[0], true);
|
||||
if (isset($responseContent['access_token'])) {
|
||||
$error = setUserRelatedInfoSession($responseContent['access_token']);
|
||||
if ($error == 0) {
|
||||
header("Location: ../index.php");
|
||||
die();
|
||||
}
|
||||
}else{
|
||||
$error = 1;
|
||||
}
|
||||
} else {
|
||||
if ($result[1] === 400) {
|
||||
if(json_decode($result[0], true)["error"]["message"] === "User is not active"){
|
||||
$error = 2;
|
||||
}else{
|
||||
if(json_decode($result[0], true)["error"]["message"] == "Incorrect username or password"){
|
||||
$error = 3;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
$error = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Top.php" ?>
|
||||
|
||||
<main class="form-signin w-100 m-auto text-center p-5" style="max-width: 500px">
|
||||
<!-- Error banners -->
|
||||
<?php if ($error == 1 || $error == 2 || $error == 3 || $error == 4 || $error == -1 || $error == -3 || $error == -3) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($error == 1) { ?>
|
||||
<?php echo $translationsLogin['login_error_access_token_not_set']; ?> (1).
|
||||
<?php } else { ?>
|
||||
<?php if ($error == 2) { ?>
|
||||
<?php echo $translationsLogin['login_error_user_inactive']; ?> (2).
|
||||
<?php } else { ?>
|
||||
<?php if ($error == 3) { ?>
|
||||
<?php echo $translationsLogin['login_error_incorrect_user_credentials']; ?> (3).
|
||||
<?php } else { ?>
|
||||
<?php if ($error == 4) { ?>
|
||||
<?php echo $translationsLogin['login_error_undefined']; ?> (4).
|
||||
<?php } else { ?>
|
||||
<?php if ($error == -1) { ?>
|
||||
<?php echo $translationsLogin['login_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($error == -2) { ?>
|
||||
<?php echo $translationsLogin['login_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($error == -3) { ?>
|
||||
<?php echo $translationsLogin['login_API_error_-3']; ?> (-3).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
<?php if (isset($_GET["sessionExpired"]) && $_GET["sessionExpired"] == 1) { ?>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if (isset($_GET["sessionExpired"]) && $_GET["sessionExpired"] == 1) { ?>
|
||||
<?php echo $translationsLogin['login_info_session expired']; ?>.
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<form action="../login.php" method="post">
|
||||
<h1>Endurain</h1>
|
||||
<p><?php echo $translationsLogin['login_subtitle']; ?></p>
|
||||
<br>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="floatingInput" placeholder="<?php echo $translationsLogin['login_insert_username']; ?>" name="loginUsername" required>
|
||||
<label for="loginUsername"><?php echo $translationsLogin['login_insert_username']; ?></label>
|
||||
</div>
|
||||
<br>
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" placeholder="<?php echo $translationsLogin['login_password']; ?>" name="loginPassword" required>
|
||||
<label for="loginPassword"><?php echo $translationsLogin['login_password']; ?></label>
|
||||
</div>
|
||||
<br>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" name="loginNeverExpires">
|
||||
<label class="form-check-label" for="loginNeverExpires"><?php echo $translationsLogin['login_neverExpires']; ?></label>
|
||||
</div>
|
||||
<br>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit"><?php echo $translationsLogin['login_login']; ?></button>
|
||||
<!--<div>
|
||||
<br>
|
||||
<p><?php echo $translationsLogin['login_signup_text']; ?></p>
|
||||
<button class="w-100 btn btn-lg btn-primary disabled" type="submit"><?php echo $translationsLogin['login_signup_button']; ?></button>
|
||||
</div>-->
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Bottom.php" ?>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
#$response = callAPIRoute("/logout/{$_SESSION["id"]}", 1, 1, NULL);
|
||||
#if ($response[0] === false) {
|
||||
# #return -1;
|
||||
#} else {#
|
||||
# if ($response[1] === 200) {
|
||||
#return json_decode($response[0], true);
|
||||
# } else {
|
||||
#return -2;
|
||||
# }
|
||||
#}
|
||||
|
||||
clearUserRelatedInfoSession();
|
||||
if (isset($_GET["sessionExpired"])) {
|
||||
header("location: ../login.php?sessionExpired=1");
|
||||
} else {
|
||||
header("location: ../login.php");
|
||||
}
|
||||
4363
frontend/package-lock.json
generated
Normal file
42
frontend/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "frontend_vue",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||
"bootstrap": "^5.3.2",
|
||||
"chart.js": "^4.4.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.15",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^3.0.3",
|
||||
"vite": "^5.0.11",
|
||||
"vitest": "^1.2.2"
|
||||
}
|
||||
}
|
||||
0
frontend/img/logo/logo.png → frontend/public/logo/logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsSettingsIntegrationSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/integration-settings/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsSettingsIntegrationSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/integration-settings/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsSettingsIntegrationSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/integration-settings/en.php';
|
||||
}
|
||||
|
||||
$strava_gear_result = -9000;
|
||||
$strava_activities_result = -9000;
|
||||
|
||||
if (isset($_GET["linkStrava"]) && $_GET["linkStrava"] == 1) {
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$generate_user_state = setUniqueUserStateStravaLink($state);
|
||||
if ($generate_user_state == 0) {
|
||||
linkStrava($state);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["stravaLinked"]) && $_GET["stravaLinked"] == 1) {
|
||||
setUserRelatedInfoSession($_SESSION["token"]);
|
||||
$unset_user_state = unsetUniqueUserStateStravaLink();
|
||||
}
|
||||
|
||||
if(isset($_GET["getUserStravaActivities"]) && $_GET["getUserStravaActivities"] == 1){
|
||||
$strava_activities_result = getStravaActivitiesLastDays(7);
|
||||
}
|
||||
|
||||
if(isset($_GET["getUserStravaGear"]) && $_GET["getUserStravaGear"] == 1){
|
||||
$strava_gear_result = getStravaGear();
|
||||
}
|
||||
?>
|
||||
<!-- Error banners -->
|
||||
<?php if ($strava_gear_result == -1 || $strava_gear_result == -2 || $strava_activities_result == -1 || $strava_activities_result == -2) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($strava_gear_result == -1 || $strava_activities_result == -1) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($strava_gear_result == -2 || $strava_activities_result == -2) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-2']; ?> (-2).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ((isset($_GET["stravaLinked"]) && $_GET["stravaLinked"] == 1) || $strava_gear_result == 0 || $strava_activities_result == 0) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if (isset($_GET["stravaLinked"]) && $_GET["stravaLinked"] == 1) { ?>
|
||||
<?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_success_stravaLinked']; ?>
|
||||
<?php }else{ ?>
|
||||
<?php if ($strava_gear_result == 0) { ?>
|
||||
<?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_success_stravaGear']; ?>
|
||||
<?php }else{ ?>
|
||||
<?php if ($strava_activities_result == 0) { ?>
|
||||
<?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_success_stravaActivities']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="row row-gap-3 row-cols-sm-3 align-items-center">
|
||||
<div class="col">
|
||||
<div class="card text-center">
|
||||
<img src="../img/strava/api_logo_cptblWith_strava_stack_light.svg" alt="Compatible with Strava image" class="card-img-top">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"><?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_strava_title']; ?></h4>
|
||||
<p class="card-text"><?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_strava_body']; ?></p>
|
||||
<a href="../settings/settings.php?integrationsSettings=1&linkStrava=1" class="btn btn-primary <?php if ($_SESSION["is_strava_linked"] == 1) { echo "disabled"; } ?>"><?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_connect_button']; ?></a>
|
||||
<?php if ($_SESSION["is_strava_linked"] == 1) { ?>
|
||||
<hr>
|
||||
<a href="../settings/settings.php?integrationsSettings=1&getUserStravaActivities=1" class="btn btn-primary"><?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_retrieve_last_week_activities_button']; ?></a>
|
||||
|
||||
<a href="../settings/settings.php?integrationsSettings=1&getUserStravaGear=1" class="btn btn-primary mt-3"><?php echo $translationsSettingsIntegrationSettings['settings_integration_settings_retrieve_gear_button']; ?></a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,312 +0,0 @@
|
||||
<?php
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsSettingsProfileSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/profile-settings/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsSettingsProfileSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/profile-settings/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsSettingsProfileSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/profile-settings/en.php';
|
||||
}
|
||||
|
||||
// profile
|
||||
$editProfileAction = -9000;
|
||||
$deletePhotoProfileAction = -9000;
|
||||
|
||||
// User profile section
|
||||
/* Edit action */
|
||||
if (isset($_POST["editProfile"])) {
|
||||
if (empty(trim($_POST["profileNameEdit"]))) {
|
||||
$_POST["profileNameEdit"] = NULL;
|
||||
}else{
|
||||
$_POST["profileNameEdit"] = trim($_POST["profileNameEdit"]);
|
||||
}
|
||||
|
||||
if (empty(trim($_POST["profileBirthDateEdit"]))) {
|
||||
$_POST["profileBirthDateEdit"] = NULL;
|
||||
}
|
||||
|
||||
if (empty(trim($_POST["profileCityEdit"]))) {
|
||||
$_POST["profileCityEdit"] = NULL;
|
||||
}else{
|
||||
$_POST["profileCityEdit"] = trim($_POST["profileCityEdit"]);
|
||||
}
|
||||
|
||||
if (isset($_FILES["profileImgEdit"]) && $_FILES["profileImgEdit"]["error"] == 0) {
|
||||
$target_dir = "../img/users_img/";
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!file_exists($target_dir)) {
|
||||
mkdir($target_dir, 0755, true);
|
||||
}
|
||||
|
||||
$info = pathinfo($_FILES["profileImgEdit"]["name"]);
|
||||
$ext = $info['extension']; // get the extension of the file
|
||||
#$newname = $_GET["userID"].".".$ext;
|
||||
$newname = "img_" . rand(1, 20) . "_" . rand(1, 9999999) . "_" . rand(1, 20) . "." . $ext;
|
||||
$target_file = $target_dir . $newname;
|
||||
$uploadOk = 1;
|
||||
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
|
||||
// Check if image file is a actual image or fake image
|
||||
$check = getimagesize($_FILES["profileImgEdit"]["tmp_name"]);
|
||||
if ($check !== false) {
|
||||
$uploadOk = 1;
|
||||
} else {
|
||||
$editProfileAction = -3;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
// Allow certain file formats
|
||||
if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") {
|
||||
$editProfileAction = -4;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
// Check if $uploadOk is set to 0 by an error
|
||||
if ($uploadOk == 1) {
|
||||
if(getUserPhotoAuxFromID($_SESSION["id"]) != null){
|
||||
if (unlink(getUserPhotoAuxFromID($_SESSION["id"]))) {
|
||||
unsetUserPhoto($_SESSION["id"]);
|
||||
}
|
||||
}
|
||||
if (move_uploaded_file($_FILES["profileImgEdit"]["tmp_name"], $target_file)) {
|
||||
$photoPath = "..\img\users_img\\" . $newname;
|
||||
$photoPath_aux = $target_file;
|
||||
$editProfileAction = editUser($_POST["profileNameEdit"], trim($_POST["profileUsernameEdit"]), trim($_POST["profileEmailEdit"]), $_SESSION["id"], $_POST["profilePreferredLanguageEdit"], $_POST["profileCityEdit"], $_POST["profileBirthDateEdit"], $_POST["profileGenderEdit"], $_SESSION["access_type"], $photoPath, $photoPath_aux, 1);
|
||||
setUserRelatedInfoSession($_SESSION["token"]);
|
||||
} else {
|
||||
$editProfileAction = -5;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$profilePhoto = getUserPhotoFromID($_SESSION["id"]);
|
||||
$profilePhotoAux = getUserPhotoAuxFromID($_SESSION["id"]);
|
||||
$editProfileAction = editUser($_POST["profileNameEdit"], trim($_POST["profileUsernameEdit"]), trim($_POST["profileEmailEdit"]), $_SESSION["id"], $_POST["profilePreferredLanguageEdit"], $_POST["profileCityEdit"], $_POST["profileBirthDateEdit"], $_POST["profileGenderEdit"], $_SESSION["access_type"], $profilePhoto, $profilePhotoAux, 1);
|
||||
setUserRelatedInfoSession($_SESSION["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete user photo */
|
||||
if (isset($_GET["deleteProfilePhoto"]) && $_GET["deleteProfilePhoto"] == 1) {
|
||||
if (unlink(getUserPhotoAuxFromID($_SESSION["id"]))) {
|
||||
$deletePhotoProfileAction = unsetUserPhoto($_SESSION["id"]);
|
||||
if ($deletePhotoProfileAction == 0) {
|
||||
$_SESSION["photo_path"] = NULL;
|
||||
$_SESSION["photo_path_aux"] = NULL;
|
||||
}
|
||||
} else {
|
||||
$deletePhotoProfileAction = -3;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Error banners -->
|
||||
<?php if ($deletePhotoProfileAction == -1 || $deletePhotoProfileAction == -2 || $deletePhotoProfileAction == -3 || $editProfileAction == -1 || $editProfileAction == -2 || $editProfileAction == -3 || $editProfileAction == -4 || $editProfileAction == -5) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($deletePhotoProfileAction == -1 || $editProfileAction == -1) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($deletePhotoProfileAction == -2 || $editProfileAction == -2) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($deletePhotoProfileAction == -3) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_error_deleteProfilePhoto_-3']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($editProfileAction == -3) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_profile_settings_error_editProfile_-3']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($editProfileAction == -4) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_profile_settings_error_editProfile_-4']; ?> (-4).
|
||||
<?php } else { ?>
|
||||
<?php if ($editProfileAction == -5) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_profile_settings_error_editProfile_-5']; ?> (-5).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($deletePhotoProfileAction == 0 || $editProfileAction == 0 || (isset($_GET["stravaLinked"]) && $_GET["stravaLinked"] == 1)) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($deletePhotoProfileAction == 0) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_success_profilePhotoDeleted']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if ($editProfileAction == 0) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_success_profileEdited']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if (isset($_GET["stravaLinked"]) && $_GET["stravaLinked"] == 1) { ?>
|
||||
<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_success_stravaLinked']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Profile photo and buttons zone -->
|
||||
<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=<?php if (is_null($_SESSION["photo_path"])) {
|
||||
if ($_SESSION["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($_SESSION["photo_path"]);
|
||||
} ?> alt="Profile picture" width="180" height="180" class="rounded-circle">
|
||||
</div>
|
||||
|
||||
<!-- Delete profile photo section -->
|
||||
<?php if (!is_null($_SESSION["photo_path"])) { ?>
|
||||
<a class="mt-4 w-100 btn btn-danger" href="#" role="button" data-bs-toggle="modal" data-bs-target="#deleteProfilePhotoModal"><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_deleteProfilePhoto']; ?></a>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Modal delete profile photo -->
|
||||
<div class="modal fade" id="deleteProfilePhotoModal" tabindex="-1" aria-labelledby="deleteProfilePhotoModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteProfilePhotoModal"><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_modal_title_deleteProfilePhoto']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_modal_body_deleteProfilePhoto']; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
<a type="button" class="btn btn-danger" href="../settings/settings.php?deleteProfilePhoto=1&profileSettings=1"><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_modal_title_deleteProfilePhoto']; ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit profile section -->
|
||||
<a class="mt-2 w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#editProfileModal<?php echo ($_SESSION["id"]); ?>"><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_button_editprofile']; ?></a>
|
||||
|
||||
<!-- Modal edit profile -->
|
||||
<div class="modal fade" id="editProfileModal<?php echo ($_SESSION["id"]); ?>" tabindex="-1" aria-labelledby="editProfileModal<?php echo ($_SESSION["id"]); ?>" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editProfileModal<?php echo ($_SESSION["id"]); ?>"><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_editProfile_title']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../settings/settings.php?editProfile=1&profileSettings=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<label for="profileImgEdit"><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_photo_label']; ?></b></label>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="form-control" type="file" accept="image/*" name="profileImgEdit" id="profileImgEdit" value="<?php echo ($_SESSION["photo_path"]); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- username fields -->
|
||||
<label for="profileUsernameEdit"><b>* <?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_username_label']; ?></b></label>
|
||||
<input class="form-control" type="text" name="profileUsernameEdit" placeholder="<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_username_placeholder']; ?>" maxlength="250" value="<?php echo ($_SESSION["username"]); ?>" required>
|
||||
<!-- name fields -->
|
||||
<label for="profileNameEdit"><b>* <?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_name_label']; ?></b></label>
|
||||
<input class="form-control" type="text" name="profileNameEdit" placeholder="<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_name_placeholder']; ?>" maxlength="250" value="<?php echo ($_SESSION["name"]); ?>" required>
|
||||
<!-- email fields -->
|
||||
<label for="profileEmailEdit"><b>* <?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_email_label']; ?></b></label>
|
||||
<input class="form-control" type="text" name="profileEmailEdit" placeholder="<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_email_placeholder']; ?>" maxlength="45" value="<?php echo ($_SESSION["email"]); ?>" required>
|
||||
<!-- city fields -->
|
||||
<label for="profileCityEdit"><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_city_label']; ?></b></label>
|
||||
<input class="form-control" type="text" name="profileCityEdit" placeholder="<?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_city_placeholder']; ?>" maxlength="45" value="<?php echo ($_SESSION["city"]); ?>">
|
||||
<!-- birth date fields -->
|
||||
<label for="profileBirthDateEdit"><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_birthdate_label']; ?></b></label>
|
||||
<input class="form-control" type="date" name="profileBirthDateEdit" value="<?php echo ($_SESSION["birthdate"]); ?>">
|
||||
<!-- gender fields -->
|
||||
<label for="profileGenderEdit"><b>* <?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_gender_label']; ?></b></label>
|
||||
<select class="form-control" name="profileGenderEdit">
|
||||
<option value="1" <?php if ($_SESSION["gender"] == 1) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_gender_option1']; ?></option>
|
||||
<option value="2" <?php if ($_SESSION["gender"] == 2) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_gender_option2']; ?></option>
|
||||
</select required>
|
||||
<!-- preferred language fields -->
|
||||
<label for="profilePreferredLanguageEdit"><b>* <?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_preferredLanguage_label']; ?></b></label>
|
||||
<select class="form-control" name="profilePreferredLanguageEdit">
|
||||
<option value="en" <?php if ($_SESSION["preferred_language"] == "en") { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_preferredLanguage_option1']; ?></option>
|
||||
<option value="pt" <?php if ($_SESSION["preferred_language"] == "pt") { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_preferredLanguage_option2']; ?></option>
|
||||
</select required>
|
||||
|
||||
* <?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
<button type="submit" class="btn btn-success" name="editProfile"><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_editProfile_title']; ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strava button
|
||||
<?php if ($_SESSION["is_strava_linked"] == 1) { ?>
|
||||
<div class="mt-2">
|
||||
<span class="fs-6">Strava already linked</span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<a class="mt-2 w-100 btn <?php if ($_SESSION["is_strava_linked"] == 1) {
|
||||
echo 'disabled';
|
||||
} ?>" style="--bs-btn-bg: #FC4C02; --bs-btn-active-bg: #FC4C02; --bs-btn-hover-bg: #FC4C02; --bs-btn-disabled-bg: #FC4C02; --bs-btn-disabled-border-color: #FC4C02;" href="../settings/settings.php?profileSettings=1&linkStrava=1" <?php if ($_SESSION["is_strava_linked"] == 1) {
|
||||
echo 'aria-disabled="true"';
|
||||
} ?> role="button"><img src="../img/strava/btn_strava_connectwith_orange.png" alt="Link with strava button" width="65%" height="65%"></a>-->
|
||||
</div>
|
||||
|
||||
<!-- Profile attributes -->
|
||||
<div class="col">
|
||||
<h2><?php echo ($_SESSION["name"]); ?></h2>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_username_subtitle']; ?></b><?php echo ($_SESSION["username"]); ?></p>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_email_subtitle']; ?></b><?php echo ($_SESSION["email"]); ?></p>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_birthdate_subtitle']; ?></b><?php
|
||||
if (isset($_SESSION["birthdate"]) && !empty($_SESSION["birthdate"])) {
|
||||
echo date("d/m/Y", strtotime($_SESSION["birthdate"]));
|
||||
} else {
|
||||
echo "N/A"; // Or any default value you prefer when birthdate is not set
|
||||
}
|
||||
?></p>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_city_subtitle']; ?></b><?php
|
||||
if (isset($_SESSION["city"]) && !empty($_SESSION["city"])) {
|
||||
echo $_SESSION["city"];
|
||||
} else {
|
||||
echo "N/A"; // Or any default value you prefer when birthdate is not set
|
||||
}
|
||||
?></p>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_gender_subtitle']; ?></b><?php if ($_SESSION["gender"] == 1) {
|
||||
echo $translationsSettingsProfileSettings['settings_sidebar_profile_gender_male'];
|
||||
} else {
|
||||
echo $translationsSettingsProfileSettings['settings_sidebar_profile_gender_female'];
|
||||
} ?></p>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_preferredlanguage_subtitle']; ?></b><?php echo ($_SESSION["preferred_language"]); ?></p>
|
||||
<p><b><?php echo $translationsSettingsProfileSettings['settings_sidebar_profile_access_type_subtitle']; ?></b> <?php if ($_SESSION["access_type"] == 1) {
|
||||
echo $translationsSettingsProfileSettings['settings_sidebar_profile_access_type_regular_user'];
|
||||
} else {
|
||||
if ($_SESSION["access_type"] == 2) {
|
||||
echo $translationsSettingsProfileSettings['settings_sidebar_profile_access_type_admin'];
|
||||
} else {
|
||||
if ($_SESSION["access_type"] == 3) {
|
||||
echo $translationsSettingsProfileSettings['settings_sidebar_profile_access_type_teacher'];
|
||||
} else {
|
||||
echo $translationsSettingsProfileSettings['settings_sidebar_profile_access_type_parent'];
|
||||
}
|
||||
}
|
||||
} ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsSettingsSecuritySettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/security-settings/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsSettingsSecuritySettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/security-settings/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsSettingsSecuritySettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/security-settings/en.php';
|
||||
}
|
||||
|
||||
$editUserPasswordAction = -9000;
|
||||
|
||||
if (isset($_POST["editUserPassword"]) && $_GET["editUserPassword"] == 1) {
|
||||
if($_POST["passUserEdit"] == $_POST["passRepeatUserEdit"]){
|
||||
// Check password complexity
|
||||
if (preg_match('/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/', $_POST["passUserEdit"])) {
|
||||
$editUserPasswordAction = editUserPassword($_SESSION["id"], $_POST["passUserEdit"]);
|
||||
}else{
|
||||
$editUserPasswordAction = -4;
|
||||
}
|
||||
}else{
|
||||
$editUserPasswordAction = -3;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<!-- Error banners -->
|
||||
<?php if ($editUserPasswordAction == -1 || $editUserPasswordAction == -2 || $editUserPasswordAction == -3 || $editUserPasswordAction == -4) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($editUserPasswordAction == -1) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserPasswordAction == -2) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserPasswordAction == -3) { ?>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_error_passwords_dont_match-3']; ?> (-3).
|
||||
<?php }else{ ?>
|
||||
<?php if ($editUserPasswordAction == -4) { ?>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_error_password_complexity-4']; ?> (-4).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($editUserPasswordAction == 0) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($editUserPasswordAction == 0) { ?>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_success_password_edited']; ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<h4><?php echo $translationsSettingsSecuritySettings['settings_security_settings_subtitle_change_password']; ?></h4>
|
||||
|
||||
<!-- info banner to display password complexity requirements -->
|
||||
<div class="alert alert-info alert-dismissible d-flex align-items-center" role="alert">
|
||||
<!--<i class="fa-solid fa-circle-info me-1 allign-top"></i>-->
|
||||
<div>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_info_password_requirements']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_info_password_requirements_characters']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_info_password_requirements_capital_letters']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_info_password_requirements_numbers']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsSecuritySettings['settings_security_settings_info_password_requirements_special_characters']; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="../settings/settings.php?editUserPassword=1&securitySettings=1" method="post" enctype="multipart/form-data">
|
||||
<!-- password fields -->
|
||||
<label for="passUserEdit"><b>* <?php echo $translationsSettingsSecuritySettings['settings_security_settings_subtitle_change_password_password']; ?></b></label>
|
||||
<input class="form-control" type="password" name="passUserEdit" placeholder="<?php echo $translationsSettingsSecuritySettings['settings_security_settings_subtitle_change_password_password']; ?>" value="<?php echo isset($_POST["passUserEdit"]) ? $_POST["passUserEdit"] : ''; ?>" required>
|
||||
|
||||
<!-- repeat password fields -->
|
||||
<label class="mt-1" for="passRepeatUserEdit"><b>* <?php echo $translationsSettingsSecuritySettings['settings_security_settings_subtitle_change_password_repeat_password']; ?></b></label>
|
||||
<input class="form-control" type="password" name="passRepeatUserEdit" placeholder="<?php echo $translationsSettingsSecuritySettings['settings_security_settings_subtitle_change_password_repeat_password']; ?>" value="<?php echo isset($_POST["passRepeatUserEdit"]) ? $_POST["passRepeatUserEdit"] : ''; ?>" required>
|
||||
|
||||
<p class="mt-1">* <?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?></p>
|
||||
|
||||
<button type="submit" class="btn btn-success" name="editUserPassword"><?php echo $translationsSettingsSecuritySettings['settings_security_settings_subtitle_change_password_button']; ?></button>
|
||||
</form>
|
||||
@@ -1,655 +0,0 @@
|
||||
<?php
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsSettingsUsersSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/users-settings/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsSettingsUsersSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/users-settings/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsSettingsUsersSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/inc/users-settings/en.php';
|
||||
}
|
||||
|
||||
# Users section
|
||||
// users
|
||||
$numUsers = 0;
|
||||
$users = [];
|
||||
$addUserAction = -9000;
|
||||
$photoDeleted = -9000;
|
||||
$deleteAction = -9000;
|
||||
$deletePhotoUserAction = -9000;
|
||||
$editUserAction = -9000;
|
||||
$pageNumberUsers = 1;
|
||||
|
||||
/* Add user action */
|
||||
if (isset($_POST["addUser"]) && $_GET["addUser"] == 1) {
|
||||
if (empty(trim($_POST["userNameAdd"]))) {
|
||||
$_POST["userNameAdd"] = NULL;
|
||||
}
|
||||
|
||||
if (isset($_FILES["userImgAdd"]) && $_FILES["userImgAdd"]["error"] == 0) {
|
||||
$target_dir = "../img/users_img/";
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!file_exists($target_dir)) {
|
||||
mkdir($target_dir, 0755, true);
|
||||
}
|
||||
|
||||
$info = pathinfo($_FILES["userImgAdd"]["name"]);
|
||||
$ext = $info['extension']; // get the extension of the file
|
||||
#$number=lastIdUsers()+1;
|
||||
$newname = "img_" . rand(1, 20) . "_" . rand(1, 9999999) . "_" . rand(1, 20) . "." . $ext;
|
||||
$target_file = $target_dir . $newname;
|
||||
$uploadOk = 1;
|
||||
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
|
||||
// Check if image file is a actual image or fake image
|
||||
$check = getimagesize($_FILES["userImgAdd"]["tmp_name"]);
|
||||
if ($check !== false) {
|
||||
$uploadOk = 1;
|
||||
} else {
|
||||
$addUserAction = -4;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
// Allow certain file formats
|
||||
if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") {
|
||||
$addUserAction = -5;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
// Check if $uploadOk is set to 0 by an error
|
||||
if ($uploadOk == 1) {
|
||||
if (move_uploaded_file($_FILES["userImgAdd"]["tmp_name"], $target_file)) {
|
||||
$photoPath = "..\img\users_img\\" . $newname;
|
||||
$photoPath_aux = $target_file;
|
||||
} else {
|
||||
$addUserAction = -6;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$photoPath = NULL;
|
||||
$photoPath_aux = NULL;
|
||||
$uploadOk = 1;
|
||||
}
|
||||
if ($uploadOk == 1) {
|
||||
$userCity = trim($_POST["userCityAdd"]);
|
||||
if (empty($userCity)) {
|
||||
$userCity = NULL;
|
||||
}
|
||||
if (empty($_POST["userBirthDateAdd"])) {
|
||||
$_POST["userBirthDateAdd"] = NULL;
|
||||
}
|
||||
|
||||
$addUserAction = newUser(trim($_POST["userNameAdd"]), trim($_POST["userUsernameAdd"]), trim($_POST["userEmailAdd"]), $_POST["passUserAdd"], $_POST["userGenderAdd"], $_POST["userPreferredLanguageAdd"], $userCity, $_POST["userBirthDateAdd"], $_POST["userAccessTypeAdd"], $photoPath, $photoPath_aux, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit user action */
|
||||
if (isset($_POST["userEdit"])) {
|
||||
if (empty(trim($_POST["userNameEdit"]))) {
|
||||
$_POST["userNameEdit"] = NULL;
|
||||
}
|
||||
if (empty($_POST["userTypeEdit"])) {
|
||||
$_POST["userTypeEdit"] = NULL;
|
||||
}
|
||||
|
||||
if (empty($_POST["userUsernameEdit"]) && empty($_POST["userTypeEdit"])) {
|
||||
$editAction = -3;
|
||||
} else {
|
||||
$userCity = trim($_POST["userCityEdit"]);
|
||||
if (empty($userCity)) {
|
||||
$userCity = NULL;
|
||||
}
|
||||
if (empty($_POST["userBirthDateEdit"])) {
|
||||
$_POST["userBirthDateEdit"] = NULL;
|
||||
}
|
||||
if (isset($_FILES["userImgEdit"]) && $_FILES["userImgEdit"]["error"] == 0) {
|
||||
$target_dir = "../img/users_img/";
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!file_exists($target_dir)) {
|
||||
mkdir($target_dir, 0777, true);
|
||||
}
|
||||
|
||||
$info = pathinfo($_FILES["userImgEdit"]["name"]);
|
||||
$ext = $info['extension']; // get the extension of the file
|
||||
#$newname = $_GET["userID"].".".$ext;
|
||||
$newname = "img_" . rand(1, 20) . "_" . rand(1, 9999999) . "_" . rand(1, 20) . "." . $ext;
|
||||
$target_file = $target_dir . $newname;
|
||||
$uploadOk = 1;
|
||||
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
|
||||
// Check if image file is a actual image or fake image
|
||||
$check = getimagesize($_FILES["userImgEdit"]["tmp_name"]);
|
||||
if ($check !== false) {
|
||||
$uploadOk = 1;
|
||||
} else {
|
||||
$editUserAction = -4;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
// Allow certain file formats
|
||||
if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") {
|
||||
$editUserAction = -5;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
// Check if $uploadOk is set to 0 by an error
|
||||
if ($uploadOk == 1) {
|
||||
if(getUserPhotoAuxFromID($_GET["userID"]) != null){
|
||||
if (unlink(getUserPhotoAuxFromID($_GET["userID"]))) {
|
||||
unsetUserPhoto($_GET["userID"]);
|
||||
}
|
||||
}
|
||||
if (move_uploaded_file($_FILES["userImgEdit"]["tmp_name"], $target_file)) {
|
||||
$photoPath = "..\img\users_img\\" . $newname;
|
||||
$photoPath_aux = $target_file;
|
||||
$editUserAction = editUser(trim($_POST["userNameEdit"]), trim($_POST["userUsernameEdit"]), trim($_POST["userEmailEdit"]), $_GET["userID"], $_POST["userPreferredLanguageEdit"], $userCity, $_POST["userBirthDateEdit"], $_POST["userGenderEdit"], $_POST["userTypeEdit"], $photoPath, $photoPath_aux, $_POST["userIsActiveEdit"]);
|
||||
if ($_GET["userID"] == $_SESSION["id"]) {
|
||||
setUserRelatedInfoSession($_SESSION["token"]);
|
||||
}
|
||||
} else {
|
||||
$editUserAction = -6;
|
||||
$uploadOk = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$userPhoto = getUserPhotoFromID($_GET["userID"]);
|
||||
if (empty($userPhoto)) {
|
||||
$userPhoto = NULL;
|
||||
}
|
||||
$userPhotoAux = getUserPhotoAuxFromID($_GET["userID"]);
|
||||
if (empty($userPhotoAux)) {
|
||||
$userPhotoAux = NULL;
|
||||
}
|
||||
$editUserAction = editUser(trim($_POST["userNameEdit"]), trim($_POST["userUsernameEdit"]), trim($_POST["userEmailEdit"]), $_GET["userID"], $_POST["userPreferredLanguageEdit"], $userCity, $_POST["userBirthDateEdit"], $_POST["userGenderEdit"], $_POST["userTypeEdit"], $userPhoto, $userPhotoAux, $_POST["userIsActiveEdit"]);
|
||||
if ($_GET["userID"] == $_SESSION["id"]) {
|
||||
setUserRelatedInfoSession($_SESSION["token"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete user photo */
|
||||
if (isset($_GET["deletePhotoUser"]) && $_GET["deletePhotoUser"] == 1) {
|
||||
if (unlink(getUserPhotoAuxFromID($_GET["userID"]))) {
|
||||
$deletePhotoUserAction = unsetUserPhoto($_GET["userID"]);
|
||||
if ($_GET["userID"] == $_SESSION["id"]) {
|
||||
setUserRelatedInfoSession($_SESSION["token"]);
|
||||
}
|
||||
} else {
|
||||
$deletePhotoUserAction = -3;
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete user */
|
||||
if (isset($_GET["deleteUser"]) && $_GET["deleteUser"] == 1) {
|
||||
if ($_GET["userID"] != $_SESSION["id"]) {
|
||||
$photo_path = getUserPhotoAuxFromID($_GET["userID"]);
|
||||
$deleteAction = deleteUser($_GET["userID"]);
|
||||
if ($deleteAction == 0) {
|
||||
if (!is_null($photo_path)) {
|
||||
if (unlink($photo_path)) {
|
||||
$photoDeleted = 0;
|
||||
} else {
|
||||
$photoDeleted = 1;
|
||||
}
|
||||
} else {
|
||||
$photoDeleted = 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$deleteAction = -3;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["pageNumberUsers"])) {
|
||||
$pageNumberUsers = $_GET["pageNumberUsers"];
|
||||
}
|
||||
|
||||
if (!isset($_POST["userSearch"])) {
|
||||
$users = getUsersPagination($pageNumberUsers, $numRecords);
|
||||
$numUsers = numUsers();
|
||||
$total_pages = ceil($numUsers / $numRecords);
|
||||
} else {
|
||||
$users = getUsersIfContainsUsername(urlencode(trim($_POST["userUsername"])));
|
||||
if ($users == NULL) {
|
||||
$numUsers = 0;
|
||||
} else {
|
||||
$numUsers = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// edit user password
|
||||
$editUserPasswordAdminAction = -9000;
|
||||
|
||||
if (isset($_POST["editUserPasswordAdmin"]) && $_GET["editUserPasswordAdmin"] == 1) {
|
||||
if(isset($_GET["userID"]) && $_GET["userID"] > 0){
|
||||
if($_POST["passUserEditAdmin"] == $_POST["passRepeatUserEditAdmin"]){
|
||||
// Check password complexity
|
||||
if (preg_match('/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/', $_POST["passUserEditAdmin"])) {
|
||||
$editUserPasswordAdminAction = editUserPassword($_GET["userID"], $_POST["passUserEditAdmin"]);
|
||||
}else{
|
||||
$editUserPasswordAdminAction = -4;
|
||||
}
|
||||
}else{
|
||||
$editUserPasswordAdminAction = -3;
|
||||
}
|
||||
}else{
|
||||
$editUserPasswordAdminAction = -5;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Error banners -->
|
||||
<?php if ($users == -1 || $users == -2 || $editUserAction == -1 || $editUserAction == -2 || $editUserAction == -3 || $editUserAction == -4 || $editUserAction == -5 || $editUserAction == -6 || $deleteAction == -1 || $deleteAction == -2 || $deleteAction == -409 || $deleteAction == -3 || $addUserAction == -1 || $addUserAction == -2 || $addUserAction == -3 || $addUserAction == -4 || $addUserAction == -5 || $addUserAction == -6 || $deletePhotoUserAction == -1 || $deletePhotoUserAction == -2 || $deletePhotoUserAction == -3 || $editUserPasswordAdminAction == -1 || $editUserPasswordAdminAction == -2 || $editUserPasswordAdminAction == -3 || $editUserPasswordAdminAction == -4 || $editUserPasswordAdminAction == -5) { ?>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($users == -1 || $addUserAction == -1 || $editUserAction == -1 || $deletePhotoUserAction == -1 || $deleteAction == -1) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($users == -2 || $addUserAction == -2 || $editUserAction == -2 || $deletePhotoUserAction == -2 || $deleteAction == -2) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserAction == -3) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_editUser_-3']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserAction == -4) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addEditUser_-4']; ?> (-4).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserAction == -5) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addEditUser_-5']; ?> (-5).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserAction == -6) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addEditUser_-6']; ?> (-6).
|
||||
<?php } else { ?>
|
||||
<?php if ($deleteAction == -3) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_deleteUser_-3']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($deleteAction == -409) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_deleteUser_-409']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($addUserAction == -3) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addUser_-3']; ?> (-3).
|
||||
<?php } else { ?>
|
||||
<?php if ($addUserAction == -4) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addEditUser_-4']; ?> (-4).
|
||||
<?php } else { ?>
|
||||
<?php if ($addUserAction == -5) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addEditUser_-5']; ?> (-5).
|
||||
<?php } else { ?>
|
||||
<?php if ($addUserAction == -6) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_addEditUser_-6']; ?> (-6).
|
||||
<?php } else { ?>
|
||||
<?php if ($deletePhotoUserAction == -3) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_deleteUserPhoto_-3']; ?> (-3).
|
||||
<?php }else{ ?>
|
||||
<?php if ($editUserPasswordAdminAction == -1) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-1']; ?> (-1).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserPasswordAdminAction == -2) { ?>
|
||||
API ERROR | <?php echo $translationsSettings['settings_API_error_-2']; ?> (-2).
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserPasswordAdminAction == -3) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_passwords_dont_match-3']; ?> (-3).
|
||||
<?php }else{ ?>
|
||||
<?php if ($editUserPasswordAdminAction == -4) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_password_complexity-4']; ?> (-4).
|
||||
<?php }else{ ?>
|
||||
<?php if ($editUserPasswordAdminAction == -4) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_user_id_not_valid-5']; ?> (-5).
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Info banners -->
|
||||
<?php if ($users == NULL || ($deleteAction == 0 && $photoDeleted == 1)) { ?>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation me-1"></i>
|
||||
<div>
|
||||
<?php if ($users == NULL) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_error_searchUser_NULL']; ?> (NULL).
|
||||
<?php } else { ?>
|
||||
<?php if ($deleteAction == 0 && $photoDeleted == 1) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_info_userDeleted_photoNotDeleted']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Success banners -->
|
||||
<?php if ($addUserAction == 0 || $editUserAction == 0 || ($deleteAction == 0 && $photoDeleted == 0) || ($deleteAction == 0 && $photoDeleted == 2) || $deletePhotoUserAction == 0 || $editUserPasswordAdminAction == 0) { ?>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fa-regular fa-circle-check me-1"></i>
|
||||
<?php if ($addUserAction == 0) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_success_userAdded']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if ($editUserAction == 0) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_success_userEdited']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if (($deleteAction == 0 && $photoDeleted == 0) || ($deleteAction == 0 && $photoDeleted == 2)) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_success_userDeleted']; ?>
|
||||
<?php } else { ?>
|
||||
<?php if ($deletePhotoUserAction == 0) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_success_userPhotoDeleted']; ?>
|
||||
<?php }else{ ?>
|
||||
<?php if ($editUserPasswordAdminAction == 0) { ?>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_success_password_edited']; ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="row row-gap-3">
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<!-- add user button -->
|
||||
<a class="w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addUserModal"><?php echo $translationsSettingsUsersSettings['settings_sidebar_button_addUser']; ?></a>
|
||||
|
||||
<!-- Modal add user -->
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addUserModal"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addUser_title']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../settings/settings.php?addUser=1&users=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- img fields -->
|
||||
<label for="userImgAdd"><b><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_photoLabel']; ?></b></label>
|
||||
<input class="form-control" type="file" accept="image/*" name="userImgAdd" id="userImgAdd" value="<?php echo isset($_POST["userImgAdd"]) ? $_POST["userImgAdd"] : ''; ?>">
|
||||
<!-- username fields -->
|
||||
<label for="userUsernameAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_usernameLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userUsernameAdd" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_usernamePlaceholder']; ?>" maxlength="45" value="<?php echo isset($_POST["userUsernameAdd"]) ? htmlspecialchars($_POST["userUsernameAdd"]) : ''; ?>" required>
|
||||
<!-- name fields -->
|
||||
<label for="userNameAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_nameLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userNameAdd" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_namePlaceholder']; ?>" maxlength="45" value="<?php echo isset($_POST["userNameAdd"]) ? htmlspecialchars($_POST["userNameAdd"]) : ''; ?>" required>
|
||||
<!-- email fields -->
|
||||
<label for="userEmailAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_emailLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userEmailAdd" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_emailPlaceholder']; ?>" maxlength="45" value="<?php echo isset($_POST["userEmailAdd"]) ? htmlspecialchars($_POST["userEmailAdd"]) : ''; ?>" required>
|
||||
<!-- password fields -->
|
||||
<label for="passUserAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_passwordLabel']; ?></b></label>
|
||||
<input class="form-control" type="password" name="passUserAdd" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_passwordPlaceholder']; ?>" value="<?php echo isset($_POST["passUserAdd"]) ? $_POST["passUserAdd"] : ''; ?>" required>
|
||||
<!-- city fields -->
|
||||
<label for="userCityAdd"><b><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_cityLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userCityAdd" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_cityPlaceholder']; ?>" maxlength="45" value="<?php echo isset($_POST["userCityAdd"]) ? htmlspecialchars($_POST["userCityAdd"]) : ''; ?>">
|
||||
<!-- birth date fields -->
|
||||
<label for="userBirthDateAdd"><b><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_birthdateLabel']; ?></b></label>
|
||||
<input class="form-control" type="date" name="userBirthDateAdd" value="<?php echo isset($_POST["userBirthDateAdd"]) ? htmlspecialchars($_POST["userBirthDateAdd"]) : ''; ?>">
|
||||
<!-- gender fields -->
|
||||
<label for="userGenderAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_genderLabel']; ?></b></label>
|
||||
<select class="form-control" name="userGenderAdd">
|
||||
<option value="1" <?php if (isset($_POST["userGenderAdd"]) && $_POST["userGenderAdd"] == 1) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_genderOption1']; ?></option>
|
||||
<option value="2" <?php if (isset($_POST["userGenderAdd"]) && $_POST["userGenderAdd"] == 2) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_genderOption2']; ?></option>
|
||||
</select required>
|
||||
<!-- preferred language fields -->
|
||||
<label for="userPreferredLanguageAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_preferredLanguageLabel']; ?></b></label>
|
||||
<select class="form-control" name="userPreferredLanguageAdd">
|
||||
<option value="en" <?php if (isset($_POST["userPreferredLanguageAdd"]) && $_POST["userPreferredLanguageAdd"] == "en") { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_preferredLanguageOption1']; ?></option>
|
||||
<option value="pt" <?php if (isset($_POST["userPreferredLanguageAdd"]) && $_POST["userPreferredLanguageAdd"] == "pt") { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_preferredLanguageOption2']; ?></option>
|
||||
</select required>
|
||||
<!-- access type fields -->
|
||||
<label for="userAccessTypeAdd"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeLabel']; ?></b></label>
|
||||
<select class="form-control" name="userAccessTypeAdd">
|
||||
<option value="1" <?php if (isset($_POST["userAccessTypeAdd"]) && $_POST["userAccessTypeAdd"] == 1) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeOption1']; ?>
|
||||
<option value="2" <?php if (isset($_POST["userAccessTypeAdd"]) && $_POST["userAccessTypeAdd"] == 2) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeOption2']; ?></option>
|
||||
</select required>
|
||||
* <?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
<button type="submit" class="btn btn-success" name="addUser"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addUser_title']; ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<form action="../settings/settings.php?users=1" method="post" class="d-flex">
|
||||
<input class="form-control me-2" type="text" name="userUsername" placeholder="<?php echo $translationsSettingsUsersSettings['settings_sidebar_form_searchUser_usernamePlaceholder']; ?>" required>
|
||||
<button class="btn btn-success" type="submit" name="userSearch"><?php echo $translationsSettingsUsersSettings['settings_users_settings_search']; ?></button>
|
||||
<?php if (isset($_POST["userSearch"])) { ?>
|
||||
<a class="ms-2 w-25 btn btn-primary" href="../settings/settings.php?users=1" role="button"><?php echo $translationsTemplateTop['template_top_global_button_listAll']; ?></a>
|
||||
<?php } ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- users list zone -->
|
||||
<?php if ($users != NULL && $users != -1 && $users != -2 && $users != -3) { ?>
|
||||
<!-- title zone -->
|
||||
<br>
|
||||
<p><?php echo $translationsSettingsUsersSettings['settings_users_settings_list_title1']; ?> <?php echo ($numUsers); ?> <?php echo $translationsSettingsUsersSettings['settings_users_settings_list_title2']; ?> (<?php echo $numRecords; ?> <?php echo $translationsSettingsUsersSettings['settings_users_settings_list_title3']; ?>:</p>
|
||||
<!-- list zone -->
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($users as $user) { ?>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src=<?php if (is_null($user["photo_path"])) {
|
||||
if ($user["gender"] == 1) {
|
||||
echo ("../img/avatar/male1.png");
|
||||
} else {
|
||||
echo ("../img/avatar/female1.png");
|
||||
}
|
||||
} else {
|
||||
echo ($user["photo_path"]);
|
||||
} ?> alt="userPicture" class="rounded-circle" width="55" height="55">
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
<?php echo ($user["username"]); ?>
|
||||
</div>
|
||||
<b><?php echo $translationsSettingsUsersSettings['settings_users_settings_list_user_accesstype']; ?></b><?php if ($user["access_type"] == 1) {
|
||||
echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeOption1'];
|
||||
} else {
|
||||
if ($user["access_type"] == 2) {
|
||||
echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeOption2'];
|
||||
}
|
||||
} ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<?php if ($user["is_active"] == 1) { ?>
|
||||
<span class="badge bg-success-subtle border border-success-subtle text-success-emphasis rounded-pill align-middle"><?php echo $translationsSettingsUsersSettings['settings_users_settings_list_isactive']; ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis rounded-pill align-middle"><?php echo $translationsSettingsUsersSettings['settings_users_settings_list_isinactive']; ?></span>
|
||||
<?php } ?>
|
||||
|
||||
<!-- change user password button -->
|
||||
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal" data-bs-target="#editUserPasswordModal<?php echo ($user["id"]); ?>"><i class="fa-solid fa-key"></i></a>
|
||||
|
||||
<!-- change user password Modal -->
|
||||
<div class="modal fade" id="editUserPasswordModal<?php echo ($user["id"]); ?>" tabindex="-1" aria-labelledby="editUserPasswordModal<?php echo ($user["id"]); ?>" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editUserPasswordModal<?php echo ($user["id"]); ?>"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_changeUserPassword_title']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../settings/settings.php?userID=<?php echo ($user["id"]); ?>&editUserPasswordAdmin=1&users=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<!-- info banner to display password complexity requirements -->
|
||||
<div class="alert alert-info alert-dismissible d-flex align-items-center" role="alert">
|
||||
<!--<i class="fa-solid fa-circle-info me-1 allign-top"></i>-->
|
||||
<div>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_info_password_requirements']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_info_password_requirements_characters']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_info_password_requirements_capital_letters']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_info_password_requirements_numbers']; ?>
|
||||
<br>
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_info_password_requirements_special_characters']; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_changeUserPassword_body']; ?> <b><?php echo ($user["username"]); ?></b></p>
|
||||
|
||||
<!-- password fields -->
|
||||
<label for="passUserEditAdmin"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_change_password_password']; ?></b></label>
|
||||
<input class="form-control" type="password" name="passUserEditAdmin" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_change_password_password']; ?>" value="<?php echo isset($_POST["passUserEditAdmin"]) ? $_POST["passUserEditAdmin"] : ''; ?>" required>
|
||||
|
||||
<!-- repeat password fields -->
|
||||
<label class="mt-1" for="passRepeatUserEditAdmin"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_change_password_repeat_password']; ?></b></label>
|
||||
<input class="form-control" type="password" name="passRepeatUserEditAdmin" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_change_password_repeat_password']; ?>" value="<?php echo isset($_POST["passRepeatUserEditAdmin"]) ? $_POST["passRepeatUserEditAdmin"] : ''; ?>" required>
|
||||
|
||||
<p class="mt-1">* <?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
|
||||
<button type="submit" class="btn btn-success" name="editUserPasswordAdmin"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_changeUserPassword_title']; ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- edit user button -->
|
||||
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal" data-bs-target="#editUserModal<?php echo ($user["id"]); ?>"><i class="fa-solid fa-pen-to-square"></i></a>
|
||||
|
||||
<!-- Modal edit user -->
|
||||
<div class="modal fade" id="editUserModal<?php echo ($user["id"]); ?>" tabindex="-1" aria-labelledby="editUserModal<?php echo ($user["id"]); ?>" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editUserModal<?php echo ($user["id"]); ?>"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_editUser_title']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="../settings/settings.php?userID=<?php echo ($user["id"]); ?>&editUser=1&users=1" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<label for="userImgEdit"><b><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_photoLabel']; ?></b></label>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="form-control" type="file" accept="image/*" name="userImgEdit" id="userImgEdit" value="<?php echo ($user["photo_path"]); ?>">
|
||||
</div>
|
||||
<?php if (!is_null($user["photo_path"])) { ?>
|
||||
<div class="col">
|
||||
<a class="w-100 btn btn-danger" href="../settings/settings.php?userID=<?php echo ($user["id"]); ?>&deletePhotoUser=1&users=1" role="button"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_editUser_deleteUserPhoto']; ?></a>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- username fields -->
|
||||
<label for="userUsernameEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_usernameLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userUsernameEdit" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_usernamePlaceholder']; ?>" maxlength="250" value="<?php echo ($user["username"]); ?>" required>
|
||||
<!-- name fields -->
|
||||
<label for="userNameEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_nameLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userNameEdit" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_namePlaceholder']; ?>" maxlength="250" value="<?php echo ($user["name"]); ?>" required>
|
||||
<!-- email fields -->
|
||||
<label for="userEmailEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_emailLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userEmailEdit" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_emailPlaceholder']; ?>" maxlength="45" value="<?php echo ($user["email"]); ?>" required>
|
||||
<!-- city fields -->
|
||||
<label for="userCityEdit"><b><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_cityLabel']; ?></b></label>
|
||||
<input class="form-control" type="text" name="userCityEdit" placeholder="<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_cityPlaceholder']; ?>" maxlength="45" value="<?php echo ($user["city"]); ?>">
|
||||
<!-- birth date fields -->
|
||||
<label for="userBirthDateEdit"><b><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_birthdateLabel']; ?></b></label>
|
||||
<input class="form-control" type="date" name="userBirthDateEdit" value="<?php echo ($user["birthdate"]); ?>">
|
||||
<!-- gender fields -->
|
||||
<label for="userGenderEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_genderLabel']; ?></b></label>
|
||||
<select class="form-control" name="userGenderEdit">
|
||||
<option value="1" <?php if ($user["gender"] == 1) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_genderOption1']; ?></option>
|
||||
<option value="2" <?php if ($user["gender"] == 2) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_genderOption2']; ?></option>
|
||||
</select required>
|
||||
<!-- preferred language fields -->
|
||||
<label for="userPreferredLanguageEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_preferredLanguageLabel']; ?></b></label>
|
||||
<select class="form-control" name="userPreferredLanguageEdit">
|
||||
<option value="en" <?php if ($user["preferred_language"] == "en") { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_preferredLanguageOption1']; ?></option>
|
||||
<option value="pt" <?php if ($user["preferred_language"] == "pt") { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_preferredLanguageOption2']; ?></option>
|
||||
</select required>
|
||||
<!-- access type fields -->
|
||||
<label for="userTypeEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeLabel']; ?></b></label>
|
||||
<select class="form-control" name="userTypeEdit">
|
||||
<option value="1" <?php if ($user["access_type"] == 1) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeOption1']; ?></option>
|
||||
<option value="2" <?php if ($user["access_type"] == 2) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_typeOption2']; ?></option>
|
||||
</select required>
|
||||
<!-- user is_active fields -->
|
||||
<label for="userIsActiveEdit"><b>* <?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_isActiveLabel']; ?></b></label>
|
||||
<select class="form-control" name="userIsActiveEdit">
|
||||
<option value="1" <?php if ($user["is_active"] == 1) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_isActiveOption1']; ?></option>
|
||||
<option value="2" <?php if ($user["is_active"] == 2) { ?> selected="selected" <?php } ?>><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_addEditUser_isActiveOption2']; ?></option>
|
||||
</select required>
|
||||
* <?php echo $translationsTemplateTop['template_top_global_requiredFields']; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
<button type="submit" class="btn btn-success" name="userEdit"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_editUser_title']; ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($user["id"] != $_SESSION["id"]) { ?>
|
||||
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal" data-bs-target="#deleteUserModal<?php echo ($user["id"]); ?>"><i class="fa-solid fa-trash-can"></i></a>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="deleteUserModal<?php echo ($user["id"]); ?>" tabindex="-1" aria-labelledby="deleteUserModal<?php echo ($user["id"]); ?>" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteUserModal<?php echo ($user["id"]); ?>"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_deleteUser_title']; ?></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_deleteUser_body']; ?> <b><?php echo ($user["username"]); ?></b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo $translationsTemplateTop['template_top_global_close']; ?></button>
|
||||
<a type="button" class="btn btn-danger" href="../settings/settings.php?userID=<?php echo ($user["id"]); ?>&deleteUser=1&users=1"><?php echo $translationsSettingsUsersSettings['settings_users_settings_modal_deleteUser_title']; ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php if (!isset($_POST["userSearch"])) { ?>
|
||||
<br>
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <?php if ($pageNumberUsers == 1) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?pageNumberUsers=1">«</a></li>
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++) { ?>
|
||||
<li class="page-item <?php if ($i == $pageNumberUsers) {
|
||||
echo "active";
|
||||
} ?>"><a class="page-link" href="?pageNumberUsers=<?php echo ($i); ?>"><?php echo ($i); ?></a></li>
|
||||
<?php } ?>
|
||||
<li class="page-item <?php if ($pageNumberUsers == $total_pages) {
|
||||
echo "disabled";
|
||||
} ?>"><a class="page-link" href="?pageNumberUsers=<?php echo ($total_pages); ?>">»</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
@@ -1,169 +0,0 @@
|
||||
<?php
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php";
|
||||
|
||||
$page = "settings";
|
||||
|
||||
if (!isLogged()) {
|
||||
header("Location: ../login.php");
|
||||
die();
|
||||
}
|
||||
|
||||
if (!isTokenValid($_SESSION["token"])) {
|
||||
header("Location: ../logout.php?sessionExpired=1");
|
||||
die();
|
||||
}
|
||||
|
||||
if ($_SESSION["access_type"] != 2) {
|
||||
$_GET["profileSettings"] = 1;
|
||||
}
|
||||
|
||||
// general
|
||||
$numRecords = 5;
|
||||
|
||||
// Load the language file based on the user's preferred language
|
||||
switch ($_SESSION["preferred_language"]) {
|
||||
case 'en':
|
||||
$translationsSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/en.php';
|
||||
break;
|
||||
case 'pt':
|
||||
$translationsSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/pt.php';
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
$translationsSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/en.php';
|
||||
}
|
||||
?>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Top.php" ?>
|
||||
<div class="container mt-4">
|
||||
<div class="row row-gap-3">
|
||||
<div>
|
||||
<h1><?php echo $translationsSettings['settings_title']; ?></h1>
|
||||
</div>
|
||||
<!-- sidebar zone -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<ul class="nav nav-pills flex-column mb-auto" id="sidebarNav">
|
||||
<?php if ($_SESSION["access_type"] == 2) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link <?php if (!isset($_GET["profileSettings"]) && !isset($_GET["securitySettings"]) && !isset($_GET["integrationsSettings"]) && $_SESSION["access_type"] == 2) {
|
||||
echo "text-white active";
|
||||
}else{ echo "text-white"; } ?>" onclick="changeActive(event, 'divUsers')">
|
||||
<i class="fa-solid fa-users me-1"></i>
|
||||
<?php echo $translationsSettings['settings_sidebar_users']; ?>
|
||||
</a>
|
||||
</li>
|
||||
<hr>
|
||||
<?php } ?>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link <?php if (isset($_GET["profileSettings"]) || ($_SESSION["access_type"] == 1 && !isset($_GET["profileSettings"]))) {
|
||||
echo "text-white active";
|
||||
}else{ echo "text-white"; } ?>" onclick="changeActive(event, 'divProfileSettings')">
|
||||
<i class="fa-solid fa-address-card me-1"></i>
|
||||
<?php echo $translationsSettings['settings_sidebar_profileSettings']; ?>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link <?php if (isset($_GET["securitySettings"])) {
|
||||
echo "text-white active";
|
||||
}else{ echo "text-white"; } ?>" onclick="changeActive(event, 'divSecuritySettings')">
|
||||
<i class="fa-solid fa-shield me-1"></i>
|
||||
<?php echo $translationsSettings['settings_sidebar_securitySettings']; ?>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link <?php if (isset($_GET["integrationsSettings"])) {
|
||||
echo "text-white active";
|
||||
}else{ echo "text-white"; } ?>" onclick="changeActive(event, 'divIntegrationsSettings')">
|
||||
<i class="fa-solid fa-puzzle-piece me-1"></i>
|
||||
<?php echo $translationsSettings['settings_sidebar_integrationsSettings']; ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr class="d-lg-none">
|
||||
|
||||
<!-- users zone -->
|
||||
<div class="col" id="divUsers" style="display: <?php if (isset($_GET["integrationsSettings"]) || isset($_GET["profileSettings"])) { echo "none"; } else { echo "block"; } ?>;">
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/settings/inc/users-settings.php" ?>
|
||||
</div>
|
||||
|
||||
<!-- profile settings zone -->
|
||||
<div class="col" id="divProfileSettings" style="display: <?php if (isset($_GET["profileSettings"])) { echo "block"; } else { echo "none"; } ?>;">
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/settings/inc/profile-settings.php" ?>
|
||||
</div>
|
||||
|
||||
<!-- security settings zone -->
|
||||
<div class="col" id="divSecuritySettings" style="display: <?php if (isset($_GET["securitySettings"])) { echo "block"; } else { echo "none"; } ?>;">
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/settings/inc/security-settings.php" ?>
|
||||
</div>
|
||||
|
||||
<!-- integrations settings zone -->
|
||||
<div class="col" id="divIntegrationsSettings" style="display: <?php if (isset($_GET["integrationsSettings"])) { echo "block"; } else { echo "none"; } ?>;">
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/settings/inc/integration-settings.php" ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
if (window.location.search.indexOf("users=1") !== -1) {
|
||||
changeActive(null, 'divUsers');
|
||||
}
|
||||
if (window.location.search.indexOf("profileSettings=1") !== -1) {
|
||||
changeActive(null, 'divProfileSettings');
|
||||
}
|
||||
if (window.location.search.indexOf("securitySettings=1") !== -1) {
|
||||
changeActive(null, 'divSecuritySettings');
|
||||
}
|
||||
if (window.location.search.indexOf("integrationsSettings=1") !== -1) {
|
||||
changeActive(null, 'divIntegrationsSettings');
|
||||
}
|
||||
});
|
||||
|
||||
function changeActive(event, div) {
|
||||
// Prevent the default link behavior
|
||||
if (event) event.preventDefault();
|
||||
|
||||
// Remove the active class from all nav items
|
||||
var navItems = document.querySelectorAll('#sidebarNav .nav-item');
|
||||
navItems.forEach(function(item) {
|
||||
if (event) item.querySelector('.nav-link').classList.remove('active');
|
||||
});
|
||||
|
||||
// Add the active class to the clicked nav item
|
||||
if (event) event.target.classList.add('active');
|
||||
|
||||
if (div == "divUsers") {
|
||||
document.getElementById("divUsers").style.display = 'block';
|
||||
document.getElementById("divProfileSettings").style.display = 'none';
|
||||
document.getElementById("divSecuritySettings").style.display = 'none';
|
||||
document.getElementById("divIntegrationsSettings").style.display = 'none';
|
||||
} else {
|
||||
if (div == "divProfileSettings") {
|
||||
document.getElementById("divUsers").style.display = 'none';
|
||||
document.getElementById("divProfileSettings").style.display = 'block';
|
||||
document.getElementById("divSecuritySettings").style.display = 'none';
|
||||
document.getElementById("divIntegrationsSettings").style.display = 'none';
|
||||
} else {
|
||||
if (div == "divSecuritySettings") {
|
||||
document.getElementById("divUsers").style.display = 'none';
|
||||
document.getElementById("divProfileSettings").style.display = 'none';
|
||||
document.getElementById("divSecuritySettings").style.display = 'block';
|
||||
document.getElementById("divIntegrationsSettings").style.display = 'none';
|
||||
}else{
|
||||
if (div == "divIntegrationsSettings") {
|
||||
document.getElementById("divUsers").style.display = 'none';
|
||||
document.getElementById("divProfileSettings").style.display = 'none';
|
||||
document.getElementById("divSecuritySettings").style.display = 'none';
|
||||
document.getElementById("divIntegrationsSettings").style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/Template-Bottom.php" ?>
|
||||
13
frontend/src/App.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import NavbarComponent from './components/NavbarComponent.vue'
|
||||
import FooterComponent from './components/FooterComponent.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavbarComponent />
|
||||
<main class="container py-4 bg-body">
|
||||
<RouterView />
|
||||
</main>
|
||||
<FooterComponent />
|
||||
</template>
|
||||
0
frontend/img/avatar/bicycle1.png → frontend/src/assets/avatar/bicycle1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
0
frontend/img/avatar/bicycle2.png → frontend/src/assets/avatar/bicycle2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
frontend/img/avatar/female1.png → frontend/src/assets/avatar/female1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
0
frontend/img/avatar/male1.png → frontend/src/assets/avatar/male1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
0
frontend/img/avatar/running_shoe1.png → frontend/src/assets/avatar/running_shoe1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
0
frontend/img/avatar/running_shoe2.png → frontend/src/assets/avatar/running_shoe2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
0
frontend/img/avatar/wetsuit1.png → frontend/src/assets/avatar/wetsuit1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
frontend/img/avatar/wetsuit2.png → frontend/src/assets/avatar/wetsuit2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
102
frontend/src/components/Activities/ActivityMapComponent.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<!-- map zone -->
|
||||
<div v-if="isLoading">
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div ref="activityMap" class="map" style="height: 300px;" v-if="sourceProp === 'home'"></div>
|
||||
<div ref="activityMap" class="map" style="height: 500px;" v-if="sourceProp === 'activity'"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, watchEffect, nextTick } from 'vue';
|
||||
import { activityStreams } from '@/services/activityStreams';
|
||||
import LoadingComponent from '@/components/LoadingComponent.vue';
|
||||
import L from 'leaflet';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingComponent,
|
||||
},
|
||||
props: {
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
source:{
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const isLoading = ref(true);
|
||||
const activityStreamLatLng = ref(null);
|
||||
const activityMap = ref(null);
|
||||
const sourceProp = ref(props.source);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
activityStreamLatLng.value = await activityStreams.getActivitySteamByStreamTypeByActivityId(props.activity.id, 7);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch activity details:", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
if (activityStreamLatLng.value) {
|
||||
initMap();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
|
||||
});
|
||||
|
||||
const initMap = () => {
|
||||
if (!activityMap.value) return;
|
||||
|
||||
const waypoints = activityStreamLatLng.value.stream_waypoints;
|
||||
|
||||
|
||||
const map = L.map(activityMap.value, {
|
||||
dragging: false, // Disable panning
|
||||
touchZoom: false, // Disable touch zoom
|
||||
scrollWheelZoom: false, // Disable scroll wheel zoom
|
||||
zoomControl: false // Remove zoom control buttons
|
||||
}).fitWorld();
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
const latlngs = waypoints.map(waypoint => [waypoint.lat, waypoint.lon]);
|
||||
L.polyline(latlngs, { color: 'blue' }).addTo(map);
|
||||
|
||||
// Fit map to polyline bounds
|
||||
if (latlngs.length > 0) {
|
||||
map.fitBounds(latlngs);
|
||||
|
||||
// Add start and end markers
|
||||
L.marker(latlngs[0], {
|
||||
icon: L.divIcon({ className: 'bg-success dot' })
|
||||
}).addTo(map);
|
||||
|
||||
L.marker(latlngs[latlngs.length - 1], {
|
||||
icon: L.divIcon({ className: 'bg-danger dot' })
|
||||
}).addTo(map);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
activityStreamLatLng,
|
||||
activityMap,
|
||||
sourceProp,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<canvas ref="chartCanvas"></canvas>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
Chart.register(...registerables);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
graphSelection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
activityStreams: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const chartCanvas = ref(null);
|
||||
let myChart = null;
|
||||
const computedChartData = computed(() => {
|
||||
const data = [];
|
||||
let label = "";
|
||||
const labels = [];
|
||||
let roundValues = true;
|
||||
|
||||
props.activityStreams.forEach((stream) => {
|
||||
if (stream.stream_type == 1 && props.graphSelection == 'hr') {
|
||||
for (const streamPoint of stream.stream_waypoints) {
|
||||
data.push(parseInt(streamPoint.hr));
|
||||
label = "Heart Rate (bpm)";
|
||||
}
|
||||
} else if (stream.stream_type == 2 && props.graphSelection == 'power') {
|
||||
for (const streamPoint of stream.stream_waypoints) {
|
||||
data.push(parseInt(streamPoint.power));
|
||||
label = "Power (Watts)";
|
||||
}
|
||||
} else if (stream.stream_type == 3 && props.graphSelection == 'cad') {
|
||||
for (const streamPoint of stream.stream_waypoints) {
|
||||
data.push(parseInt(streamPoint.cad));
|
||||
label = "Cadence (rpm)";
|
||||
}
|
||||
} else if (stream.stream_type == 4 && props.graphSelection == 'ele') {
|
||||
for (const streamPoint of stream.stream_waypoints) {
|
||||
data.push(parseFloat(streamPoint.ele));
|
||||
label = "Elevation (m)";
|
||||
}
|
||||
} else if (stream.stream_type == 5 && props.graphSelection == 'vel') {
|
||||
data.push(...stream.stream_waypoints.map(velData => parseFloat((velData.vel * 3.6).toFixed(0))));
|
||||
label = "Velocity (km/h)";
|
||||
} else if (stream.stream_type == 6 && props.graphSelection == 'pace') {
|
||||
roundValues = false;
|
||||
stream.stream_waypoints.forEach(paceData => {
|
||||
if (paceData.pace == 0 || paceData.pace === null) {
|
||||
data.push(0);
|
||||
} else {
|
||||
if (props.activity.activity_type == 1 || props.activity.activity_type == 2 || props.activity.activity_type == 3) {
|
||||
data.push((paceData.pace * 1000) / 60);
|
||||
} else if (props.activity.activity_type == 8 || props.activity.activity_type == 9) {
|
||||
data.push((paceData.pace * 100) / 60);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (props.activity.activity_type == 1 || props.activity.activity_type == 2 || props.activity.activity_type == 3) {
|
||||
label = "Pace (min/km)";
|
||||
} else if (props.activity.activity_type == 8 || props.activity.activity_type == 9) {
|
||||
label = "Pace (min/100m)";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const dataDS = downsampleData(data, 200, roundValues);
|
||||
|
||||
const totalDistance = props.activity.distance / 1000;
|
||||
const numberOfDataPoints = dataDS.length;
|
||||
const distanceInterval = totalDistance / numberOfDataPoints;
|
||||
|
||||
for (let i = 0; i < numberOfDataPoints; i++) {
|
||||
labels.push((i * distanceInterval).toFixed(0) + "km");
|
||||
/* if (props.graphSelection == 'pace') {
|
||||
let paceCalculated = 0;
|
||||
if (props.activity.activity_type == 1 || props.activity.activity_type == 2 || props.activity.activity_type == 3) {
|
||||
paceCalculated = (dataDS[i] * 1000) / 60;
|
||||
} else if (props.activity.activity_type == 8 || props.activity.activity_type == 9) {
|
||||
paceCalculated = (dataDS[i] * 100) / 60;
|
||||
}
|
||||
const minutes = Math.floor(paceCalculated);
|
||||
const seconds = Math.round((paceCalculated - minutes) * 60);
|
||||
dataDS[i] = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
} */
|
||||
}
|
||||
|
||||
return {
|
||||
datasets: [{
|
||||
label: label,
|
||||
data: dataDS,
|
||||
}],
|
||||
labels: labels,
|
||||
};
|
||||
});
|
||||
|
||||
watch(computedChartData, (newChartData) => {
|
||||
if (myChart.value) {
|
||||
myChart.value.data.datasets = newChartData.datasets;
|
||||
myChart.data.labels = newChartData.labels;
|
||||
myChart.value.update();
|
||||
}
|
||||
console.log(computedChartData.value)
|
||||
}, { deep: true });
|
||||
|
||||
function downsampleData(data, threshold, roundValues) {
|
||||
if (data.length <= threshold) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const factor = Math.ceil(data.length / threshold);
|
||||
const downsampledData = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += factor) {
|
||||
const chunk = data.slice(i, i + factor);
|
||||
const average = chunk.reduce((a, b) => a + b) / chunk.length;
|
||||
if (roundValues) {
|
||||
downsampledData.push(parseInt(average));
|
||||
}else{
|
||||
downsampledData.push(average);
|
||||
}
|
||||
}
|
||||
|
||||
return downsampledData;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
myChart = new Chart(chartCanvas.value.getContext('2d'), {
|
||||
type: 'line',
|
||||
data: computedChartData.value,
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false
|
||||
},
|
||||
x: {
|
||||
autoSkip: true
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (myChart.value) {
|
||||
myChart.value.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
chartCanvas
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
264
frontend/src/components/Activities/ActivitySummaryComponent.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<!-- Error alerts -->
|
||||
<ErrorToastComponent v-if="errorMessage" />
|
||||
|
||||
<div v-if="isLoading">
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="d-flex justify-content-between">
|
||||
<!-- user name and photo zone -->
|
||||
<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'">
|
||||
{{ activity.name}}
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'user', params: { id: userActivity.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="sourceProp === 'activity'">
|
||||
{{ userActivity.name}}
|
||||
</router-link>
|
||||
</div>
|
||||
<h6>
|
||||
<span v-if="activity.activity_type == 1 || activity.activity_type == 2">
|
||||
<font-awesome-icon :icon="['fas', 'person-running']" />
|
||||
</span>
|
||||
<span v-else-if="activity.activity_type == 3">
|
||||
<font-awesome-icon :icon="['fas', 'person-running']" /> (Virtual)
|
||||
</span>
|
||||
<span v-else-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6">
|
||||
<font-awesome-icon :icon="['fas', 'fa-person-biking']" />
|
||||
</span>
|
||||
<span v-else-if="activity.activity_type == 7">
|
||||
<font-awesome-icon :icon="['fas', 'fa-person-biking']" /> (Virtual)
|
||||
</span>
|
||||
<span v-else-if="activity.activity_type == 8 || activity.activity_type == 9">
|
||||
<font-awesome-icon :icon="['fas', 'fa-person-swimming']" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<font-awesome-icon :icon="['fas', 'fa-dumbbell']" />
|
||||
</span>
|
||||
<span>{{ " " + formatDate(activity.start_time) }}</span> @
|
||||
<span>{{ formatTime(activity.start_time) }}</span>
|
||||
<!-- Conditionally display city and country -->
|
||||
<span v-if="activity.city || activity.country">
|
||||
-
|
||||
<span v-if="activity.town">{{ activity.town }},</span>
|
||||
<span v-if="activity.country">{{ " " + activity.country }}</span>
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-flex">
|
||||
<a class="btn btn-link btn-lg mt-1 link-body-emphasis" :href="`https://www.strava.com/activities/${activity.strava_activity_id}`" role="button" v-if="activity.strava_activity_id">
|
||||
<font-awesome-icon :icon="['fab', 'fa-strava']" />
|
||||
</a>
|
||||
<div v-if="sourceProp === 'activity'">
|
||||
<button class="btn btn-link btn-lg link-body-emphasis" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<font-awesome-icon :icon="['fas', 'fa-ellipsis-vertical']" />
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" :class="{ disabled: activity.strava_activity_id }" href="#" data-bs-toggle="modal" data-bs-target="#deleteActivityModal">
|
||||
{{ $t("activitySummary.buttonDeleteActivity") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal delete gear -->
|
||||
<div class="modal fade" id="deleteActivityModal" tabindex="-1" aria-labelledby="deleteActivityModal"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteActivityModal">
|
||||
{{ $t("activitySummary.buttonDeleteActivity") }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span>{{ $t("activitySummary.modalDeleteBody1") }}<b>{{ activity.name }}</b>?</span>
|
||||
<br>
|
||||
<span>{{ $t("activitySummary.modalDeleteBody2") }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t("generalItens.buttonClose") }}
|
||||
</button>
|
||||
<a @click="submitDeleteActivity" type="button" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("activitySummary.buttonDeleteActivity") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity title -->
|
||||
<h1 class="mt-3" v-if="sourceProp === 'activity'">
|
||||
{{ activity.name }}
|
||||
</h1>
|
||||
<!-- Activity summary -->
|
||||
<div class="row d-flex mt-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityDistance") }}
|
||||
</span>
|
||||
<br>
|
||||
<span>
|
||||
<!-- Check if activity_type is not 9 -->
|
||||
{{ activity.activity_type != 9
|
||||
? (activity.distance / 1000).toFixed(2) + ' km' : activity.distance + ' m'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityTime") }}
|
||||
</span>
|
||||
<br>
|
||||
<span>{{ calculateTimeDifference(activity.start_time, activity.end_time) }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<div v-if="activity.activity_type != 9 && activity.activity_type != 1">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityElevationGain") }}
|
||||
</span>
|
||||
<br>
|
||||
<span>{{ activity.elevation_gain }} m</span>
|
||||
</div>
|
||||
<div v-else-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3 || activity.activity_type == 9">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityPace") }}
|
||||
</span>
|
||||
<br>
|
||||
{{ formattedPace }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-flex mt-3" v-if="sourceProp === 'activity'">
|
||||
<div class="col" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityAvgPower") }}
|
||||
</span>
|
||||
<br>
|
||||
<span v-if="activity.average_power">{{ activity.average_power }} W</span>
|
||||
<span v-else>{{ $t("activitySummary.activityNoData") }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
|
||||
<span class="fw-lighter">{{ $t("activitySummary.activityEleGain") }}</span>
|
||||
<br>
|
||||
<span>{{ activity.elevation_gain }} m</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityEleLoss") }}
|
||||
</span>
|
||||
<br>
|
||||
<span>{{ activity.elevation_loss }} m</span>
|
||||
</div>
|
||||
<div class="col" v-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6">
|
||||
<span class="fw-lighter">{{ $t("activitySummary.activityAvgSpeed") }}</span>
|
||||
<br>
|
||||
<span>{{ (activity.average_speed * 3.6).toFixed(0) }} km/h</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummary.activityAvgPower") }}
|
||||
</span>
|
||||
<br>
|
||||
<span v-if="activity.average_power">{{ activity.average_power }} W</span>
|
||||
<span v-else>{{ $t("activitySummary.activityNoData") }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6">
|
||||
<span class="fw-lighter">{{ $t("activitySummary.activityEleLoss") }}</span>
|
||||
<br>
|
||||
<span>{{ activity.elevation_loss }} m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, watchEffect, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
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
|
||||
import { users } from '@/services/user';
|
||||
import { activities } from '@/services/activities';
|
||||
import { formatDate, formatTime, calculateTimeDifference } from '@/utils/dateTimeUtils';
|
||||
import { formatPace } from '@/utils/activityUtils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingComponent,
|
||||
ErrorToastComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
props: {
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
source:{
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const isLoading = ref(true);
|
||||
const errorMessage = ref('');
|
||||
const userActivity = ref(null);
|
||||
const formattedPace = computed(() => formatPace(props.activity.pace));
|
||||
const sourceProp = ref(props.source);
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
userActivity.value = await users.getUserById(props.activity.user_id);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch activity details:", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
|
||||
});
|
||||
|
||||
async function submitDeleteActivity() {
|
||||
try {
|
||||
userActivity.value = await activities.deleteActivity(props.activity.id);
|
||||
router.push({ path: '/', query: { activityDeleted: 'true', activityId: props.activity.id } });
|
||||
} catch (error) {
|
||||
errorMessage.value = t('generalItens.errorDeletingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
t,
|
||||
userActivity,
|
||||
formatDate,
|
||||
formatTime,
|
||||
calculateTimeDifference,
|
||||
formattedPace,
|
||||
sourceProp,
|
||||
submitDeleteActivity,
|
||||
errorMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="mb-3 mt-3">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.thisWeekDistancesTitle") }}</span>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.distancesRun") }}</span>
|
||||
<br>
|
||||
<span>{{ thisWeekDistances && thisWeekDistances.run ? (thisWeekDistances.run / 1000).toFixed(2) + ' km' : '0 km' }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.distancesBike") }}</span>
|
||||
<br>
|
||||
<span>{{ thisWeekDistances && thisWeekDistances.bike ? (thisWeekDistances.bike / 1000).toFixed(2) + ' km' : '0 km' }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.distancesSwim") }}</span>
|
||||
<br>
|
||||
<span v-if="thisWeekDistances && thisWeekDistances.swim">
|
||||
{{ thisWeekDistances.swim > 10000 ? (thisWeekDistances.swim / 1000).toFixed(2) + ' km' : thisWeekDistances.swim + ' m' }}
|
||||
</span>
|
||||
<span v-else>0 m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.thisMonthDistancesTitle") }}</span>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.distancesRun") }}</span>
|
||||
<br>
|
||||
<span>{{ thisMonthDistances && thisMonthDistances.run ? (thisMonthDistances.run / 1000).toFixed(2) + ' km' : '0 km' }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.distancesBike") }}</span>
|
||||
<br>
|
||||
<span>{{ thisMonthDistances && thisMonthDistances.bike ? (thisMonthDistances.bike / 1000).toFixed(2) + ' km' : '0 km' }}</span>
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<span class="fw-lighter">{{ $t("userDistanceStats.distancesSwim") }}</span>
|
||||
<br>
|
||||
<span v-if="thisMonthDistances && thisMonthDistances.swim">
|
||||
{{ thisMonthDistances.swim > 10000 ? (thisMonthDistances.swim / 1000).toFixed(2) + ' km' : thisMonthDistances.swim + ' m' }}
|
||||
</span>
|
||||
<span v-else>0 m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Access both stats directly from the store
|
||||
const thisWeekDistances = computed(() => userStore.thisWeekDistances);
|
||||
const thisMonthDistances = computed(() => userStore.thisMonthDistances);
|
||||
|
||||
return {
|
||||
thisWeekDistances,
|
||||
thisMonthDistances,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
29
frontend/src/components/Alerts/ErrorAlertComponent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="alert alert-danger alert-dismissible d-flex align-items-center" role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'fa-circle-exclamation']" />
|
||||
<div class="ms-1">
|
||||
<span>{{ errorMessage }}</span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" v-if="closable"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
|
||||
// Access both stats directly from the store
|
||||
const errorMessage = computed(() => errorAlertStore.message);
|
||||
const closable = computed(() => errorAlertStore.closable);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
closable,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
29
frontend/src/components/Alerts/InfoAlertComponent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center" role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'fa-triangle-exclamation']" />
|
||||
<div class="ms-1">
|
||||
<span>{{ errorMessage }}</span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" v-if="closable"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { useInfoAlertStore } from '@/stores/Alerts/infoAlert';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const infoAlertStore = useInfoAlertStore();
|
||||
|
||||
// Access both stats directly from the store
|
||||
const errorMessage = computed(() => infoAlertStore.message);
|
||||
const closable = computed(() => infoAlertStore.closable);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
closable,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
28
frontend/src/components/Alerts/LoadingAlertComponent.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="alert alert-primary d-flex align-items-center" role="alert">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div class="ms-1">
|
||||
<span>{{ loadingMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { useLoadingAlertStore } from '@/stores/Alerts/loadingAlert';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const loadingAlertStore = useLoadingAlertStore();
|
||||
|
||||
// Access both stats directly from the store
|
||||
const loadingMessage = computed(() => loadingAlertStore.message);
|
||||
|
||||
return {
|
||||
loadingMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
29
frontend/src/components/Alerts/SuccessAlertComponent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'circle-check']" />
|
||||
<div class="ms-1">
|
||||
<span>{{ successMessage }}</span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" v-if="closable"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const successAlertStore = useSuccessAlertStore();
|
||||
|
||||
// Access both stats directly from the store
|
||||
const successMessage = computed(() => successAlertStore.message);
|
||||
const closable = computed(() => successAlertStore.closable);
|
||||
|
||||
return {
|
||||
successMessage,
|
||||
closable,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
28
frontend/src/components/BackButtonComponent.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<!-- back button -->
|
||||
<div>
|
||||
<br class="d-lg-none">
|
||||
<button @click="goBack" type="button" class="w-100 btn btn-primary d-lg-none">{{ $t("generalItens.buttonBack") }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
function goBack() {
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
return {
|
||||
goBack,
|
||||
t,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
233
frontend/src/components/Followers/FollowersListComponent.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div v-if="isLoading">
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<div class="d-flex align-items-center" v-if="!isLoading">
|
||||
<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">
|
||||
{{ userFollower.name }}
|
||||
</router-link>
|
||||
<!--<a :href="`/user/${userFollower.id}`" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover">
|
||||
{{ userFollower.name }}
|
||||
</a>-->
|
||||
</div>
|
||||
{{ userFollower.username }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-3 align-middle" v-if="!isLoading">
|
||||
<!-- badge is accepted -->
|
||||
<span class="badge bg-success-subtle border border-success-subtle text-success-emphasis rounded-pill align-middle" v-if="followerProp.is_accepted == 1">{{ $t("followersListComponent.requestAccepted") }}</span>
|
||||
<!-- badge pending request -->
|
||||
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis rounded-pill align-middle" v-else>{{ $t("followersListComponent.requestPending") }}</span>
|
||||
|
||||
<!-- delete following zone -->
|
||||
<a class="ms-2 btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#deleteFollowingModal${userFollower.id}`" v-if="typeProp == 1 && loggedUserId == idFromParam"><font-awesome-icon :icon="['fas', 'fa-trash']" /></a>
|
||||
|
||||
<div class="modal fade" :id="`deleteFollowingModal${userFollower.id}`" tabindex="-1" :aria-labelledby="`deleteFollowingModal${userFollower.id}`" aria-hidden="true" v-if="typeProp == 1 && loggedUserId == idFromParam">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">
|
||||
{{ $t("followersListComponent.followingModalTitle") }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ $t("followersListComponent.followingModalBody") }}<b>
|
||||
{{ userFollower.name }}
|
||||
</b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t("generalItens.buttonClose") }}
|
||||
</button>
|
||||
<a @click="submitDeleteFollowing" type="button" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("followersListComponent.followingModalTitle") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- delete follower zone -->
|
||||
<a class="ms-2 btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#deleteFollowerModal${userFollower.id}`" v-if="typeProp != 1 && loggedUserId == idFromParam && followerProp.is_accepted == 1"><font-awesome-icon :icon="['fas', 'fa-trash']" /></a>
|
||||
|
||||
<!-- Modal delete follower -->
|
||||
<div class="modal fade" :id="`deleteFollowerModal${userFollower.id}`" tabindex="-1" :aria-labelledby="`deleteFollowerModal${userFollower.id}`" aria-hidden="true" v-if="typeProp != 1 && loggedUserId == idFromParam && followerProp.is_accepted == 1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">
|
||||
{{ $t("followersListComponent.followerModalTitle") }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ $t("followersListComponent.followerModalBody") }}<b>
|
||||
{{ userFollower.name }}
|
||||
</b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t("generalItens.buttonClose") }}
|
||||
</button>
|
||||
<a @click="submitDeleteFollower" type="button" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("followersListComponent.followerModalTitle") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- accept folllower request -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#acceptRequestModal${userFollower.id}`" v-if="typeProp != 1 && loggedUserId == idFromParam && followerProp.is_accepted == 0"><font-awesome-icon :icon="['fas', 'fa-check']" /></a>
|
||||
|
||||
<!-- Modal accept user request -->
|
||||
<div class="modal fade" :id="`acceptRequestModal${userFollower.id}`" tabindex="-1" :aria-labelledby="`acceptRequestModal${userFollower.id}`" aria-hidden="true" v-if="typeProp != 1 && loggedUserId == idFromParam && followerProp.is_accepted == 0">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">
|
||||
{{ $t("followersListComponent.followerAcceptModalTitle") }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ $t("followersListComponent.followerAcceptModalBody") }}<b>
|
||||
{{ userFollower.name }}
|
||||
</b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t("generalItens.buttonClose") }}
|
||||
</button>
|
||||
<a @click="submitAcceptFollowerRequest" type="button" class="btn btn-success" data-bs-dismiss="modal">
|
||||
{{ $t("followersListComponent.followerAcceptModalTitle") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- decline user request button -->
|
||||
<a class="ms-2 btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#declineRequestModal${userFollower.id}`" v-if="typeProp != 1 && loggedUserId == idFromParam && followerProp.is_accepted == 0"><font-awesome-icon :icon="['fas', 'fa-x']" /></a>
|
||||
|
||||
<!-- Modal decline user request -->
|
||||
<div class="modal fade" :id="`declineRequestModal${userFollower.id}`" tabindex="-1" :aria-labelledby="`declineRequestModal${userFollower.id}`" aria-hidden="true" v-if="typeProp != 1 && loggedUserId == idFromParam && followerProp.is_accepted == 0">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">
|
||||
{{ $t("followersListComponent.followerDeclineModalTitle") }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ $t("followersListComponent.followerDeclineModalBody") }}<b>
|
||||
{{ userFollower.name }}
|
||||
</b>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t("generalItens.buttonClose") }}
|
||||
</button>
|
||||
<a @click="submitDeleteFollower" type="button" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("followersListComponent.followerDeclineModalTitle") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
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: {
|
||||
follower: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
type:{
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const route = useRoute();
|
||||
const userFollower = ref(null);
|
||||
const isLoading = ref(true);
|
||||
const loggedUserId = JSON.parse(localStorage.getItem('userMe')).id;
|
||||
const idFromParam = computed(() => route.params.id);
|
||||
const followerProp = ref(props.follower);
|
||||
const typeProp = ref(props.type);
|
||||
|
||||
async function submitDeleteFollowing() {
|
||||
try {
|
||||
await followers.deleteUserFollowsSpecificUser(props.follower.follower_id, props.follower.following_id);
|
||||
emit('followingDeleted', props.follower.following_id);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete following:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDeleteFollower() {
|
||||
try {
|
||||
await followers.deleteUserFollowsSpecificUser(props.follower.follower_id, props.follower.following_id);
|
||||
emit('followerDeleted', props.follower.follower_id);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete follower:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAcceptFollowerRequest() {
|
||||
try {
|
||||
await followers.acceptUserFollowsSpecificUser(props.follower.following_id, props.follower.follower_id);
|
||||
emit('followerAccepted', props.follower.follower_id);
|
||||
} catch (error) {
|
||||
console.error("Failed to update follower:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (props.type == '1') {
|
||||
userFollower.value = await users.getUserById(props.follower.following_id);
|
||||
} else {
|
||||
userFollower.value = await users.getUserById(props.follower.follower_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch follower details:", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
userFollower,
|
||||
isLoading,
|
||||
idFromParam,
|
||||
loggedUserId,
|
||||
followerProp,
|
||||
typeProp,
|
||||
submitDeleteFollowing,
|
||||
submitDeleteFollower,
|
||||
submitAcceptFollowerRequest,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
137
frontend/src/components/FooterComponent.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<footer class="py-5 bg-body-tertiary">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-center">
|
||||
<div class="col-md" v-if="isLoggedIn">
|
||||
<form>
|
||||
<label for="inputSelectTypeToSearch" class="form-label">{{ $t("footer.searchSelectLabel") }}</label>
|
||||
<select id="inputSelectTypeToSearch" class="form-select" v-model="searchSelectValue">
|
||||
<option value="1">{{ $t("footer.searchSelectOptionUser") }}</option>
|
||||
<option value="2">{{ $t("footer.searchSelectOptionActivity") }}</option>
|
||||
<option value="3">{{ $t("footer.searchSelectOptionGear") }}</option>
|
||||
</select>
|
||||
<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">
|
||||
<!-- 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}}
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'activity', params: { id: result.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-else-if="searchSelectValue == 2">
|
||||
{{ result.name}}
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'gear', params: { id: result.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-else-if="searchSelectValue == 3">
|
||||
{{ result.nickname }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mt-3 col">
|
||||
<p class="text-center text-muted">© {{ new Date().getFullYear() === 2023 ? '2023' : '2023 - ' + new Date().getFullYear() }} Endurain • <a class="link-body-emphasis" href="https://github.com/joaovitoriasilva/endurain" role="button"><font-awesome-icon :icon="['fab', 'fa-github']" /></a> • <a class="link-body-emphasis" href="https://fosstodon.org/@endurain"><font-awesome-icon :icon="['fab', 'fa-mastodon']" /></a> • v0.2.0</p>
|
||||
<p class="text-center text-muted"><img src="/src/assets/strava/api_logo_cptblWith_strava_horiz_light.png" alt="Compatible with STRAVA image" height="25" /></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { watch, ref } from 'vue';
|
||||
import { auth } from '@/services/auth';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { users } from '@/services/user';
|
||||
import { gears } from '@/services/gears';
|
||||
import { activities } from '@/services/activities';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const path = ref(route.path);
|
||||
const isLoggedIn = ref(auth.isTokenValid(localStorage.getItem('accessToken')))
|
||||
const searchSelectValue = ref(1);
|
||||
const inputSearch = ref('');
|
||||
const searchResults = ref([]);
|
||||
|
||||
const fetchUserResults = async (query) => {
|
||||
if (!query) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
searchResults.value = await users.getUserByUsername(query);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user results:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchActivityResults = async (query) => {
|
||||
if (!query) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Replace with your actual API call for activities
|
||||
searchResults.value = await activities.getActivityByName(query);
|
||||
} catch (error) {
|
||||
console.error('Error fetching activity results:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchGearResults = async (query) => {
|
||||
if (!query) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Replace with your actual API call for gear
|
||||
searchResults.value = await gears.getGearByNickname(query);
|
||||
} catch (error) {
|
||||
console.error('Error fetching gear results:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => route.path, (newPath, oldPath) => {
|
||||
path.value = newPath;
|
||||
/* reset search values */
|
||||
searchSelectValue.value = 1;
|
||||
inputSearch.value = '';
|
||||
searchResults.value = [];
|
||||
// Perform actions based on newPath if needed
|
||||
if (newPath === '/login' && isLoggedIn.value) {
|
||||
isLoggedIn.value = auth.isTokenValid(localStorage.getItem('accessToken'));
|
||||
}
|
||||
if (oldPath === '/login' && !isLoggedIn.value) {
|
||||
isLoggedIn.value = auth.isTokenValid(localStorage.getItem('accessToken'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(searchSelectValue, () => {
|
||||
inputSearch.value = '';
|
||||
searchResults.value = [];
|
||||
});
|
||||
|
||||
watch(inputSearch, async (newQuery) => {
|
||||
if (searchSelectValue.value == 1) {
|
||||
await fetchUserResults(newQuery);
|
||||
} else if (searchSelectValue.value == 2) {
|
||||
await fetchActivityResults(newQuery);
|
||||
} else {
|
||||
await fetchGearResults(newQuery);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
searchSelectValue,
|
||||
inputSearch,
|
||||
searchResults,
|
||||
t,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
7
frontend/src/components/LoadingComponent.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
114
frontend/src/components/NavbarComponent.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<router-link :to="{ name: 'home' }" class="navbar-brand" @click="collapseNavbar">
|
||||
Endurain
|
||||
</router-link>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup" ref="navbarCollapse">
|
||||
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<!-- if is logged in -->
|
||||
<router-link :to="{ name: 'gears' }" class="nav-link" v-if="isLoggedIn" @click="collapseNavbar">
|
||||
<font-awesome-icon :icon="['fas', 'fa-bicycle']" />
|
||||
<span class="ms-1">
|
||||
{{ $t("navbar.gear") }}
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<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" @click="collapseNavbar">
|
||||
<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>
|
||||
<a class="nav-link d-none d-sm-block" v-if="isLoggedIn">|</a>
|
||||
<router-link :to="{ name: 'settings' }" class="nav-link" v-if="isLoggedIn" @click="collapseNavbar">
|
||||
<font-awesome-icon :icon="['fas', 'fa-gear']" />
|
||||
<span class="ms-1">{{ $t("navbar.settings") }}</span>
|
||||
</router-link>
|
||||
<a class="nav-link" href="#" v-if="isLoggedIn" @click="handleLogout">
|
||||
<font-awesome-icon :icon="['fas', 'fa-sign-out-alt']" />
|
||||
<span class="ms-1">{{ $t("navbar.logout") }}</span>
|
||||
</a>
|
||||
<!-- if is not logged in -->
|
||||
<router-link :to="{ name: 'login' }" class="nav-link" v-if="!isLoggedIn" @click="collapseNavbar">
|
||||
<font-awesome-icon :icon="['fas', 'fa-sign-in-alt']" />
|
||||
<span class="ms-1">{{ $t("navbar.login") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="alert alert-warning alert-dismissible d-flex align-items-center mx-2 my-2 justify-content-center"
|
||||
role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'triangle-exclamation']" />
|
||||
<div class="ms-2">
|
||||
<span>
|
||||
{{ $t("navbar.warningZone") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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()
|
||||
const path = ref(route.path)
|
||||
const isLoggedIn = ref(auth.isTokenValid(localStorage.getItem('accessToken')))
|
||||
const userMe = ref(JSON.parse(localStorage.getItem('userMe')))
|
||||
|
||||
function handleLogout() {
|
||||
auth.removeLoggedUser()
|
||||
router.push('/login')
|
||||
collapseNavbar()
|
||||
}
|
||||
|
||||
function updateVariablesBasedOnLocalStorage() {
|
||||
isLoggedIn.value = auth.isTokenValid(localStorage.getItem('accessToken'))
|
||||
userMe.value = JSON.parse(localStorage.getItem('userMe'))
|
||||
}
|
||||
|
||||
function collapseNavbar() {
|
||||
const navbarToggler = document.querySelector('.navbar-toggler');
|
||||
const navbarCollapse = document.querySelector('#navbarNavAltMarkup');
|
||||
if (navbarToggler && navbarCollapse.classList.contains('show')) {
|
||||
navbarToggler.click();
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.path, (newPath, oldPath) => {
|
||||
path.value = newPath;
|
||||
// Perform actions based on newPath if needed
|
||||
if (newPath === '/login' && isLoggedIn.value) {
|
||||
updateVariablesBasedOnLocalStorage()
|
||||
}
|
||||
if (oldPath === '/login' && !isLoggedIn.value) {
|
||||
updateVariablesBasedOnLocalStorage()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userMe,
|
||||
handleLogout,
|
||||
path,
|
||||
collapseNavbar,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
11
frontend/src/components/NoItemsFoundComponents.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="centered-card">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<!--<h5 class="card-title">{{ $t("noItemsFoundComponent.title") }}</h5>-->
|
||||
<h1 class="card-text"><font-awesome-icon :icon="['fas', 'fa-circle-info']" /></h1>
|
||||
<p class="card-text">{{ $t("noItemsFoundComponent.subtitle") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
125
frontend/src/components/Settings/SettingsIntegrationsZone.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<ErrorToastComponent v-if="errorMessage" />
|
||||
<SuccessToastComponent v-if="successMessage" />
|
||||
|
||||
<div class="row row-gap-3 row-cols-sm-3 align-items-center">
|
||||
<div class="col">
|
||||
<div class="card text-center">
|
||||
<img src="/src/assets/strava/api_logo_cptblWith_strava_stack_light.png" alt="Compatible with Strava image" class="card-img-top">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{ $t("settingsIntegrationsZone.stravaIntegrationTitle") }}</h4>
|
||||
<p class="card-text">{{ $t("settingsIntegrationsZone.stravaIntegrationBody") }}</p>
|
||||
<a href="#" class="btn btn-primary" :class="{ 'disabled': userMe.is_strava_linked == 1 }" @click="submitConnectStrava">{{ $t("settingsIntegrationsZone.buttonConnect") }}</a>
|
||||
<div v-if="userMe.is_strava_linked == 1">
|
||||
<hr>
|
||||
<a href="#" class="btn btn-primary" @click="submitRetrieveStravaLastWeekActivities">{{ $t("settingsIntegrationsZone.buttonStravaRetrieveLastWeekActivities") }}</a>
|
||||
<a href="#" class="btn btn-primary mt-3" @click="submitRetrieveStravaGear">{{ $t("settingsIntegrationsZone.buttonStravaRetrieveGear") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// Importing the services
|
||||
import { strava } from '@/services/strava';
|
||||
// Importing the stores
|
||||
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
import { useLoadingAlertStore } from '@/stores/Alerts/loadingAlert';
|
||||
// Importing the components
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
},
|
||||
setup() {
|
||||
const userMe = JSON.parse(localStorage.getItem('userMe'));
|
||||
const { t } = useI18n();
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
const successAlertStore = useSuccessAlertStore();
|
||||
const loadingAlertStore = useLoadingAlertStore();
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const loadingMessage = ref('');
|
||||
|
||||
function resetMessageValues() {
|
||||
successMessage.value = '';
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
errorMessage.value = '';
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
|
||||
async function submitConnectStrava() {
|
||||
resetMessageValues();
|
||||
|
||||
const state = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
try{
|
||||
await strava.setUniqueUserStateStravaLink(state);
|
||||
|
||||
strava.linkStrava(state);
|
||||
} catch(error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('settingsIntegrationsZone.errorMessageUnableToLinkStrava') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRetrieveStravaLastWeekActivities() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
await strava.getStravaActivitiesLastDays(7);
|
||||
|
||||
// Set the loading message and show the loading alert.
|
||||
loadingMessage.value = t('settingsIntegrationsZone.loadingMessageRetrievingStravaActivities');
|
||||
loadingAlertStore.setAlertMessage(loadingMessage.value);
|
||||
loadingAlertStore.setClosableState(true);
|
||||
} catch(error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('settingsIntegrationsZone.errorMessageUnableToGetStravaActivities') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRetrieveStravaGear() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
await strava.getStravaGear();
|
||||
|
||||
// Set the loading message and show the loading alert.
|
||||
loadingMessage.value = t('settingsIntegrationsZone.loadingMessageRetrievingStravaGear');
|
||||
loadingAlertStore.setAlertMessage(loadingMessage.value);
|
||||
loadingAlertStore.setClosableState(true);
|
||||
} catch(error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('settingsIntegrationsZone.errorMessageUnableToGetStravaGear') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userMe,
|
||||
t,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
loadingMessage,
|
||||
submitConnectStrava,
|
||||
submitRetrieveStravaLastWeekActivities,
|
||||
submitRetrieveStravaGear,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<!-- info banner to display password complexity requirements -->
|
||||
<div class="alert alert-info alert-dismissible d-flex align-items-center" role="alert">
|
||||
<!--<i class="fa-solid fa-circle-info me-1 allign-top"></i>-->
|
||||
<div>
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordPasswordRequirementsTitle") }}
|
||||
<br>
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordCharacters") }}
|
||||
<br>
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordCapitalLetters") }}
|
||||
<br>
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordNumbers") }}
|
||||
<br>
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordSpecialCharacters") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
119
frontend/src/components/Settings/SettingsSecurityZone.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<ErrorToastComponent v-if="errorMessage" />
|
||||
<SuccessToastComponent v-if="successMessage" />
|
||||
|
||||
<h4>{{ $t("settingsSecurityZone.subtitleChangePassword") }}</h4>
|
||||
<SettingsPasswordRequirementsComponent />
|
||||
|
||||
<form @submit.prevent="submitChangeUserPasswordForm">
|
||||
<!-- password fields -->
|
||||
<label for="validationNewPassword"><b>* {{ $t("usersListComponent.modalChangeUserPasswordPasswordLabel") }}</b></label>
|
||||
<input class="form-control" :class="{ 'is-invalid': !isNewPasswordValid || !isPasswordMatch }" type="password" id="validationNewPassword" aria-describedby="validationNewPasswordFeedback" :placeholder='$t("usersListComponent.modalChangeUserPasswordPasswordLabel")' v-model="newPassword" required>
|
||||
<div id="validationNewPasswordFeedback" class="invalid-feedback" v-if="!isNewPasswordValid">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordFeedbackLabel") }}
|
||||
</div>
|
||||
<div id="validationNewPasswordFeedback" class="invalid-feedback" v-if="!isPasswordMatch">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordPasswordsDoNotMatchFeedbackLabel") }}
|
||||
</div>
|
||||
<!-- repeat password fields -->
|
||||
|
||||
<label class="mt-1" for="validationNewPasswordRepeat"><b>* {{ $t("usersListComponent.modalChangeUserPasswordPasswordConfirmationLabel") }}</b></label>
|
||||
<input class="form-control" :class="{ 'is-invalid': !isNewPasswordRepeatValid || !isPasswordMatch }" type="password" id="validationNewPasswordRepeat" aria-describedby="validationNewPasswordRepeatFeedback" :placeholder='$t("usersListComponent.modalChangeUserPasswordPasswordConfirmationLabel")' v-model="newPasswordRepeat" required>
|
||||
<div id="validationNewPasswordRepeatFeedback" class="invalid-feedback" v-if="!isNewPasswordRepeatValid">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordFeedbackLabel") }}
|
||||
</div>
|
||||
<div id="validationNewPasswordRepeatFeedback" class="invalid-feedback" v-if="!isPasswordMatch">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordPasswordsDoNotMatchFeedbackLabel") }}
|
||||
</div>
|
||||
|
||||
<p>* {{ $t("generalItens.requiredField") }}</p>
|
||||
|
||||
<button type="submit" class="btn btn-success" name="editUserPassword">{{ $t("settingsSecurityZone.subtitleChangePassword") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// Importing the services
|
||||
import { users } from '@/services/user';
|
||||
// Importing the stores
|
||||
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
// Importing the components
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
import SettingsPasswordRequirementsComponent from '@/components/Settings/SettingsPasswordRequirementsComponent.vue';
|
||||
// Importing the crypto-js
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
SettingsPasswordRequirementsComponent,
|
||||
},
|
||||
setup() {
|
||||
const userMe = JSON.parse(localStorage.getItem('userMe'));
|
||||
const { t } = useI18n();
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
const successAlertStore = useSuccessAlertStore();
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const newPassword = ref('');
|
||||
const newPasswordRepeat = ref('');
|
||||
const isNewPasswordValid = computed(() => {
|
||||
const regex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/;
|
||||
return regex.test(newPassword.value);
|
||||
});
|
||||
const isNewPasswordRepeatValid = computed(() => {
|
||||
const regex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/;
|
||||
return regex.test(newPasswordRepeat.value);
|
||||
});
|
||||
const isPasswordMatch = computed(() => newPassword.value === newPasswordRepeat.value);
|
||||
|
||||
function resetMessageValues() {
|
||||
successMessage.value = '';
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
errorMessage.value = '';
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
|
||||
async function submitChangeUserPasswordForm() {
|
||||
resetMessageValues();
|
||||
|
||||
try{
|
||||
if (isNewPasswordValid.value && isNewPasswordRepeatValid.value && isPasswordMatch.value) {
|
||||
const data = {
|
||||
id: userMe.id,
|
||||
password: CryptoJS.SHA256(newPassword.value).toString(CryptoJS.enc.Hex),
|
||||
};
|
||||
await users.editUserPassword(data);
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('usersListComponent.userChangePasswordSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('usersListComponent.userChangePasswordErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
newPassword,
|
||||
newPasswordRepeat,
|
||||
isNewPasswordValid,
|
||||
isNewPasswordRepeatValid,
|
||||
isPasswordMatch,
|
||||
submitChangeUserPasswordForm
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<ul class="nav nav-pills flex-column mb-auto" id="sidebarNav">
|
||||
<li class="nav-item" v-if="userMe.access_type == 2">
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'users' }" @click.prevent="changeActive('users')">
|
||||
<font-awesome-icon :icon="['fas', 'fa-users']" />
|
||||
<span class="ms-1">{{ $t("settingsSideBar.usersSection") }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<hr v-if="userMe.access_type == 2">
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'myProfile' }" @click.prevent="changeActive('myProfile')">
|
||||
<font-awesome-icon :icon="['fas', 'fa-address-card']" />
|
||||
<span class="ms-1">{{ $t("settingsSideBar.myProfileSection") }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'security' }" @click.prevent="changeActive('security')">
|
||||
<font-awesome-icon :icon="['fas', 'fa-shield']" />
|
||||
<span class="ms-1">{{ $t("settingsSideBar.securitySection") }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'integrations' }" @click.prevent="changeActive('integrations')">
|
||||
<font-awesome-icon :icon="['fas', 'fa-puzzle-piece']" />
|
||||
<span class="ms-1">{{ $t("settingsSideBar.integratuionsSection") }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr class="d-lg-none">
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
activeSection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update-active-section'],
|
||||
setup(props, { emit }) {
|
||||
const userMe = JSON.parse(localStorage.getItem('userMe'));
|
||||
const { t } = useI18n();
|
||||
|
||||
function changeActive(section) {
|
||||
emit('update-active-section', section);
|
||||
}
|
||||
|
||||
return {
|
||||
userMe,
|
||||
t,
|
||||
changeActive,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
273
frontend/src/components/Settings/SettingsUserProfileZone.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<ErrorToastComponent v-if="errorMessage" />
|
||||
<SuccessToastComponent v-if="successMessage" />
|
||||
|
||||
<div class="row row-gap-3">
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="justify-content-center align-items-center d-flex">
|
||||
<UserAvatarComponent :userProp="userMe" :width=180 :height=180 />
|
||||
</div>
|
||||
|
||||
<!-- Delete profile photo section -->
|
||||
<a class="mt-4 w-100 btn btn-danger" href="#" role="button" data-bs-toggle="modal" data-bs-target="#deleteProfilePhotoModal" v-if="userMe.photo_path">{{ $t("settingsUserProfileZone.buttonDeleteProfilePhoto") }}</a>
|
||||
|
||||
<!-- Modal delete profile photo -->
|
||||
<div class="modal fade" id="deleteProfilePhotoModal" tabindex="-1" aria-labelledby="deleteProfilePhotoModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteProfilePhotoModal">{{ $t("settingsUserProfileZone.buttonDeleteProfilePhoto") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ $t("settingsUserProfileZone.modalDeleteProfilePhotoBody") }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
|
||||
<a type="button" class="btn btn-danger" data-bs-dismiss="modal" @click="submitDeleteUserPhoto">{{ $t("settingsUserProfileZone.buttonDeleteProfilePhoto") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit profile section -->
|
||||
<a class="mt-2 w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#editProfileModal">{{ $t("settingsUserProfileZone.buttonEditProfile") }}</a>
|
||||
|
||||
<!-- Modal edit user -->
|
||||
<div class="modal fade" id="editProfileModal" tabindex="-1" aria-labelledby="editProfileModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editProfileModal">{{ $t("usersListComponent.modalEditUserTitle") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form @submit.prevent="submitEditUserForm">
|
||||
<div class="modal-body">
|
||||
<label for="userImgEdit"><b>{{ $t("settingsUsersZone.addUserModalUserPhotoLabel") }}</b></label>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="form-control" type="file" accept="image/*" name="userImgEdit" id="userImgEdit" @change="handleFileChange">
|
||||
</div>
|
||||
<div class="col" v-if="userMe.photo_path">
|
||||
<a class="w-100 btn btn-danger" data-bs-dismiss="modal" @click="submitDeleteUserPhoto">{{ $t("usersListComponent.modalEditUserDeleteUserPhotoButton") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- username fields -->
|
||||
<label for="userUsernameEdit"><b>* {{ $t("settingsUsersZone.addUserModalUsernameLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userUsernameEdit" :placeholder='$t("settingsUsersZone.addUserModalUsernamePlaceholder")' maxlength="250" v-model="editUserUsername" required>
|
||||
<!-- name fields -->
|
||||
<label for="userNameEdit"><b>* {{ $t("settingsUsersZone.addUserModalNameLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userNameEdit" :placeholder='$t("settingsUsersZone.addUserModalNamePlaceholder")' maxlength="250" v-model="editUserName" required>
|
||||
<!-- email fields -->
|
||||
<label for="userEmailEdit"><b>* {{ $t("settingsUsersZone.addUserModalEmailLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userEmailEdit" :placeholder='$t("settingsUsersZone.addUserModalEmailPlaceholder")' maxlength="45" v-model="editUserEmail" required>
|
||||
<!-- city fields -->
|
||||
<label for="userCityEdit"><b>{{ $t("settingsUsersZone.addUserModalTownLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userCityEdit" :placeholder='$t("settingsUsersZone.addUserModalTownPlaceholder")' maxlength="45" v-model="editUserTown">
|
||||
<!-- birth date fields -->
|
||||
<label for="userBirthDateEdit"><b>{{ $t("settingsUsersZone.addUserModalBirthdayLabel") }}</b></label>
|
||||
<input class="form-control" type="date" name="userBirthDateEdit" v-model="editUserBirthdate">
|
||||
<!-- gender fields -->
|
||||
<label for="userGenderEdit"><b>* {{ $t("settingsUsersZone.addUserModalGenderLabel") }}</b></label>
|
||||
<select class="form-control" name="userGenderEdit" v-model="editUserGender" required>
|
||||
<option value="1">{{ $t("settingsUsersZone.addUserModalGenderOption1") }}</option>
|
||||
<option value="2">{{ $t("settingsUsersZone.addUserModalGenderOption2") }}</option>
|
||||
</select>
|
||||
<!-- preferred language fields -->
|
||||
<label for="userPreferredLanguageEdit"><b>* {{ $t("settingsUsersZone.addUserModalUserPreferedLanguageLabel") }}</b></label>
|
||||
<select class="form-control" name="userPreferredLanguageEdit" v-model="editUserPreferredLanguage" required>
|
||||
<option value="en">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption1") }}</option>
|
||||
</select>
|
||||
<p>* {{ $t("generalItens.requiredField") }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
|
||||
<button type="submit" class="btn btn-success" name="userEdit" data-bs-dismiss="modal">{{ $t("usersListComponent.modalEditUserTitle") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2>{{ userMe.name }}</h2>
|
||||
<p><b>{{ $t("settingsUsersZone.addUserModalUsernameLabel") }}: </b>{{ userMe.username }}</p>
|
||||
<p><b>{{ $t("settingsUsersZone.addUserModalEmailLabel") }}: </b>{{ userMe.email }}</p>
|
||||
<p>
|
||||
<b>{{ $t("settingsUsersZone.addUserModalTownLabel") }}: </b>
|
||||
<span v-if="userMe.birthdate">{{ userMe.birthdate }}</span>
|
||||
<span v-else>N/A</span>
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ $t("settingsUsersZone.addUserModalBirthdayLabel") }}: </b>
|
||||
<span v-if="userMe.city">{{ userMe.city }}</span>
|
||||
<span v-else>N/A</span>
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ $t("settingsUsersZone.addUserModalGenderLabel") }}: </b>
|
||||
<span v-if="userMe.gender == 1">{{ $t("settingsUsersZone.addUserModalGenderOption1") }}</span>
|
||||
<span v-else>{{ $t("settingsUsersZone.addUserModalGenderOption2") }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ $t("settingsUsersZone.addUserModalUserPreferedLanguageLabel") }}: </b>
|
||||
<span v-if="userMe.preferred_language == 'en'">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption1") }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ $t("settingsUsersZone.addUserModalUserTypeLabel") }}: </b>
|
||||
<span v-if="userMe.access_type == 1">{{ $t("settingsUsersZone.addUserModalUserTypeOption1") }}</span>
|
||||
<span v-else>{{ $t("settingsUsersZone.addUserModalUserTypeOption2") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// Importing the services
|
||||
import { users } from '@/services/user';
|
||||
// Importing the stores
|
||||
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
|
||||
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')));
|
||||
const { t } = useI18n();
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
const successAlertStore = useSuccessAlertStore();
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const editUserPhotoFile = ref(null);
|
||||
const editUserUsername = ref(userMe.value.username);
|
||||
const editUserName = ref(userMe.value.name);
|
||||
const editUserEmail = ref(userMe.value.email);
|
||||
const editUserTown = ref(userMe.value.city);
|
||||
const editUserBirthdate = ref(userMe.value.birthdate);
|
||||
const editUserGender = ref(userMe.value.gender);
|
||||
const editUserPreferredLanguage = ref(userMe.value.preferred_language);
|
||||
const editUserAccessType = ref(userMe.value.access_type);
|
||||
|
||||
function resetMessageValues() {
|
||||
successMessage.value = '';
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
errorMessage.value = '';
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
|
||||
async function handleFileChange(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
editUserPhotoFile.value = event.target.files[0];
|
||||
} else {
|
||||
editUserPhotoFile.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitEditUserForm() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
const data = {
|
||||
id: userMe.value.id,
|
||||
username: editUserUsername.value,
|
||||
name: editUserName.value,
|
||||
email: editUserEmail.value,
|
||||
city: editUserTown.value,
|
||||
birthdate: editUserBirthdate.value,
|
||||
gender: editUserGender.value,
|
||||
preferred_language: editUserPreferredLanguage.value,
|
||||
access_type: editUserAccessType.value,
|
||||
photo_path: null,
|
||||
photo_path_aux: null,
|
||||
is_active: 1,
|
||||
};
|
||||
|
||||
await users.editUser(data);
|
||||
|
||||
// If there is a photo, upload it and get the photo url.
|
||||
if (editUserPhotoFile.value) {
|
||||
try {
|
||||
userMe.value.photo_path = await users.uploadImage(editUserPhotoFile.value, userMe.value.id);
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
userMe.value.username = editUserUsername.value;
|
||||
userMe.value.name = editUserName.value;
|
||||
userMe.value.email = editUserEmail.value;
|
||||
userMe.value.city = editUserTown.value;
|
||||
userMe.value.birthdate = editUserBirthdate.value;
|
||||
userMe.value.city = editUserTown.value;
|
||||
userMe.value.birthdate = editUserBirthdate.value;
|
||||
userMe.value.gender = editUserGender.value;
|
||||
userMe.value.preferred_language = editUserPreferredLanguage.value;
|
||||
|
||||
localStorage.setItem('userMe', JSON.stringify(userMe.value));
|
||||
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('usersListComponent.userEditSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('usersListComponent.userEditErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDeleteUserPhoto() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
await users.deleteUserPhoto(userMe.value.id);
|
||||
userMe.value.photo_path = null;
|
||||
|
||||
localStorage.setItem('userMe', JSON.stringify(userMe.value));
|
||||
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('usersListComponent.userPhotoDeleteSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
errorMessage.value = t('usersListComponent.userPhotoDeleteErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userMe,
|
||||
t,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
editUserUsername,
|
||||
editUserName,
|
||||
editUserEmail,
|
||||
editUserTown,
|
||||
editUserBirthdate,
|
||||
editUserGender,
|
||||
editUserPreferredLanguage,
|
||||
editUserAccessType,
|
||||
submitEditUserForm,
|
||||
submitDeleteUserPhoto,
|
||||
handleFileChange,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
341
frontend/src/components/Settings/SettingsUsersZone.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<ErrorToastComponent v-if="errorMessage" />
|
||||
<SuccessToastComponent v-if="successMessage" />
|
||||
|
||||
<div class="row row-gap-3">
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<!-- add user button -->
|
||||
<a class="w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addUserModal">{{ $t("settingsUsersZone.buttonAddUser") }}</a>
|
||||
|
||||
<!-- Modal add user -->
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addUserModal">{{ $t("settingsUsersZone.buttonAddUser") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form @submit.prevent="submitAddUserForm">
|
||||
<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" @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>
|
||||
<!-- name fields -->
|
||||
<label for="userNameAdd"><b>* {{ $t("settingsUsersZone.addUserModalNameLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userNameAdd" :placeholder='$t("settingsUsersZone.addUserModalNamePlaceholder")' maxlength="45" v-model="newUserName" required>
|
||||
<!-- email fields -->
|
||||
<label for="userEmailAdd"><b>* {{ $t("settingsUsersZone.addUserModalEmailLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userEmailAdd" :placeholder='$t("settingsUsersZone.addUserModalEmailPlaceholder")' maxlength="45" v-model="newUserEmail" required>
|
||||
<!-- password fields -->
|
||||
<label for="passUserAdd"><b>* {{ $t("settingsUsersZone.addUserModalPasswordLabel") }}</b></label>
|
||||
<input class="form-control" :class="{ 'is-invalid': !isPasswordValid }" type="password" id="validationPassword" aria-describedby="validationPasswordFeedback" name="passUserAdd" :placeholder='$t("settingsUsersZone.addUserModalPasswordPlaceholder")' v-model="newUserPassword" required>
|
||||
<div id="validationPasswordFeedback" class="invalid-feedback" v-if="!isPasswordValid">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordFeedbackLabel") }}
|
||||
</div>
|
||||
<!-- city fields -->
|
||||
<label for="userCityAdd"><b>{{ $t("settingsUsersZone.addUserModalTownLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userCityAdd" :placeholder='$t("settingsUsersZone.addUserModalTownPlaceholder")' maxlength="45" v-model="newUserTown">
|
||||
<!-- birth date fields -->
|
||||
<label for="userBirthDateAdd"><b>{{ $t("settingsUsersZone.addUserModalBirthdayLabel") }}</b></label>
|
||||
<input class="form-control" type="date" name="userBirthDateAdd" v-model="newUserBirthDate">
|
||||
<!-- gender fields -->
|
||||
<label for="userGenderAdd"><b>* {{ $t("settingsUsersZone.addUserModalGenderLabel") }}</b></label>
|
||||
<select class="form-control" name="userGenderAdd" v-model="newUserGender" required>
|
||||
<option value="1">{{ $t("settingsUsersZone.addUserModalGenderOption1") }}</option>
|
||||
<option value="2">{{ $t("settingsUsersZone.addUserModalGenderOption2") }}</option>
|
||||
</select>
|
||||
<!-- preferred language fields -->
|
||||
<label for="userPreferredLanguageAdd"><b>* {{ $t("settingsUsersZone.addUserModalUserPreferedLanguageLabel") }}</b></label>
|
||||
<select class="form-control" name="userPreferredLanguageAdd" v-model="newUserPreferredLanguage" required>
|
||||
<option value="en">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption1") }}</option>
|
||||
</select>
|
||||
<!-- access type fields -->
|
||||
<label for="userAccessTypeAdd"><b>* {{ $t("settingsUsersZone.addUserModalUserTypeLabel") }}</b></label>
|
||||
<select class="form-control" name="userAccessTypeAdd" v-model="newUserAccessType" required>
|
||||
<option value="1">{{ $t("settingsUsersZone.addUserModalUserTypeOption1") }}</option>
|
||||
<option value="2">{{ $t("settingsUsersZone.addUserModalUserTypeOption2") }}</option>
|
||||
</select>
|
||||
<p>* {{ $t("generalItens.requiredField") }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
|
||||
<button type="submit" class="btn btn-success" :disabled="!isPasswordValid" data-bs-dismiss="modal">{{ $t("settingsUsersZone.buttonAddUser") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- form to search-->
|
||||
<div class="col">
|
||||
<form class="d-flex">
|
||||
<input class="form-control me-2" type="text" name="userUsername" :placeholder='$t("settingsUsersZone.addUserModalUsernameLabel")' v-model="searchUsername" required>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LoadingComponent v-if="isLoading" />
|
||||
<div v-else>
|
||||
<!-- Checking if usersArray is loaded and has length -->
|
||||
<div v-if="usersArray && usersArray.length">
|
||||
<!-- title zone -->
|
||||
<br>
|
||||
<p>{{ $t("settingsUsersZone.labelNumberOfUsers1") }}{{ usersNumber }}{{ $t("settingsUsersZone.labelNumberOfUsers2") }}{{ usersArray.length }}{{ $t("settingsUsersZone.labelNumberOfUsers3") }}</p>
|
||||
|
||||
<!-- list zone -->
|
||||
<ul class="list-group list-group-flush" v-for="user in usersArray" :key="user.id" :user="user">
|
||||
<UsersListConponent :user="user" @userDeleted="updateUserList" />
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Displaying a message or component when there are no activities -->
|
||||
<div v-else>
|
||||
<br>
|
||||
<NoItemsFoundComponent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// Importing the stores
|
||||
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
// Importing the components
|
||||
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
|
||||
import SuccessToastComponent from '@/components/Toasts/SuccessToastComponent.vue';
|
||||
import LoadingComponent from '@/components/LoadingComponent.vue';
|
||||
import NoItemsFoundComponent from '@/components/NoItemsFoundComponents.vue';
|
||||
import UsersListConponent from '@/components/Settings/SettingsUsersZone/UsersListComponent.vue';
|
||||
// Importing the services
|
||||
import { users } from '@/services/user';
|
||||
// Importing the crypto-js
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingComponent,
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
NoItemsFoundComponent,
|
||||
UsersListConponent
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
const successAlertStore = useSuccessAlertStore();
|
||||
const isLoading = ref(true);
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const newUserPhotoFile = ref(null);
|
||||
const newUserUsername = ref('');
|
||||
const newUserName = ref('');
|
||||
const newUserEmail = ref('');
|
||||
const newUserPassword = ref('');
|
||||
const isPasswordValid = computed(() => {
|
||||
const regex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/;
|
||||
return regex.test(newUserPassword.value);
|
||||
});
|
||||
const newUserTown = ref(null);
|
||||
const newUserBirthDate = ref(null);
|
||||
const newUserGender = ref(1);
|
||||
const newUserPreferredLanguage = ref('en');
|
||||
const newUserAccessType = ref(1);
|
||||
const usersArray = ref([]);
|
||||
const usersNumber = ref(0);
|
||||
const hasMoreUsers = ref(true);
|
||||
const pageNumber = ref(1);
|
||||
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 fetchMoreUsers() {
|
||||
// If the component is already loading or there are no more gears to fetch, return.
|
||||
if (isLoading.value || !hasMoreUsers.value) return;
|
||||
|
||||
// Add 1 to the page number.
|
||||
pageNumber.value++;
|
||||
try {
|
||||
// Fetch the users with pagination.
|
||||
const newUsers = await users.getUsersWithPagination(pageNumber.value, numRecords);
|
||||
// Add the new users to the users array.
|
||||
Array.prototype.push.apply(usersArray.value, newUsers);
|
||||
|
||||
// If there are no more gears to fetch, set userHasMoreGears to false.
|
||||
if ((pageNumber.value * numRecords) >= usersNumber.value) {
|
||||
hasMoreUsers.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function performSearch() {
|
||||
// If the search nickname is empty, reset the list to initial state.
|
||||
if (!searchUsername.value) {
|
||||
// Reset the list to the initial state when search text is cleared
|
||||
pageNumber.value = 1;
|
||||
hasMoreUsers.value = true;
|
||||
|
||||
await fetchInitialUsers();
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Fetch the users based on the search username.
|
||||
usersArray.value = await users.getUserByUsername(searchUsername.value);
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
// If the user has reached the bottom of the window, fetch more gears.
|
||||
const bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
|
||||
|
||||
if (bottomOfWindow) {
|
||||
fetchMoreUsers();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAddUserForm() {
|
||||
try {
|
||||
if (isPasswordValid.value) {
|
||||
|
||||
// Create the gear data object.
|
||||
const data = {
|
||||
name: newUserName.value,
|
||||
username: newUserUsername.value,
|
||||
email: newUserEmail.value,
|
||||
city: newUserTown.value,
|
||||
birthdate: newUserBirthDate.value,
|
||||
preferred_language: newUserPreferredLanguage.value,
|
||||
gender: newUserGender.value,
|
||||
access_type: newUserAccessType.value,
|
||||
photo_path: null,
|
||||
photo_path_aux: null,
|
||||
is_active: 1,
|
||||
password: CryptoJS.SHA256(newUserPassword.value).toString(CryptoJS.enc.Hex),
|
||||
};
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
await users.uploadImage(newUserPhotoFile.value, createdUserId);
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the created gear and add it to the userGears array.
|
||||
const newUser = await users.getUserById(createdUserId);
|
||||
usersArray.value.unshift(newUser);
|
||||
|
||||
// Increment the number of users.
|
||||
usersNumber.value++;
|
||||
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('settingsUsersZone.successUserAdded');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
}
|
||||
} catch(error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchInitialUsers() {
|
||||
try {
|
||||
// Fetch the users with pagination.
|
||||
usersArray.value = await users.getUsersWithPagination(pageNumber.value, numRecords);
|
||||
// Get the total number of user gears.
|
||||
usersNumber.value = await users.getUsersNumber();
|
||||
|
||||
// If there are no more users to fetch, set hasMoreUsers to false.
|
||||
if ((pageNumber.value * numRecords) >= usersNumber.value) {
|
||||
hasMoreUsers.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserList(userDeletedId) {
|
||||
usersArray.value = usersArray.value.filter(user => user.id !== userDeletedId);
|
||||
usersNumber.value--;
|
||||
|
||||
successMessage.value = t('usersListComponent.userDeleteSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Add the event listener for scroll event.
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
// Fetch the initial users.
|
||||
await fetchInitialUsers();
|
||||
|
||||
// Set the isLoading to false.
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Remove the event listener for scroll event.
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
watch(searchUsername, performSearch, { immediate: false });
|
||||
|
||||
return {
|
||||
t,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
newUserPhotoFile,
|
||||
newUserUsername,
|
||||
newUserName,
|
||||
newUserEmail,
|
||||
newUserPassword,
|
||||
isPasswordValid,
|
||||
newUserTown,
|
||||
newUserBirthDate,
|
||||
newUserGender,
|
||||
newUserPreferredLanguage,
|
||||
newUserAccessType,
|
||||
submitAddUserForm,
|
||||
handleFileChange,
|
||||
usersNumber,
|
||||
usersArray,
|
||||
searchUsername,
|
||||
updateUserList,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<ErrorToastComponent v-if="errorMessage" />
|
||||
<SuccessToastComponent v-if="successMessage" />
|
||||
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<UserAvatarComponent :userProp="userProp" :width=55 :height=55 />
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
{{ userProp.username }}
|
||||
</div>
|
||||
<b>{{ $t("usersListComponent.userListAccessTypeLabel") }}</b>
|
||||
<span v-if="userProp.access_type == 1">{{ $t("usersListComponent.userListAccessTypeOption1") }}</span>
|
||||
<span v-else>{{ $t("usersListComponent.userListAccessTypeOption2") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-success-subtle border border-success-subtle text-success-emphasis rounded-pill align-middle" v-if="user.is_active == 1">{{ $t("usersListComponent.userListUserIsActiveBadge") }}</span>
|
||||
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis rounded-pill align-middle" v-else>{{ $t("usersListComponent.userListUserIsInactiveBadge") }}</span>
|
||||
|
||||
<!-- change user password button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#editUserPasswordModal${userProp.id}`"><font-awesome-icon :icon="['fas', 'fa-key']" /></a>
|
||||
|
||||
<!-- change user password Modal -->
|
||||
<div class="modal fade" :id="`editUserPasswordModal${userProp.id}`" tabindex="-1" :aria-labelledby="`editUserPasswordModal${userProp.id}`" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" :id="`editUserPasswordModal${userProp.id}`">{{ $t("usersListComponent.modalChangeUserPasswordTitle") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form @submit.prevent="submitChangeUserPasswordForm">
|
||||
<div class="modal-body">
|
||||
<SettingsPasswordRequirementsComponent />
|
||||
|
||||
<p>{{ $t("usersListComponent.modalChangeUserPasswordBodyLabel") }}<b>{{ userProp.username }}</b></p>
|
||||
|
||||
<!-- password fields -->
|
||||
<label for="validationNewPassword"><b>* {{ $t("usersListComponent.modalChangeUserPasswordPasswordLabel") }}</b></label>
|
||||
<input class="form-control" :class="{ 'is-invalid': !isNewPasswordValid || !isPasswordMatch }" type="password" id="validationNewPassword" aria-describedby="validationNewPasswordFeedback" :placeholder='$t("usersListComponent.modalChangeUserPasswordPasswordLabel")' v-model="newPassword" required>
|
||||
<div id="validationNewPasswordFeedback" class="invalid-feedback" v-if="!isNewPasswordValid">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordFeedbackLabel") }}
|
||||
</div>
|
||||
<div id="validationNewPasswordFeedback" class="invalid-feedback" v-if="!isPasswordMatch">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordPasswordsDoNotMatchFeedbackLabel") }}
|
||||
</div>
|
||||
<!-- repeat password fields -->
|
||||
|
||||
<label class="mt-1" for="validationNewPasswordRepeat"><b>* {{ $t("usersListComponent.modalChangeUserPasswordPasswordConfirmationLabel") }}</b></label>
|
||||
<input class="form-control" :class="{ 'is-invalid': !isNewPasswordRepeatValid || !isPasswordMatch }" type="password" id="validationNewPasswordRepeat" aria-describedby="validationNewPasswordRepeatFeedback" :placeholder='$t("usersListComponent.modalChangeUserPasswordPasswordConfirmationLabel")' v-model="newPasswordRepeat" required>
|
||||
<div id="validationNewPasswordRepeatFeedback" class="invalid-feedback" v-if="!isNewPasswordRepeatValid">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordFeedbackLabel") }}
|
||||
</div>
|
||||
<div id="validationNewPasswordRepeatFeedback" class="invalid-feedback" v-if="!isPasswordMatch">
|
||||
{{ $t("usersListComponent.modalChangeUserPasswordPasswordsDoNotMatchFeedbackLabel") }}
|
||||
</div>
|
||||
|
||||
<p>* {{ $t("generalItens.requiredField") }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
|
||||
<button type="submit" class="btn btn-success" :disabled="!isNewPasswordValid || !isNewPasswordRepeatValid || !isPasswordMatch" name="editUserPasswordAdmin" data-bs-dismiss="modal">{{ $t("usersListComponent.modalChangeUserPasswordTitle") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- edit user button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#editUserModal${userProp.id}`"><font-awesome-icon :icon="['fas', 'fa-pen-to-square']" /></a>
|
||||
|
||||
<!-- Modal edit user -->
|
||||
<div class="modal fade" :id="`editUserModal${userProp.id}`" tabindex="-1" :aria-labelledby="`editUserModal${userProp.id}`" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" :id="`editUserModal${userProp.id}`">{{ $t("usersListComponent.modalEditUserTitle") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form @submit.prevent="submitEditUserForm">
|
||||
<div class="modal-body">
|
||||
<label for="userImgEdit"><b>{{ $t("settingsUsersZone.addUserModalUserPhotoLabel") }}</b></label>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="form-control" type="file" accept="image/*" name="userImgEdit" id="userImgEdit" @change="handleFileChange">
|
||||
</div>
|
||||
<div class="col" v-if="userProp.photo_path">
|
||||
<a class="w-100 btn btn-danger" @click="submitDeleteUserPhoto" data-bs-dismiss="modal">{{ $t("usersListComponent.modalEditUserDeleteUserPhotoButton") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- username fields -->
|
||||
<label for="userUsernameEdit"><b>* {{ $t("settingsUsersZone.addUserModalUsernameLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userUsernameEdit" :placeholder='$t("settingsUsersZone.addUserModalUsernamePlaceholder")' maxlength="250" v-model="editUserUsername" required>
|
||||
<!-- name fields -->
|
||||
<label for="userNameEdit"><b>* {{ $t("settingsUsersZone.addUserModalNameLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userNameEdit" :placeholder='$t("settingsUsersZone.addUserModalNamePlaceholder")' maxlength="250" v-model="editUserName" required>
|
||||
<!-- email fields -->
|
||||
<label for="userEmailEdit"><b>* {{ $t("settingsUsersZone.addUserModalEmailLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userEmailEdit" :placeholder='$t("settingsUsersZone.addUserModalEmailPlaceholder")' maxlength="45" v-model="editUserEmail" required>
|
||||
<!-- city fields -->
|
||||
<label for="userCityEdit"><b>{{ $t("settingsUsersZone.addUserModalTownLabel") }}</b></label>
|
||||
<input class="form-control" type="text" name="userCityEdit" :placeholder='$t("settingsUsersZone.addUserModalTownPlaceholder")' maxlength="45" v-model="editUserTown">
|
||||
<!-- birth date fields -->
|
||||
<label for="userBirthDateEdit"><b>{{ $t("settingsUsersZone.addUserModalBirthdayLabel") }}</b></label>
|
||||
<input class="form-control" type="date" name="userBirthDateEdit" v-model="editUserBirthdate">
|
||||
<!-- gender fields -->
|
||||
<label for="userGenderEdit"><b>* {{ $t("settingsUsersZone.addUserModalGenderLabel") }}</b></label>
|
||||
<select class="form-control" name="userGenderEdit" v-model="editUserGender" required>
|
||||
<option value="1">{{ $t("settingsUsersZone.addUserModalGenderOption1") }}</option>
|
||||
<option value="2">{{ $t("settingsUsersZone.addUserModalGenderOption2") }}</option>
|
||||
</select>
|
||||
<!-- preferred language fields -->
|
||||
<label for="userPreferredLanguageEdit"><b>* {{ $t("settingsUsersZone.addUserModalUserPreferedLanguageLabel") }}</b></label>
|
||||
<select class="form-control" name="userPreferredLanguageEdit" v-model="editUserPreferredLanguage" required>
|
||||
<option value="en">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption1") }}</option>
|
||||
</select>
|
||||
<!-- access type fields -->
|
||||
<label for="userTypeEdit"><b>* {{ $t("settingsUsersZone.addUserModalUserTypeLabel") }}</b></label>
|
||||
<select class="form-control" name="userTypeEdit" v-model="editUserAccessType" required>
|
||||
<option value="1">{{ $t("settingsUsersZone.addUserModalUserTypeOption1") }}</option>
|
||||
<option value="2">{{ $t("settingsUsersZone.addUserModalUserTypeOption2") }}</option>
|
||||
</select>
|
||||
<!-- user is_active fields -->
|
||||
<label for="userIsActiveEdit"><b>* {{ $t("usersListComponent.modalEditUserTitle") }}</b></label>
|
||||
<select class="form-control" name="userIsActiveEdit" v-model="editUserIsActive" required>
|
||||
<option value="1">{{ $t("usersListComponent.modalEditUserIsUserActiveOption1") }}</option>
|
||||
<option value="2">{{ $t("usersListComponent.modalEditUserIsUserActiveOption2") }}</option>
|
||||
</select>
|
||||
<p>* {{ $t("generalItens.requiredField") }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
|
||||
<button type="submit" class="btn btn-success" name="userEdit" data-bs-dismiss="modal">{{ $t("usersListComponent.modalEditUserTitle") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- delete user button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal" :data-bs-target="`#deleteUserModal${userProp.id}`" v-if="loggedUserId != userProp.id"><font-awesome-icon :icon="['fas', 'fa-trash-can']" /></a>
|
||||
|
||||
<!-- delete user modal -->
|
||||
<div class="modal fade" :id="`deleteUserModal${userProp.id}`" tabindex="-1" :aria-labelledby="`deleteUserModal${userProp.id}`" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" :id="`deleteUserModal${userProp.id}`">{{ $t("usersListComponent.modalDeleteUserTitle") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span>{{ $t("usersListComponent.modalDeleteUserBody") }}<b>{{ userProp.username }}</b>?</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
|
||||
<a type="button" @click="submitDeleteUser" class="btn btn-danger" data-bs-dismiss="modal">{{ $t("usersListComponent.modalDeleteUserTitle") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// Importing the services
|
||||
import { users } from '@/services/user';
|
||||
// Importing the stores
|
||||
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
// Importing the components
|
||||
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';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorToastComponent,
|
||||
SuccessToastComponent,
|
||||
SettingsPasswordRequirementsComponent,
|
||||
UserAvatarComponent,
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: ['userDeleted'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const loggedUserId = JSON.parse(localStorage.getItem('userMe')).id;
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
const successAlertStore = useSuccessAlertStore();
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const userProp = ref(props.user);
|
||||
const newPassword = ref('');
|
||||
const newPasswordRepeat = ref('');
|
||||
const isNewPasswordValid = computed(() => {
|
||||
const regex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/;
|
||||
return regex.test(newPassword.value);
|
||||
});
|
||||
const isNewPasswordRepeatValid = computed(() => {
|
||||
const regex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/;
|
||||
return regex.test(newPasswordRepeat.value);
|
||||
});
|
||||
const isPasswordMatch = computed(() => newPassword.value === newPasswordRepeat.value);
|
||||
const editUserPhotoFile = ref(null);
|
||||
const editUserUsername = ref(userProp.value.username);
|
||||
const editUserName = ref(userProp.value.name);
|
||||
const editUserEmail = ref(userProp.value.email);
|
||||
const editUserTown = ref(userProp.value.city);
|
||||
const editUserBirthdate = ref(userProp.value.birthdate);
|
||||
const editUserGender = ref(userProp.value.gender);
|
||||
const editUserPreferredLanguage = ref(userProp.value.preferred_language);
|
||||
const editUserAccessType = ref(userProp.value.access_type);
|
||||
const editUserIsActive = ref(userProp.value.is_active);
|
||||
|
||||
function resetMessageValues() {
|
||||
successMessage.value = '';
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
errorMessage.value = '';
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
|
||||
async function handleFileChange(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
editUserPhotoFile.value = event.target.files[0];
|
||||
} else {
|
||||
editUserPhotoFile.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitChangeUserPasswordForm() {
|
||||
resetMessageValues();
|
||||
|
||||
try{
|
||||
if (isNewPasswordValid.value && isNewPasswordRepeatValid.value && isPasswordMatch.value) {
|
||||
const data = {
|
||||
id: userProp.value.id,
|
||||
password: CryptoJS.SHA256(newPassword.value).toString(CryptoJS.enc.Hex),
|
||||
};
|
||||
await users.editUserPassword(data);
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('usersListComponent.userChangePasswordSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('usersListComponent.userChangePasswordErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitEditUserForm() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
const data = {
|
||||
id: userProp.value.id,
|
||||
username: editUserUsername.value,
|
||||
name: editUserName.value,
|
||||
email: editUserEmail.value,
|
||||
city: editUserTown.value,
|
||||
birthdate: editUserBirthdate.value,
|
||||
gender: editUserGender.value,
|
||||
preferred_language: editUserPreferredLanguage.value,
|
||||
access_type: editUserAccessType.value,
|
||||
photo_path: null,
|
||||
photo_path_aux: null,
|
||||
is_active: editUserIsActive.value,
|
||||
};
|
||||
|
||||
await users.editUser(data);
|
||||
|
||||
// If there is a photo, upload it and get the photo url.
|
||||
if (editUserPhotoFile.value) {
|
||||
try {
|
||||
userProp.value.photo_path = await users.uploadImage(editUserPhotoFile.value, userProp.value.id);
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
userProp.value.username = editUserUsername.value;
|
||||
userProp.value.name = editUserName.value;
|
||||
userProp.value.email = editUserEmail.value;
|
||||
userProp.value.city = editUserTown.value;
|
||||
userProp.value.birthdate = editUserBirthdate.value;
|
||||
userProp.value.city = editUserTown.value;
|
||||
userProp.value.birthdate = editUserBirthdate.value;
|
||||
userProp.value.gender = editUserGender.value;
|
||||
userProp.value.preferred_language = editUserPreferredLanguage.value;
|
||||
userProp.value.access_type = editUserAccessType.value;
|
||||
userProp.value.is_active = editUserIsActive.value;
|
||||
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('usersListComponent.userEditSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('usersListComponent.userEditErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDeleteUser() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
await users.deleteUser(userProp.value.id);
|
||||
|
||||
emit('userDeleted', userProp.value.id);
|
||||
|
||||
// Set the success message and show the success alert.
|
||||
//successMessage.value = t('usersListComponent.userDeleteSuccessMessage');
|
||||
//successAlertStore.setAlertMessage(successMessage.value);
|
||||
//successAlertStore.setClosableState(true);
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
errorMessage.value = t('usersListComponent.userDeleteErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDeleteUserPhoto() {
|
||||
resetMessageValues();
|
||||
|
||||
try {
|
||||
await users.deleteUserPhoto(userProp.value.id);
|
||||
userProp.value.photo_path = null;
|
||||
|
||||
// Set the success message and show the success alert.
|
||||
successMessage.value = t('usersListComponent.userPhotoDeleteSuccessMessage');
|
||||
successAlertStore.setAlertMessage(successMessage.value);
|
||||
successAlertStore.setClosableState(true);
|
||||
} catch (error) {
|
||||
// Set the error message
|
||||
errorMessage.value = t('usersListComponent.userPhotoDeleteErrorMessage') + " - " + error.toString();
|
||||
errorAlertStore.setAlertMessage(errorMessage.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
loggedUserId,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
userProp,
|
||||
newPassword,
|
||||
newPasswordRepeat,
|
||||
isNewPasswordValid,
|
||||
isNewPasswordRepeatValid,
|
||||
isPasswordMatch,
|
||||
submitChangeUserPasswordForm,
|
||||
editUserUsername,
|
||||
editUserName,
|
||||
editUserEmail,
|
||||
editUserTown,
|
||||
editUserBirthdate,
|
||||
editUserGender,
|
||||
editUserPreferredLanguage,
|
||||
editUserAccessType,
|
||||
editUserIsActive,
|
||||
submitEditUserForm,
|
||||
handleFileChange,
|
||||
submitDeleteUser,
|
||||
submitDeleteUserPhoto,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
53
frontend/src/components/Toasts/ErrorToastComponent.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div ref="toastElement" class="toast align-items-center" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body text-danger-emphasis">
|
||||
<font-awesome-icon :icon="['fas', 'fa-circle-exclamation']" />
|
||||
<span class="ms-1">{{ errorMessage }}</span>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" v-if="closable"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
|
||||
import BootstrapToast from 'bootstrap/js/dist/toast';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const errorAlertStore = useErrorAlertStore();
|
||||
const toastElement = ref(null);
|
||||
|
||||
// Access both stats directly from the store
|
||||
const errorMessage = computed(() => errorAlertStore.message);
|
||||
const closable = computed(() => errorAlertStore.closable);
|
||||
|
||||
let toastInstance = null;
|
||||
|
||||
// Initialize and show toast when component is mounted
|
||||
onMounted(() => {
|
||||
if (toastElement.value) {
|
||||
toastInstance = new BootstrapToast(toastElement.value);
|
||||
toastInstance.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Reactively show the toast when errorMessage changes
|
||||
watch(errorMessage, (newVal) => {
|
||||
if (newVal && toastInstance) {
|
||||
toastInstance.show();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
toastElement,
|
||||
errorMessage,
|
||||
closable,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||