Skip to content

Commit

Permalink
Merge pull request #11 from Cleanwalk-org-QNSCNT/feat/discover-view
Browse files Browse the repository at this point in the history
Feature: add mobile V2 version
ArthurFrin authored Jul 25, 2024
2 parents 89ce1c7 + 733f809 commit 0238285
Showing 98 changed files with 5,441 additions and 1,457 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Deploy to VPS

on:
push:
branches:
- release # Déclencher le déploiement uniquement sur la branche release

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up SSH
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SECRET_KEY }} # Utilisation de la clé SSH

- name: Deploy to VPS
env:
VPS_IP: ${{ secrets.VPS_IP }}
run: |
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$VPS_IP '
cd /home/cleanwalk-org-v2 &&
git pull origin release &&
docker compose -f docker-compose.prod.yml down &&
docker compose -f docker-compose.prod.yml up -d --build
'
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -35,3 +35,8 @@ lerna-debug.log*
*.njsproj
*.sln
*.sw?

/uploads

/data-nginx
/letsencrypt
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -51,6 +51,49 @@ To shutdown all stack or just a specific one:
docker compose down
```

### Production
## Production

### config nginx proxy manager

**first login**: connect to @server_ip:81
- username: [email protected]
- password: changeme **! change admin info**

**proxy hosts**
Go to Hosts > Proxy Hosts and click on Add Proxy Host.

**frontend**
- Domain Names: yourdomain.example, www.yourdomain.example
- Scheme: http
- Forward Hostname / IP: frontend
- Forward Port: 80

**API**
- Domain Names: api.yourdomain.example
- Scheme: http
- Forward Hostname / IP: api
- Forward Port: 5000

**uploads**
- Domain Names : uploads.yourdomain.example
- Scheme : http
- Forward Hostname / IP : nginx-proxy-manager
- Forward Port : 81

in advanced add:
```
location / {
alias /var/www/uploads/;
autoindex on;
}
```

**SSL Config**
- check Force SSL
- select Request a new SSL certificate
- add email and save





Use Portainer for easier management. To install it, launch the script named "install-portainer.sh" and to access it you can check the service on port 9000.
4 changes: 3 additions & 1 deletion api/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DATABASE_URI=mysql+mysqlconnector://root:root@localhost:3306/cleanwalk_db
DATABASE_URI=mysql+pymysql://user:password@localhost/cleanwalk_db
API_KEY=1234567890
JWT_SECRET_KEY=098765433
UPLOAD_FOLDER=uploads
UPLOADS_URL=https://uploads.cleanwalk.org
23 changes: 0 additions & 23 deletions api/Dockerfile

This file was deleted.

20 changes: 20 additions & 0 deletions api/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Utilisez une image Python comme image de base
FROM python:3.8-alpine

# Définissez le répertoire de travail dans le conteneur
WORKDIR /app

# Copiez les fichiers de dépendances dans le conteneur
COPY requirements.txt .

# Installez les dépendances
RUN pip install --no-cache-dir -r requirements.txt

# Copiez le reste du code dans le conteneur
COPY . .

# Exposez le port sur lequel l'API s'exécute
EXPOSE 5000

# Démarrez l'API
CMD ["python3", "app.py"]
23 changes: 23 additions & 0 deletions api/Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Utiliser une image Python comme image de base
FROM python:3.8-alpine

# Définir le répertoire de travail dans le conteneur
WORKDIR /app

# Copier les fichiers de dépendances dans le conteneur
COPY requirements.txt .

# Installer les dépendances
RUN pip install --no-cache-dir -r requirements.txt

# Copier le reste du code dans le conteneur
COPY . .

# Créer le dossier pour les uploads et définir les permissions
RUN mkdir -p /app/uploads && chmod -R 755 /app/uploads

# Exposer le port sur lequel l'API s'exécute
EXPOSE 5000

# Démarrer l'API avec Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app"]
40 changes: 3 additions & 37 deletions api/app.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,7 @@
from flask import Flask
from flask_jwt_extended import JWTManager
from app.models import db
from dotenv import load_dotenv
import os
from datetime import timedelta

# load environement variables from .env file
load_dotenv()

ACCESS_EXPIRES = timedelta(minutes=1)

app = Flask(__name__)

#configure the SQLAlchemy database using environment variables
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URI')
app.config['API_KEY'] = os.getenv('API_KEY')
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = ACCESS_EXPIRES

#Init the JWTManager extension
jwt = JWTManager(app)

# Init the SQLAlchemy database
db.init_app(app)

from app.routes.users import users_bp
from app.routes.articles import articles_bp
from app.routes.cleanwalks import cleanwalks_bp
from app.routes.cities import cities_bp
from app.routes.admin import admin_bp

app.register_blueprint(users_bp , url_prefix='/users')
app.register_blueprint(articles_bp , url_prefix='/articles')
app.register_blueprint(cleanwalks_bp , url_prefix='/cleanwalks')
app.register_blueprint(cities_bp , url_prefix='/cities')
app.register_blueprint(admin_bp , url_prefix='/admin')
# app.py
from app import create_app

app = create_app()

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
46 changes: 46 additions & 0 deletions api/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# app/__init__.py
from flask import Flask
from flask_jwt_extended import JWTManager
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from dotenv import load_dotenv
import os
from datetime import timedelta

# Charger les variables d'environnement
load_dotenv()

# Initialiser l'extension SQLAlchemy
db = SQLAlchemy()

def create_app():
app = Flask(__name__)
CORS(app)
# Configurer l'application
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URI')
app.config['API_KEY'] = os.getenv('API_KEY')
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(days=30)
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER')
app.config['UPLOADS_URL'] = os.getenv('UPLOADS_URL')

# Initialiser les extensions
db.init_app(app)
JWTManager(app)

# Enregistrer les blueprints
from app.routes.users import users_bp
from app.routes.articles import articles_bp
from app.routes.cleanwalks import cleanwalks_bp
from app.routes.cities import cities_bp
from app.routes.admin import admin_bp
from app.routes.upload import upload_bp

app.register_blueprint(users_bp, url_prefix='/users')
app.register_blueprint(articles_bp, url_prefix='/articles')
app.register_blueprint(cleanwalks_bp, url_prefix='/cleanwalks')
app.register_blueprint(cities_bp, url_prefix='/cities')
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(upload_bp, url_prefix='/upload')

return app
19 changes: 14 additions & 5 deletions api/app/models.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
from app import db

class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
firstname = db.Column(db.String(255), nullable=False)
lastname = db.Column(db.String(255), nullable=False)
name = db.Column(db.String(255), nullable=False)
email = db.Column(db.String(255), unique=True, index=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
salt = db.Column(db.BINARY(16), nullable=True)
created_at = db.Column(db.TIMESTAMP, nullable=False)
profile_picture = db.Column(db.String(255), nullable=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), nullable=False)

class Organisation(db.Model):
__tablename__ = 'organisations'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
description = db.Column(db.String(255), nullable=True)
web_site = db.Column(db.String(255), nullable=True)
social_media = db.Column(db.JSON, nullable=True)
banner_img = db.Column(db.String(255), nullable=True)

# Relation vers User
user = db.relationship('User', backref=db.backref('organisation', uselist=False))

class Cleanwalk(db.Model):
__tablename__ = 'cleanwalks'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
pos_lat = db.Column(db.Float, nullable=False)
pos_long = db.Column(db.Float, nullable=False)
date_begin = db.Column(db.TIMESTAMP, nullable=False)
img_url = db.Column(db.String(255), nullable=True)
duration = db.Column(db.Integer, nullable=False)
description = db.Column(db.String(255), nullable = False)
address = db.Column(db.String(255), nullable=False)
2 changes: 2 additions & 0 deletions api/app/routes/admin.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@

@admin_bp.before_request
def check_api_key():
if request.method == 'OPTIONS': # Handle preflight requests to enable CORS
return
api_key = request.headers.get('X-API-Key') # Get the api key from the header

# Verify the api key
2 changes: 2 additions & 0 deletions api/app/routes/articles.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@

@articles_bp.before_request
def check_api_key():
if request.method == 'OPTIONS': # Handle preflight requests to enable CORS
return
api_key = request.headers.get('X-API-Key') # Get the api key from the header

# Verify the api key
2 changes: 2 additions & 0 deletions api/app/routes/cities.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@

@cities_bp.before_request
def check_api_key():
if request.method == 'OPTIONS': # Handle preflight requests to enable CORS
return
api_key = request.headers.get('X-API-Key') # Get the api key from the header

# Verify the api key
224 changes: 185 additions & 39 deletions api/app/routes/cleanwalks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import datetime
from flask import Blueprint, jsonify, request
from app.models import db, Cleanwalk
from flask_cors import CORS
from flask_jwt_extended import jwt_required
from sqlalchemy import func, text, and_
from app.models import City, CleanwalkUser, User, db, Cleanwalk
from app.utils import validate_api_key


cleanwalks_bp = Blueprint('cleanwalks', __name__)


@cleanwalks_bp.before_request
def check_api_key():
if request.method == 'OPTIONS': # Handle preflight requests to enable CORS
return
api_key = request.headers.get('X-API-Key') # Get the api key from the header

# Verify the api key
@@ -19,66 +26,181 @@ def check_api_key():

# Route for get Cleanwalk by id
@cleanwalks_bp.route('/<int:cleanwalk_id>', methods=['GET'])
def get_cleanwalk(cleanwalk_id):
cleanwalk = Cleanwalk.query.get(cleanwalk_id)
def get_cleanwalk_by_id(cleanwalk_id):
user_id = request.args.get('user_id', None)
print("user id", user_id )
cleanwalk = db.session.query(
Cleanwalk.id.label('cleanwalk_id'),
Cleanwalk.name.label('cleanwalk_name'),
Cleanwalk.pos_lat,
Cleanwalk.pos_long,
Cleanwalk.date_begin,
Cleanwalk.duration,
Cleanwalk.description,
Cleanwalk.address,
User.name,
User.role_id,
User.profile_picture,
User.id.label('author_id')
).join(
CleanwalkUser, Cleanwalk.id == CleanwalkUser.cleanwalk_id
).join(
User, CleanwalkUser.user_id == User.id
).filter(
Cleanwalk.id == cleanwalk_id,
CleanwalkUser.is_host == True
).one_or_none()

if cleanwalk:
# Get the number of participants
participant_count = db.session.query(CleanwalkUser).filter(
CleanwalkUser.cleanwalk_id == cleanwalk_id
).count()

# Check if the user is already a participant
is_user_participant = False
if user_id:
user_participation = db.session.query(CleanwalkUser).filter(
and_(CleanwalkUser.cleanwalk_id == cleanwalk_id, CleanwalkUser.user_id == user_id)
).first()
is_user_participant = user_participation is not None

cleanwalk_data = {
'id': cleanwalk.id,
'name': cleanwalk.name,
'id': cleanwalk.cleanwalk_id,
'name': cleanwalk.cleanwalk_name,
'pos_lat': cleanwalk.pos_lat,
'pos_long': cleanwalk.pos_long,
'date_begin': cleanwalk.date_begin,
'date_begin': cleanwalk.date_begin.isoformat(),
'duration': cleanwalk.duration,
'description': cleanwalk.description,
'city_id': cleanwalk.city_id,
'address': cleanwalk.address
'address': cleanwalk.address,
'host': {
'author_id': cleanwalk.author_id,
'name': cleanwalk.name,
'role_id': cleanwalk.role_id,
'profile_picture': cleanwalk.profile_picture
},
'participant_count': participant_count,
'is_user_participant': is_user_participant
}
return jsonify(cleanwalk_data)
else:
return jsonify({'message': 'Cleanwalk not found'}), 404

# Route for get all Cleanwalks

@cleanwalks_bp.route('', methods=['GET'])

def get_all_cleanwalks():
cleanwalks = Cleanwalk.query.all()
if cleanwalks:
cleanwalk_data = []
for cleanwalk in cleanwalks:
cleanwalk_data.append({
'id': cleanwalk.id,
'name': cleanwalk.name,
'pos_lat': cleanwalk.pos_lat,
'pos_long': cleanwalk.pos_long,
'date_begin': cleanwalk.date_begin,
'duration': cleanwalk.duration,
'description': cleanwalk.description,
'address': cleanwalk.address
})
return jsonify(cleanwalk_data)
else:
return jsonify({'message': 'Cleanwalks not found'}), 404
now = func.now() # Use current server time; ensure that the server time zone matches your requirements
two_months_later = func.date_add(now, text("interval 2 month")) # Calculate the date 2 months from now

cleanwalks = db.session.query(
Cleanwalk.id.label('cleanwalk_id'),
Cleanwalk.name.label('cleanwalk_name'),
Cleanwalk.pos_lat,
Cleanwalk.pos_long,
Cleanwalk.date_begin,
Cleanwalk.duration,
Cleanwalk.description,
Cleanwalk.address,
User.name,
User.role_id,
User.profile_picture
).join(
CleanwalkUser, Cleanwalk.id == CleanwalkUser.cleanwalk_id
).join(
User, CleanwalkUser.user_id == User.id
).filter(
CleanwalkUser.is_host == True,
Cleanwalk.date_begin + func.interval(Cleanwalk.duration, 'MINUTE') > now,
Cleanwalk.date_begin <= two_months_later
).all()

cleanwalk_data = [{
'id': cw.cleanwalk_id,
'name': cw.cleanwalk_name,
'pos_lat': cw.pos_lat,
'pos_long': cw.pos_long,
'date_begin': cw.date_begin.isoformat(), # Format datetime for JSON output
'duration': cw.duration,
'description': cw.description,
'address': cw.address,
'host': {
'name': cw.name,
'role_id': cw.role_id,
'profile_picture': cw.profile_picture
}
} for cw in cleanwalks] if cleanwalks else []

return jsonify(cleanwalk_data)

#------------------------------------POST------------------------------------#

# Route for create a new Cleanwalk
@cleanwalks_bp.route('', methods=['POST'])
@jwt_required()
def create_cleanwalk():
new_cleanwalk_data = request.json
new_cleanwalk = Cleanwalk(
name=new_cleanwalk_data['name'],
pos_lat=new_cleanwalk_data['pos_lat'],
pos_long=new_cleanwalk_data['pos_long'],
date_begin=new_cleanwalk_data['date_begin'],
duration=new_cleanwalk_data['duration'],
description=new_cleanwalk_data['description'],
city_id=new_cleanwalk_data['city_id'],
address=new_cleanwalk_data['address']
)
db.session.add(new_cleanwalk)
db.session.commit()
return jsonify({'message': 'Cleanwalk created successfully'}), 201
try:
data = request.json

city = City.query.filter_by(name=data['city']).one_or_none()
if not city:
city = City(name=data['city'])
db.session.add(city)
db.session.commit()

new_cleanwalk = Cleanwalk(
name=data['name'],
pos_lat=data['pos_lat'],
pos_long=data['pos_long'],
date_begin=data['date_begin'],
duration=data['duration'],
description=data['description'],
img_url=data['img_url'],
address=data['address'],
city_id=city.id
)
db.session.add(new_cleanwalk)
db.session.commit() # Commit here to get the cleanwalk_id

new_cleanwalk_user = CleanwalkUser(
cleanwalk_id=new_cleanwalk.id,
user_id=data['user_id'], # This needs to be provided in the request
nb_person=1,
is_host=True
)
db.session.add(new_cleanwalk_user)
db.session.commit()

return jsonify({'message': 'Cleanwalk and host association created successfully'}), 201

except Exception as e:
db.session.rollback() # Roll back in case of error
return jsonify({'error': str(e)}), 500


#route for participate in a cleanwalk
@cleanwalks_bp.route('/join', methods=['POST'])
@jwt_required()
def participate_cleanwalk():
try:
data = request.json
print("my datattatta",data)

new_cleanwalk_user = CleanwalkUser(
cleanwalk_id=data['cleanwalk_id'],
user_id=data['user_id'],
nb_person=data['nb_person'],
is_host=False
)
db.session.add(new_cleanwalk_user)
db.session.commit()

return jsonify({'message': 'User added to the cleanwalk successfully'}), 201

except Exception as e:
db.session.rollback()
#------------------------------------PUT------------------------------------#

# Route for update a Cleanwalk by its ID
@@ -114,4 +236,28 @@ def delete_cleanwalk(cleanwalk_id):
return jsonify({'message': 'Cleanwalk deleted successfully'}), 200
else:
return jsonify({'message': 'Cleanwalk not found'}), 404

# Route for leave a Cleanwalk
@cleanwalks_bp.route('/leave', methods=['DELETE'])
@jwt_required()
def leave_cleanwalk():
try:
data = request.json
print("my datattatta",data)
cleanwalk_user = CleanwalkUser.query.filter_by(
cleanwalk_id=data['cleanwalk_id'],
user_id=data['user_id']
).first()

if cleanwalk_user:
db.session.delete(cleanwalk_user)
db.session.commit()
return jsonify({'message': 'User removed from the cleanwalk successfully'}), 200
else:
return jsonify({'message': 'User not found in the cleanwalk'}), 404

except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500


45 changes: 45 additions & 0 deletions api/app/routes/upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from flask import Blueprint, request, jsonify
from werkzeug.utils import secure_filename
from flask import current_app as app
from app.utils import validate_api_key
import uuid
import os

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
_, extension = os.path.splitext(filename)
extension = extension[1:]
if extension.lower() in ALLOWED_EXTENSIONS:
return extension.lower()
else:
return False

upload_bp = Blueprint('upload', __name__)

@upload_bp.before_request
def check_api_key():
if request.method == 'OPTIONS': # Handle preflight requests to enable CORS
return
api_key = request.headers.get('X-API-Key') # Get the api key from the header

# Verify the api key
if not validate_api_key(api_key):
return jsonify({'message': 'Invalide API KEY'}), 401

@upload_bp.route('', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
if file:
filename = secure_filename(file.filename)
extention = allowed_file(filename)
if not extention:
return jsonify({'error': 'File extention not allowed'}), 400
unique_filename = str(uuid.uuid4()) + '.' + extention
file.save(os.path.join(app.config['UPLOAD_FOLDER'], unique_filename))

return jsonify({'message': 'File successfully uploaded', 'img_url': app.config['UPLOADS_URL']+unique_filename}), 200
117 changes: 97 additions & 20 deletions api/app/routes/users.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# users_bp.py (ou le nom de votre Blueprint)

import datetime
from flask import Blueprint, jsonify, request
from app.models import db, User, Role
from app.utils import validate_api_key, hash_password
from flask_cors import CORS
from app.models import db, User, Role, Organisation
from app.utils import validate_api_key, hash_password, upload_img
from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, get_jwt
from sqlalchemy.exc import IntegrityError

@@ -12,6 +14,9 @@

@users_bp.before_request
def check_api_key():
if request.method == 'OPTIONS': # Handle preflight requests to enable CORS
return

api_key = request.headers.get('X-API-Key') # Get the api key from the header

# Verify the api key
@@ -30,8 +35,7 @@ def get_user(user_id):
if user:
user_data = {
'id': user.User.id,
'firstname': user.User.firstname,
'lastname': user.User.lastname,
'name': user.User.name,
'email': user.User.email,
'created_at': user.User.created_at,
'profile_picture': user.User.profile_picture,
@@ -40,6 +44,52 @@ def get_user(user_id):
return jsonify(user_data)
else:
return jsonify({'message': 'User not found'}), 404

# route for get association by user id
@users_bp.route('/association/<string:user_id>', methods=['GET'])
def get_association(user_id):
user = db.session.query(User, Organisation).join(Organisation, User.id == Organisation.user_id).filter(User.id == user_id).first()

if user:
user_data = {
'id':user.User.id,
'name':user.User.name,
'email':user.User.email,
'description':user.Organisation.description,
'web_site':user.Organisation.website,
'social_media':user.Organisation.social_media,
'banner_img':user.Organisation.banner_img,
'profile_picture':user.User.profile_picture,
'role':user.User.role_id
}
return jsonify(user_data)
else:
return jsonify({'message': 'User not found'}), 404

# route for get all organisations
@users_bp.route('/organisations', methods=['GET'])
def get_all_organisations():
organisations = db.session.query(Organisation).join(User).all()
if organisations:
organisation_data = []
for organisation in organisations:
user = User.query.get(organisation.user_id)
organisation_data.append({
'id': user.id,
'name': user.name,
'email': user.email,
'description': organisation.description,
'web_site': organisation.web_site,
'social_media': organisation.social_media,
'banner_img': organisation.banner_img,
'profile_picture': user.profile_picture,
'role': user.role_id
})
return jsonify(organisation_data)
else:
return jsonify({'message': 'Organisations not found'}), 404



# route for get all users
@users_bp.route('', methods=['GET'])
@@ -50,8 +100,7 @@ def get_all_users():
for user in users:
user_data.append({
'id': user.User.id,
'firstname': user.User.firstname,
'lastname': user.User.lastname,
'name': user.User.name,
'email': user.User.email,
'created_at': user.User.created_at,
'profile_picture': user.User.profile_picture,
@@ -89,7 +138,7 @@ def login():
return jsonify({
'message': 'Successful connection',
'id': res.User.id, 'email': email,
'firstname': res.User.firstname,
'name': res.User.name,
'profile_picture': res.User.profile_picture,
'access_token': access_token,
'role': res.Role.role
@@ -113,46 +162,57 @@ def tokenLogin():
'message': 'Successful connection',
'id': res.User.id,
'email': res.User.email,
'firstname': res.User.firstname,
'lastname': res.User.lastname,
'name': res.User.name,
'profile_picture': res.User.profile_picture,
"role":claims["role"]
}), 200
else:
return jsonify({'message': 'invalide token'}), 401


# route for creating a new user
@users_bp.route('', methods=['POST'])
def create_user():
data = request.get_json()
data = request.get_json(silent=True) # Get JSON data from the request
print(data)
if not data:
return jsonify({'message': 'Request body is empty or not a valid JSON'}), 400

required_keys = ['name', 'email', 'password', 'role_id']
if not all(key in data for key in required_keys):
return jsonify({'message': 'Request body is incomplete'}), 400

# Extract user data from the request JSON
firstname = data.get('firstname')
lastname = data.get('lastname')
name = data.get('name')
email = data.get('email')
profile_picture = data.get('profile_picture')
salt = os.urandom(16)
password = hash_password(data.get('password'), salt)
role_id = data.get('role_id')


try:
# Essayez de créer le nouvel utilisateur
new_user = User(
firstname=firstname,
lastname=lastname,
name=name,
email=email,
profile_picture=profile_picture,
password=password,
salt=salt,
role_id=role_id
role_id=role_id,
created_at=datetime.datetime.now()
)
db.session.add(new_user)
db.session.commit()

if role_id == 2: # If the user is an organisation create association table
new_association = Organisation(user_id=new_user.id)
db.session.add(new_association)
db.session.commit()

return jsonify({'message': 'User created successfully', 'user_id': new_user.id}), 201

except IntegrityError as e:
print(e, "ErrorIntegrity")
# Gérez l'erreur d'intégrité (adresse e-mail en double)
db.session.rollback() # Annuler la transaction
return jsonify({'message': 'Email address already in use'}), 400
@@ -163,19 +223,36 @@ def create_user():
# route for updating user by id

@users_bp.route('/<string:user_id>', methods=['PUT'])
@jwt_required()
def update_user(user_id):
user = User.query.get(user_id)

if user:
data = request.get_json()

# Update user data from the request JSON
user.firstname = data.get('firstname', user.firstname)
user.lastname = data.get('lastname', user.lastname)
user.email = data.get('email', user.email)
user.name = data.get('name', user.name)
user.profile_picture = data.get('profile_picture', user.profile_picture)

db.session.commit()
return jsonify({'message': 'User updated successfully'})
else:
return jsonify({'message': 'User not found'}), 404

# route for updating user password by id
@users_bp.route('/password/<string:user_id>', methods=['PUT'])
@jwt_required()
def update_user_password(user_id):
user = User.query.get(user_id)

if user:
data = request.get_json()
print("my datata",data)

# Check if the old password is correct
if user.password == hash_password(data.get('old_password'), user.salt):
# Update user password
user.password = hash_password(data.get('new_password'), user.salt)
db.session.commit()
return jsonify({'message': 'Password updated successfully'})
else:
return jsonify({'message': 'Old password is incorrect'}), 400
17 changes: 15 additions & 2 deletions api/app/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# utils.py
import hashlib
import hashlib
from flask import current_app
from werkzeug.utils import secure_filename
import os

def validate_api_key(api_key):
# Get the API key from app.config
@@ -13,4 +15,15 @@ def validate_api_key(api_key):
def hash_password(password, salt):
salted_password = salt + password.encode('utf-8')
hashed_password = hashlib.sha256(salted_password).hexdigest()
return hashed_password
return hashed_password

def upload_img(image):
# Check if the image is allowed
if '.' in image.filename and image.filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']:
filename = secure_filename(image.filename)
image.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
return filename
else:
return None


9 changes: 7 additions & 2 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Flask
Flask-SQLAlchemy
Flask-JWT-Extended
mysql-connector
pymysql
python-dotenv
flask-uploads
flask-cors
Flask>=2.0.3
Werkzeug>=2.0.3
gunicorn==20.1.0

4 changes: 4 additions & 0 deletions api/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# wsgi.py for running the app in production
from app import create_app

app = create_app()
85 changes: 0 additions & 85 deletions database-dump.sql

This file was deleted.

305 changes: 305 additions & 0 deletions database/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
-- phpMyAdmin SQL Dump
-- version 5.1.2
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Apr 09, 2024 at 02:02 PM
-- Server version: 5.7.24
-- PHP Version: 8.0.1

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

--
-- Database: `cleanwalk_db`
--

-- --------------------------------------------------------

--
-- Table structure for table `articles`
--

CREATE TABLE `articles` (
`id` int(11) NOT NULL,
`title` varchar(255) NOT NULL,
`author_id` int(11) NOT NULL,
`content` json NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`description` text NOT NULL,
`published` tinyint(1) NOT NULL,
`preview_picture` varchar(255) NOT NULL,
`categorie_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- Table structure for table `categories`
--

CREATE TABLE `categories` (
`id` int(11) NOT NULL,
`category` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- Table structure for table `categories_article`
--

CREATE TABLE `categories_article` (
`id_category` int(11) NOT NULL,
`id_article` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- Table structure for table `cities`
--

CREATE TABLE `cities` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Dumping data for table `cities`
--

INSERT INTO `cities` (`id`, `name`) VALUES
(1, 'Nantes'),
(2, 'Paris');

-- --------------------------------------------------------

--
-- Table structure for table `cleanwalks`
--

CREATE TABLE `cleanwalks` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`pos_lat` float NOT NULL,
`pos_long` float NOT NULL,
`date_begin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`duration` int(11) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`address` varchar(255) NOT NULL,
`city_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Dumping data for table `cleanwalks`
--

INSERT INTO `cleanwalks` (`id`, `name`, `pos_lat`, `pos_long`, `date_begin`, `duration`, `description`, `address`, `city_id`) VALUES
(1, 'cleanwalk1', 51, 32, '2024-01-08 14:49:22', 1, 'lalalallalalaa', '124 rue des arbres', 1),
(2, 'cleanwalk2', 42, 42, '2024-01-08 14:49:22', 3, 'lololololololo', '134 rue de cactus', 1);

-- --------------------------------------------------------

--
-- Table structure for table `roles`
--

CREATE TABLE `roles` (
`id` int(11) NOT NULL,
`role` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Dumping data for table `roles`
--

INSERT INTO `roles` (`id`, `role`) VALUES
(1, 'user'),
(2, 'organisation');

-- --------------------------------------------------------

--
-- Table structure for table `users`
--

CREATE TABLE `users` (
`id` int(11) NOT NULL,
`firstname` varchar(255) NOT NULL,
`lastname` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`profile_picture` varchar(255) DEFAULT NULL,
`salt` binary(16) NOT NULL,
`role_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Dumping data for table `users`
--

INSERT INTO `users` (`id`, `firstname`, `lastname`, `email`, `password`, `created_at`, `profile_picture`, `salt`, `role_id`) VALUES
(16, 'John', 'Doe', 'john.doe@example.com', '8f8c062c5315145371bc3d8e6b3f4f2cda9fc98f7b04b674059badc25346f0b8', '2024-01-08 14:28:31', NULL, 0x83c89d22a4c22b2b3b53034a70b6ec3c, 1),
(18, 'Arthur', 'FRIN', 'frin.arthur@gmail.com', '9bd42f54288ff497a94e03d3b787d97bbd1b79edcd53bd6736a86dec9bbbe9b1', '2024-01-08 14:30:34', NULL, 0xb98d190daef45db537fa2c357f652068, 1);

-- --------------------------------------------------------

--
-- Table structure for table `user_cleanwalk`
--

CREATE TABLE `user_cleanwalk` (
`user_id` int(11) NOT NULL,
`cleanwalk_id` int(11) NOT NULL,
`nb_person` int(11) NOT NULL,
`is_host` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `articles`
--
ALTER TABLE `articles`
ADD PRIMARY KEY (`id`),
ADD KEY `author_id` (`author_id`),
ADD KEY `categorie_id` (`categorie_id`);

--
-- Indexes for table `categories`
--
ALTER TABLE `categories`
ADD PRIMARY KEY (`id`);

--
-- Indexes for table `categories_article`
--
ALTER TABLE `categories_article`
ADD PRIMARY KEY (`id_category`,`id_article`),
ADD KEY `id_article` (`id_article`);

--
-- Indexes for table `cities`
--
ALTER TABLE `cities`
ADD PRIMARY KEY (`id`);

--
-- Indexes for table `cleanwalks`
--
ALTER TABLE `cleanwalks`
ADD PRIMARY KEY (`id`),
ADD KEY `city_id` (`city_id`);

--
-- Indexes for table `roles`
--
ALTER TABLE `roles`
ADD PRIMARY KEY (`id`);

--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `email` (`email`),
ADD KEY `role_id` (`role_id`);

--
-- Indexes for table `user_cleanwalk`
--
ALTER TABLE `user_cleanwalk`
ADD PRIMARY KEY (`user_id`,`cleanwalk_id`),
ADD KEY `cleanwalk_id` (`cleanwalk_id`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `articles`
--
ALTER TABLE `articles`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

--
-- AUTO_INCREMENT for table `categories`
--
ALTER TABLE `categories`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

--
-- AUTO_INCREMENT for table `cities`
--
ALTER TABLE `cities`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;

--
-- AUTO_INCREMENT for table `cleanwalks`
--
ALTER TABLE `cleanwalks`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;

--
-- AUTO_INCREMENT for table `roles`
--
ALTER TABLE `roles`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;

--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=19;

--
-- Constraints for dumped tables
--

--
-- Constraints for table `articles`
--
ALTER TABLE `articles`
ADD CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`);

--
-- Constraints for table `categories_article`
--
ALTER TABLE `categories_article`
ADD CONSTRAINT `categories_article_ibfk_1` FOREIGN KEY (`id_article`) REFERENCES `articles` (`id`),
ADD CONSTRAINT `categories_article_ibfk_2` FOREIGN KEY (`id_category`) REFERENCES `categories` (`id`);

--
-- Constraints for table `cleanwalks`
--
ALTER TABLE `cleanwalks`
ADD CONSTRAINT `cleanwalks_ibfk_2` FOREIGN KEY (`city_id`) REFERENCES `cities` (`id`);

--
-- Constraints for table `users`
--
ALTER TABLE `users`
ADD CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);

--
-- Constraints for table `user_cleanwalk`
--
ALTER TABLE `user_cleanwalk`
ADD CONSTRAINT `user_cleanwalk_ibfk_1` FOREIGN KEY (`cleanwalk_id`) REFERENCES `cleanwalks` (`id`),
ADD CONSTRAINT `user_cleanwalk_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);
COMMIT;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
14 changes: 0 additions & 14 deletions docker-compose.dev.yml

This file was deleted.

38 changes: 38 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
version: '3.8'
services:

frontend:
build:
context: ./front
dockerfile: Dockerfile
restart: always
networks:
- my_network

api:
build:
context: ./api
dockerfile: Dockerfile.prod
volumes:
- ./uploads:/app/uploads
restart: always
networks:
- my_network

nginx-proxy-manager:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data-nginx:/data
- ./letsencrypt:/etc/letsencrypt
- ./uploads:/var/www/uploads
networks:
- my_network

networks:
my_network:
external: true
84 changes: 23 additions & 61 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,65 +1,27 @@
version: '3.7'
services:
front:
hostname: front
container_name: front
image: cleanwalk-org/front
build:
context: ./front
dockerfile: Dockerfile
target: prod
restart: unless-stopped
depends_on:
- api
- db
ports:
- "80:80"
networks:
- frontend

api:
hostname: api
container_name: api
image: cleanwalk-org/api
build:
context: ./api
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
- db
env_file:
- stash.env
networks:
- frontend
- backend

db:
hostname: db
container_name: db
image: mysql:latest
restart: unless-stopped
env_file:
- stash.env
volumes:
- db:/var/lib/mysql
networks:
- backend

adminer:
container_name: adminer
image: adminer:latest
restart: unless-stopped
depends_on:
- db
ports:
- "8080:8080"
env_file:
- stash.env
networks:
- backend

volumes:
db:
adminer:
image: adminer
restart: always
environment:
- ADMINER_DESIGN=dracula
ports:
- '8080:8080'

api:
build:
context: ./api
dockerfile: Dockerfile.dev # Chemin vers le Dockerfile de production de l'API
restart: always
volumes:
- ./api:/app # Montez le code de l'API dans le conteneur
- ./uploads:/app/images
ports:
- '5000:5000' # Exposez le port 5000 pour l'API Flask
environment:
- UPLOAD_FOLDER=/app/images

networks:
frontend:
backend:
my_network:
external: true
3 changes: 3 additions & 0 deletions front/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_API_KEY=1234567890
VITE_GOOGLE_CLIENT_ID= usdfhsuofgsdugfusdghusdfg.apps.googleusercontent.com
VITE_API_URL=http://127.0.0.1:5000
39 changes: 8 additions & 31 deletions front/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,34 +1,11 @@
FROM node:20-alpine AS pre-prod

RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

WORKDIR /home/node/app

FROM node:latest as build-stage
WORKDIR /app
COPY package*.json ./

USER node

RUN npm ci

COPY --chown=node:node . .


FROM pre-prod AS dev

EXPOSE 5173

CMD [ "npm", "run", "start" ]


FROM pre-prod AS build

RUN npm install
COPY ./ .
RUN npm run build


FROM httpd:alpine AS prod

COPY --from=build /home/node/app/dist /usr/local/apache2/htdocs/

EXPOSE 80

CMD ["httpd-foreground"]
FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf
33 changes: 19 additions & 14 deletions front/index.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
<title>Cleanwalk.org</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Catamaran:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet">
<title>Cleanwalk.org</title>
</head>

<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>

</html>
44 changes: 44 additions & 0 deletions front/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;

server {
listen 80;
server_name localhost;

location / {
root /app;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

location /nominatim {
proxy_pass https://nominatim.openstreetmap.org;
proxy_http_version 1.1;
proxy_set_header Host nominatim.openstreetmap.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
1,784 changes: 1,135 additions & 649 deletions front/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions front/package.json
Original file line number Diff line number Diff line change
@@ -19,16 +19,19 @@
"ky": "^1.2.0",
"leaflet": "^1.9.4",
"pinia": "^2.1.7",
"uuid": "^9.0.1",
"vue": "^3.4.15",
"vue-cookies": "^1.8.3",
"vue-router": "^4.2.5"
"vue-router": "^4.2.5",
"vue3-google-login": "^2.0.26"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node20": "^20.1.2",
"@types/jsdom": "^21.1.6",
"@types/leaflet": "^1.9.8",
"@types/node": "^20.11.10",
"@types/node": "^20.14.11",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
5 changes: 4 additions & 1 deletion front/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import {RouterView } from 'vue-router'
import Toast from './components/Toast.vue';
</script>

<template>
<Toast />
<RouterView />
</template>
Binary file added front/src/assets/Openstreetmap_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion front/src/assets/base.scss
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ body {
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family: "Roboto", sans-serif;
font-family: "Catamaran", sans-serif;
font-size: 16px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
1 change: 1 addition & 0 deletions front/src/assets/googleMap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 36 additions & 2 deletions front/src/assets/main.scss
Original file line number Diff line number Diff line change
@@ -8,7 +8,41 @@
.button-primary {
background-color: var(--color-primary );
color: #fff;
font-family: "Roboto", sans-serif;
font-family: "Catamaran", sans-serif;
font-size: 16px;
border-radius: 8px;
}
}

.google-button {
border-radius: var(--Button-Border-radius, 8px);
border: 1px solid #CBD5E1;
}

.action-button {
background-color: var(--color-primary);
color: #fff;
padding:0.75rem 0;
width: 100%;
text-align: center;
border-radius: 8px;
}

.danger-button {
background-color: #FF5757;
color: #fff;
padding:0.75rem 0;
width: 100%;
text-align: center;
border-radius: 8px;
}

.cancel-button {
background-color: #E8E8E8;
color: #373646;
padding:0.75rem 0;
width: 100%;
text-align: center;
border-radius: 8px;
}


61 changes: 61 additions & 0 deletions front/src/components/AddChoice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import iconAdd from './icons/icon-add.vue';
</script>

<template>
<section class="container">
<h1>Que souhaitez vous ajouter ?</h1>
<router-link class="add" to="/add/cleanwalk">
<icon-add />
<div>Ajouter une cleanwalk</div>
</router-link>
<router-link class="add" to="/add/article">
<icon-add />
<div>Ajouter un article</div>
</router-link>
</section>
</template>

<style scoped lang="scss">
.container {
padding-top: 78px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 78px 2rem 0;
color: #333;
h1 {
margin-top: 2rem;
font-size: 20px;
font-style: normal;
font-weight: 700;
}
.add {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
stroke: #373646;
background-color: var(--color-secondary);
border-radius: 8px;
width: 100%;
height: 165px;
margin-top: 2rem;
svg {
width: 68px;
height: 68px;
}
div {
text-align: center;
font-size: 20px;
font-weight: 700;
}
}
}
</style>
439 changes: 395 additions & 44 deletions front/src/components/AddCleanwalk.vue

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions front/src/components/ArticlesList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { type Article } from '@/interfaces/articleInterface';
const articles: Article[] = [
{
id: 1,
content: "Contenu de l'article 1",
title: "Titre de l'article 1",
imageUrl: "https://cdn2.thecatapi.com/images/b4q.jpg",
createdAt: "2022-04-02"
},
{
id: 2,
content: "Contenu de l'article 2",
title: "Titre de l'article 2",
imageUrl: "https://cdn2.thecatapi.com/images/8jl.jpg",
createdAt: "2022-01-02"
},
{
id: 3,
content: "Contenu de l'article 3",
title: "Titre de l'article 3",
imageUrl: "https://cdn2.thecatapi.com/images/a66.jpg",
createdAt: "2022-01-03"
},
{
id: 4,
content: "Contenu de l'article 4",
title: "Titre de l'article 4",
imageUrl: "https://cdn2.thecatapi.com/images/a90.jpg",
createdAt: "2022-01-04"
},
{
id: 5,
content: "Contenu de l'article 5",
title: "Titre de l'article 5",
imageUrl: "https://cdn2.thecatapi.com/images/a66.jpg",
createdAt: "2022-01-05"
},
{
id: 3,
content: "Contenu de l'article 3",
title: "Titre de l'article 3",
imageUrl: "https://cdn2.thecatapi.com/images/a66.jpg",
createdAt: "2022-01-03"
},
{
id: 4,
content: "Contenu de l'article 4",
title: "Titre de l'article 4",
imageUrl: "https://cdn2.thecatapi.com/images/a90.jpg",
createdAt: "2022-01-04"
},
{
id: 5,
content: "Contenu de l'article 5",
title: "Titre de l'article 5",
imageUrl: "https://cdn2.thecatapi.com/images/a66.jpg",
createdAt: "2022-01-05"
}
];
</script>

<template>
<div class="container">
<div v-for="article in articles" :key="article.id" class="card">
<img :src="article.imageUrl" alt="image de l'article" />
<div class="right">
<h2>{{ article.title }}</h2>
<div class="creation-date">{{ article.createdAt }}</div>
</div>
</div>

</div>
</template>

<style scoped lang="scss">
.container{
padding-top: 8rem;
overflow: auto;
display: flex;
flex-direction: column;
.card {
display: flex;
padding: 25px 0px;
margin: 0 30px;
border-bottom: 1px solid #CBD5E1;
img {
width: 22vw;
aspect-ratio: 1;
object-fit: cover;
border-radius: 8px;
}
.right {
padding: 2px 0 2px 1rem;
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-between;
h2 {
font-size: 12px;
font-weight: 700;
}
.creation-date {
font-size: 1rem;
font-size: 8px;
}
}
}
}
</style>
140 changes: 140 additions & 0 deletions front/src/components/AssoList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { type Asso } from '@/interfaces/assoInterface';
import { ref, type Ref } from 'vue';
const assoList: Ref<Asso[]> = ref([
{
title: "Association 1",
description: "Description de l'association 1",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 2",
description: "Description de l'association 2",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 3",
description: "Description de l'association 3",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 4",
description: "Description de l'association 4",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 5",
description: "Description de l'association 5",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 6",
description: "Description de l'association 6",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 7",
description: "Description de l'association 7",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 8",
description: "Description de l'association 8",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 9",
description: "Description de l'association 9",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 10",
description: "Description de l'association 10",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 11",
description: "Description de l'association 11",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
},
{
title: "Association 12",
description: "Description de l'association 12",
imageUrl: "https://cdn2.thecatapi.com/images/4f3.jpg",
coverImageUrl: "https://cdn2.thecatapi.com/images/700.jpg"
}
]);
</script>


<template>
<section class="container">
<div v-for="asso in assoList" :key="asso.title" class="asso-card">
<img :src="asso.coverImageUrl" alt="cover-img" class="cover">
<img :src="asso.imageUrl" alt="asso-img" class="img">
<h3>{{ asso.title }}</h3>
</div>
</section>
</template>

<style scoped lang="scss">
.container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
width: 100vw;
justify-content: center;
padding: 150px 0 90px;
.asso-card {
width: 171px; // Inclut les marges et les bordures dans la largeur de la carte
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border: 1px solid #CBD5E1;
.cover {
width: 100%;
object-fit: cover;
aspect-ratio: 9/3;
}
.img {
width: 100%;
object-fit: cover;
aspect-ratio: 1/1;
border-radius: 9999px;
width: 55px;
position: relative;
margin-top: -30px;
margin-left: 58px;
border: 2px solid #fff;
}
h3 {
font-size: 12px;
font-weight: 700;
color: #373646;
text-align: center;
margin: 5px 0 10px 0;
}
}
}
</style>
80 changes: 80 additions & 0 deletions front/src/components/LeaveCwPopup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from 'vue'
import iconCross from './icons/icon-cross.vue';
defineProps({
isVisible: Boolean,
tooglePopup: Function,
leaveCw: Function
});
</script>

<template>
<div v-if="isVisible" class="popup">
<div class="popup-content">
<div class="cross"><iconCross @click="tooglePopup!" /></div>
<h2>Se désinscrire</h2>
<p>Etes vous certain de vouloir vous désinscrire de la clenawalk</p>
<div class="btn-container">
<button @click="tooglePopup!" class="cancel-button">annuler</button>
<button @click="leaveCw!" class="danger-button">confirmer</button>
</div>
</div>
</div>
</template>

<style scoped lang="scss">
.popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
.popup-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
padding: 1rem;
margin: 1.5rem;
.cross {
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
stroke: var(--text-color-primary);
}
h2 {
font-size: 18px;
font-weight: 700;
margin-bottom: 1rem;
text-align: center;
padding-bottom: 1rem;
}
p {
font-size: 12px;
font-style: normal;
font-weight: 400;
padding: 0 1rem;
margin-bottom: 3rem;
}
}
.btn-container {
display: flex;
justify-content: space-between;
gap: 1rem;
}
}
</style>
169 changes: 169 additions & 0 deletions front/src/components/Login.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<script setup lang="ts">
import { ref } from 'vue';
import Toast from '@/components/Toast.vue';
import apiHelper from '@/helpers/apiHelper';
import { useAccountStore } from '@/stores/AccountStore';
import type { User } from '@/interfaces/userInterface';
import { useRouter } from 'vue-router';
import {useUtilsStore} from '@/stores/UtilsStore';
const router = useRouter();
const accountStore = useAccountStore();
const showToast = useUtilsStore().showToast;
const email = ref("");
const password = ref("");
const login = async ( ) => {
if(!email.value) {
showToast("Veuillez renseigner votre email", false);
return;
}
if(!password.value) {
showToast("Veuillez renseigner votre mot de passe", false);
return;
}
// Call the login API
const response = await apiHelper.kyPostWithoutToken( "/users/login", {
email: email.value,
password: password.value
});
if(response.success === false) {
showToast('Email ou mot de passe incorrect', false);
return;
}
const user:User = {
email: response.data.email as string,
name: response.data.name as string,
id: response.data.id as number,
profile_picture: response.data.profile_picture as string,
role: response.data.role as "organisation" | "user",
}
accountStore.CurrentUser = user;
router.push({ path: '/' });
accountStore.setToken(response.data.access_token as string);
}
</script>

<template>
<Toast />
<section class="container">

<h1>
Se connecter
</h1>
<!-- <GoogleLogin :callback="callback" />
<div class="or">
<div class="line"></div>
<span>ou</span>
<div class="line"></div>
</div> -->
<form @submit.prevent="login()">
<label class="label" for="email">Email</label>
<input v-model="email" class="input" name="mdp" type="email" placeholder="user@domain.fr">
<label class="label" for="mdp">Mot de passe</label>
<input v-model="password" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<button class="action-button" type="submit">Se connecter</button>
</form>
<router-link to="/signup" class="go-signup">
Vous êtes nouveau chez cleanwalk.org : <span>Inscrivez-vous</span>
</router-link>
</section>
</template>

<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
padding: 0 2rem;
h1 {
font-size: 24px;
font-weight: 500;
margin-bottom: 1rem;
}
}
.go-signup {
color: var(--text-color-secondary);
font-size: 12px;
width: 100%;
text-align: left;
margin-top: 2rem;
span {
color: var(--text-color-primary);
font-weight: 500;
cursor: pointer;
text-decoration: underline;
}
}
.or {
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
span {
margin: 0 1rem;
}
.line {
width: 35vw;
height: 1px;
background-color: #747474;
}
}
form {
display: flex;
width: 100%;
flex-direction: column;
color: #94A3B8;
.label {
font-size: 12px;
font-weight: 500;
position: relative;
margin-bottom: -18px;
background-color: #fff;
width: fit-content;
margin-left: 13px;
margin-top: 5px;
}
.input {
border: 1px solid #94A3B8;
border-radius: 8px;
padding: 12px;
margin-top: 0.5rem;
font-size: 14px;
font-style: normal;
font-weight: 500;
&::placeholder {
color: #94A3B8;
}
&:focus {
outline: none;
}
}
.action-button {
margin-top: 1.5rem;
}
}
.danger-button {
margin-top: 3.5rem;
}
</style>
81 changes: 81 additions & 0 deletions front/src/components/LogoutPopup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import iconCross from './icons/icon-cross.vue';
import { useAccountStore } from '@/stores/AccountStore';
const lougout = useAccountStore().logout;
defineProps({
isVisible: Boolean,
togglePopup: Function,
});
</script>

<template>
<div v-if="isVisible" class="popup">
<div class="popup-content">
<div class="cross"><iconCross @click="togglePopup!" /></div>
<h2>Se déconnecter</h2>
<p>Etes vous certain de vouloir vous déconnecter ?</p>
<div class="btn-container">
<button @click="togglePopup!" class="cancel-button">annuler</button>
<button @click="lougout()" class="danger-button">confirmer</button>
</div>
</div>
</div>
</template>

<style scoped lang="scss">
.popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
.popup-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
padding: 1rem;
margin: 1.5rem;
.cross {
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
stroke: var(--text-color-primary);
}
h2 {
font-size: 18px;
font-weight: 700;
margin-bottom: 1rem;
text-align: center;
padding-bottom: 1rem;
}
p {
font-size: 12px;
font-style: normal;
font-weight: 400;
padding: 0 1rem;
margin-bottom: 3rem;
}
}
.btn-container {
display: flex;
justify-content: space-between;
gap: 1rem;
}
}
</style>
165 changes: 165 additions & 0 deletions front/src/components/Menu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<script setup lang="ts">
import iconRightArrow from '@/components/icons/icon-right-arrow.vue';
import iconCalendar from '@/components/icons/icon-calendar.vue';
import iconSettings from '@/components/icons/icon-settings.vue';
import iconBackTime from '@/components/icons/icon-back-time.vue';
import iconFile from '@/components/icons/icon-file.vue';
import iconPaperPlane from '@/components/icons/icon-paper-plane.vue';
import iconLogout from '@/components/icons/icon-logout.vue';
import { useAccountStore } from '@/stores/AccountStore';
import { ref } from 'vue';
import LogoutPopup from './LogoutPopup.vue';
const accountStore = useAccountStore();
const isPopupVisible = ref(false);
const togglePopup = () => {
isPopupVisible.value = !isPopupVisible.value;
};
const currentUser = accountStore.CurrentUser!;
</script>

<template>
<LogoutPopup :is-visible="isPopupVisible" :toggle-popup="togglePopup" />
<section class="container">
<router-link v-if="currentUser" to="/menu/profile" class="profil">
<img class="img" :src="currentUser.profile_picture" alt="">
<h3 >{{ currentUser.name }}</h3>
<iconRightArrow />
</router-link>
<div v-if="!currentUser" class="unlog-profiles">
<router-link to="/login" class="profil unlog">
<h3 >Se connecter</h3>
<iconRightArrow />
</router-link>
<router-link to="/signup" class="profil unlog">
<h3 >S'inscrire</h3>
<iconRightArrow />
</router-link>
</div>
<ul class="list">
<li v-if="currentUser">
<iconCalendar />
<h3>Mes évènements</h3>
<iconRightArrow />
</li>
<li>
<iconSettings />
<h3>Paramètres</h3>
<iconRightArrow />
</li>
<li>
<iconBackTime />
<h3>L’histoire de Cleanwalk.org</h3>
<iconRightArrow />
</li>
<li>
<iconFile />
<h3>Guide de la cleanwalk</h3>
<iconRightArrow />
</li>
<li>
<iconPaperPlane />
<h3>Nous contacter</h3>
<iconRightArrow />
</li>
</ul>

<button v-if="currentUser" @click="togglePopup()" class="logout">
<iconLogout />
<h3>Se Déconnecter</h3>
<iconRightArrow class="arrow"/>
</button>
</section>
</template>

<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
padding: 100px 1.6rem 100px 1.6rem;
color: #94A3B8;
font-size: 14px;
font-style: normal;
font-weight: 700;
justify-content: space-between;
height: 100vh;
.unlog-profiles {
width: 100%;
gap: 1rem;
display: flex;
flex-direction: column;
}
.profil {
stroke: #94A3B8;
border: 1px solid #CBD5E1;
width: 100%;
display: flex;
align-items: center;
padding: 1rem;
border-radius: 8px;
&.unlog {
color: var(--color-primary);
;
}
.img {
width: 44px;
aspect-ratio: 1;
object-fit: cover;
border-radius: 9999px;
}
}
h3 {
margin: 0 1rem;
flex-grow: 1;
}
.list {
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
li {
stroke: #94A3B8;
display: flex;
width: 100%;
border: 1px solid #CBD5E1;
border-radius: 8px;
align-items: center;
padding: 12px;
}
}
.logout {
stroke: #FF5757;
background-color: transparent;
border: 1px solid #CBD5E1;
color: #FF5757;
width: 100%;
display: flex;
align-items: center;
padding: 1rem;
border-radius: 8px;
margin-top: 50px;
.arrow {
stroke: #94A3B8;
}
}
}
</style>
250 changes: 101 additions & 149 deletions front/src/components/MobileHome.vue
Original file line number Diff line number Diff line change
@@ -2,108 +2,42 @@
import "leaflet/dist/leaflet.css";
import { LMap, LTileLayer, LMarker, LIcon } from "@vue-leaflet/vue-leaflet";
import L, { LatLng, LatLngBounds, Map, type PointExpression } from "leaflet";
import { ref, type Ref, onMounted, nextTick } from "vue";
import { ref, type Ref, onMounted, nextTick, computed } from "vue";
import iconLeftArrow from "@/components/icons/icon-left-arrow.vue";
import iconSearch from "@/components/icons/icon-search.vue";
import iconInfo from "./icons/icon-info.vue";
import { useCleanwalkStore } from '@/stores/CleanwalkStore';
import iconClock from './icons/icon-clock.vue';
import iconMiniMap from './icons/icon-mini-map.vue';
import router from "@/router";
import { max } from "date-fns";
import type { Cleanwalk } from "@/interfaces/cleanwalkInterface";
import dateHelper from "@/helpers/dateHelper";
import cleanwalkCard from './cards/CleanwalkListCard.vue';
import { useAccountStore } from "@/stores/AccountStore";
const cleanwalkStore = useCleanwalkStore()
const userImg = useAccountStore().CurrentUser?.profile_picture;
const cleanwalkStore = useCleanwalkStore();
const draggableCard = ref<HTMLElement | null>(null);
let zoom = ref(5);
let centerMap = ref([48.866667, 2.333333]);
let center: Ref<PointExpression> = ref([48.866667, 2.333333]);
let center: Ref<PointExpression> = ref([47.866667, 2.333333]);
let mapInstance: Ref<Map | null> = ref(null);
let cardListBool = ref(false); //to display or not the cleanwalk list
const cleanwalkListContainer = ref<HTMLElement | null>(null); //ref to the html cleanwalk list container
const testCleanwalkList = ref([
{
id: 1,
title: "Cleanwalk 1",
lat: 42.866667,
lng: 2.333333,
isAsso: true
},
{
id: 1,
title: "Cleanwalk 2",
lat: 45.866667,
lng: 1.333333,
isAsso: false
},
{
id: 1,
title: "Cleanwalk 3",
lat: 48.866667,
lng: 4.333333,
isAsso: false
},
{
id: 1,
title: "Cleanwalk 4",
lat: 49.866667,
lng: 3.333333,
isAsso: true
},
{
id: 1,
title: "Cleanwalk 1",
lat: 42.866667,
lng: 2.333333,
isAsso: true
},
{
id: 1,
title: "Cleanwalk 2",
lat: 45.866667,
lng: 1.333333,
isAsso: false
},
{
id: 1,
title: "Cleanwalk 3",
lat: 48.866667,
lng: 4.333333,
isAsso: false
},
{
id: 1,
title: "Cleanwalk 4",
lat: 49.866667,
lng: 3.333333,
isAsso: true
},
{
id: 1,
title: "Cleanwalk 2",
lat: 45.866667,
lng: 1.333333,
isAsso: false
},
{
id: 1,
title: "Cleanwalk 3",
lat: 48.866667,
lng: 4.333333,
isAsso: false
},
{
id: 1,
title: "Cleanwalk 4",
lat: 49.866667,
lng: 3.333333,
isAsso: true
},
]);
let selectedCleanwalk:Ref<Cleanwalk | null> = ref(null);
const searchInput = ref("");
const filteredCleanwalks = computed(() => {
if (!searchInput.value) {
return cleanwalkStore.cleanwalksTab;
}
return cleanwalkStore.cleanwalksTab.filter(cleanwalk =>
cleanwalk.name.toLowerCase().includes(searchInput.value.toLowerCase())||
cleanwalk.address.toLowerCase().includes(searchInput.value.toLowerCase())
);
});
const backButton = () => {
cardListBool.value = false
searchInput.value = "" //reset the search input
@@ -117,7 +51,7 @@ const setMapEvents = (map: Map) => {
};
onMounted(() => {
console.log(cleanwalkStore.cleanwalkIsSelect.valueOf())
cleanwalkStore.getAllCleanwalks();
const card = draggableCard.value;
if (!card) return;
@@ -126,7 +60,7 @@ onMounted(() => {
let maxHeight = -10 // Limite de hauteur en px
const onTouchStart = (e: TouchEvent) => {
if (cleanwalkStore.cleanwalkIsSelect) { //bloquer le slide si aucune cleanwalk sélectionnée
if (selectedCleanwalk.value !== null) { //bloquer le slide si aucune cleanwalk sélectionnée
maxHeight = 65;
} else {
maxHeight = -10;
@@ -141,7 +75,7 @@ onMounted(() => {
e.preventDefault(); // Prévenir le scroll de la page
const diffY = startY - e.touches[0].clientY;
let newBottom = initialBottom + diffY;
if (!cleanwalkStore.cleanwalkIsSelect) { // si aucune cleanwalk n'est sélectionnée on affiche la liste
if (selectedCleanwalk.value === null) { // si aucune cleanwalk n'est sélectionnée on affiche la liste
showCleanwalkList();
}
if (newBottom > maxHeight) {
@@ -160,8 +94,31 @@ onMounted(() => {
card.addEventListener('touchstart', onTouchStart);
});
function slideUp() {
cleanwalkStore.cleanwalkIsSelect = true;
const setSelectedCleanwalk = (cleanwalkId: number) => {
const cw = cleanwalkStore.cleanwalksTab.find((cleanwalk) => cleanwalk.id === cleanwalkId);
if (cw) {
selectedCleanwalk.value = cw;
}
};
const getCleanwalkVisibleCount = () => {
let count = 0;
cleanwalkStore.cleanwalksTab.forEach((cleanwalk) => {
if (isPointVisible(cleanwalk.pos_lat, cleanwalk.pos_long)) {
count++;
}
});
if (count === 1) {
slideUp(cleanwalkStore.cleanwalksTab.find((cleanwalk) => isPointVisible(cleanwalk.pos_lat, cleanwalk.pos_long))?.id);
}
return count;
};
function slideUp(id?: number) {
if(!id) {
return;
}
setSelectedCleanwalk(id);
if (draggableCard.value) {
draggableCard.value.style.bottom = '65px'; // Modifiez cette valeur selon la hauteur désirée
}
@@ -203,6 +160,7 @@ function isPointVisible(lat: number, lng: number): boolean {
}
function mapClick() {
selectedCleanwalk.value = null;
slideDown()
}
@@ -214,75 +172,68 @@ function mapClick() {
<l-map ref="map" v-model:zoom="zoom" v-model:center="center" @ready="setMapEvents" :min-zoom="5"
@click="mapClick()" :useGlobalLeaflet="false">
<l-tile-layer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png" layer-type="base"></l-tile-layer>
<div v-for="cleanwalk in testCleanwalkList">
<l-marker @click="slideUp()" :lat-lng="L.latLng(cleanwalk.lat, cleanwalk.lng)">
<div v-for="cleanwalk in cleanwalkStore.cleanwalksTab">
<l-marker @click="slideUp(cleanwalk.id!)" :lat-lng="L.latLng(cleanwalk.pos_lat, cleanwalk.pos_long)">
<l-icon :icon-size="[25, 41]" :icon-anchor="[12, 41]"
:iconUrl="cleanwalk.isAsso ? 'https://i.ibb.co/XkyvQmm/blue-map.png' : 'https://i.ibb.co/zZjWfnp/green-map.png'">
:iconUrl="cleanwalk.host?.role_id === 1 ? 'https://firebasestorage.googleapis.com/v0/b/horrorfire-88d56.appspot.com/o/cw%2FGroup%20172.svg?alt=media&token=2b337af1-bed2-4491-834c-c2aeaf8be593' : 'https://firebasestorage.googleapis.com/v0/b/horrorfire-88d56.appspot.com/o/cw%2FGroup%20173.svg?alt=media&token=2c75133b-e73f-4cac-95fb-277e4516dfb7'">
</l-icon>
</l-marker>
</div>
</l-map>
</div>
<div class="top-bar">
<img src="../assets/logo.svg" alt="logo" v-if="!cardListBool">
<img class="logo" src="../assets/logo.svg" alt="logo" v-if="!cardListBool">
<div class="search-bar" :class="{ 'active': cardListBool, 'base': !cardListBool }">
<button @click="backButton()">
<iconLeftArrow />
</button>
<input @click="hideCleanwalkList()" name="search" type="text" placeholder="Rechercher une cleanwalk" v-model="searchInput" />
<input @click="hideCleanwalkList()" name="search" autocomplete="off" type="text" placeholder="Rechercher une cleanwalk" v-model="searchInput" />
<label for="search" @click="cardListBool = true">
<iconSearch />
</label>
</div>
<button class="info">
<RouterLink to="/menu/profile" class="pp" v-if="userImg">
<img :src="userImg" alt="user img">
</RouterLink>
<button class="info" v-else>
<iconInfo />
</button>
</div>
<div ref="draggableCard" class="draggable-card">
<div class="card-handle" @click="!cleanwalkStore.cleanwalkIsSelect && (cardListBool = true);">
<div class="card-handle" @click="!selectedCleanwalk && (cardListBool = true);">
<svg width="43" height="3" viewBox="0 0 43 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="43" height="3" rx="1.5" fill="#373646" />
</svg>
</div>
<div class="card-content" v-if="cleanwalkStore.cleanwalkIsSelect">
<h3>Je nettoie la Nature</h3>
<router-link to="/cleanwalk" class="flex-container">
<div class="card-content" v-if="selectedCleanwalk">
<h3>{{ selectedCleanwalk.name }}</h3>
<router-link :to="{name: 'cleanwalk', params:{id: selectedCleanwalk.id}}" class="flex-container">
<div class="left">
<div class="top">
<icon-clock />
<div>Date et heure de l’évènement</div>
<div>{{ dateHelper.getCleanwalkWrittenDate( new Date(selectedCleanwalk.date_begin), selectedCleanwalk.duration) }}</div>
</div>
<div class="bot">
<iconMiniMap />
<div>Localité, France</div>
<div>{{ selectedCleanwalk.address }}</div>
</div>
</div>
<div class="right">
<img src="../assets/defaultprofile.png" alt="profile_picture">
<div>username</div>
<img :src="selectedCleanwalk.host!.profile_picture" alt="profile_picture">
<div>{{ selectedCleanwalk.host!.name }}</div>
</div>
</router-link>
</div>
<div class="card-nb-cw" v-else>
<h3>10 cleanwalks à proximité</h3>
<h3>{{ getCleanwalkVisibleCount() }} cleanwalks à proximité </h3>
</div>
</div>
<div class="cleanwalk-list" :class="{ 'active': cardListBool === false }">
<div class="container" ref="cleanwalkListContainer">
<router-link to="/cleanwalk" v-for="cleanwalk in testCleanwalkList" :key="cleanwalk.id" class="cleanwalk">
<div class="title">{{ cleanwalk.title }}</div>
<div class="flex">
<icon-clock />
<div>Date et heure de l’évènement</div>
</div>
<div class="flex">
<iconMiniMap />
<div>Localité, France</div>
</div>
<router-link v-for="cleanwalk in filteredCleanwalks" :to="{name: 'cleanwalk', params:{id: cleanwalk.id}}" :key="cleanwalk.id" class="listContainer">
<cleanwalk-card :cleanwalk="cleanwalk" />
</router-link>
</div>


</div>
</main>
</template>
@@ -304,16 +255,29 @@ main {
z-index: 9998;
background-color: var(--color-primary);
display: flex;
padding: 30px 19px 20px;
padding: 20px 19px 20px;
justify-content: end;
img {
.logo {
position: absolute;
left: 31px;
width: 104px;
margin-top: 8px;
margin-top: 10px;
}
.pp {
border-radius: 999px;
width: 38px;
height: 38px;
margin-left: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
.info {
background-color: #fff;
border-radius: 8px;
@@ -330,6 +294,14 @@ main {
.search-bar {
&.base {
background-color: #fff;
border-radius: 8px;
border: 1px solid #CBD5E1;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
input {
opacity: 0;
@@ -345,14 +317,7 @@ main {
padding: 0 10px;
}
background-color: #fff;
border-radius: 8px;
border: 1px solid #CBD5E1;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
svg {
stroke: #94A3B8;
@@ -407,7 +372,8 @@ main {
.map-container {
height: 100vh;
padding: 78px 0 ;
height: calc(100vh - 20px);
}
.cleanwalk-list {
@@ -438,25 +404,11 @@ main {
gap: 10px;
overflow: auto;
.cleanwalk {
border: 2px solid rgb(155, 155, 155);
border-radius: 12px;
padding: 10px;
background-color: white;
width: 90%;
.title {
font-size: 16px;
font-style: normal;
font-weight: 500;
}
.flex {
display: flex;
align-items: center;
gap: 10px;
}
.listContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
}
293 changes: 293 additions & 0 deletions front/src/components/Profile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
<script setup lang="ts">
import { watch, ref, onMounted, type Ref } from 'vue';
const backgroundImageUrl = ref('https://cdn2.thecatapi.com/images/1nk.jpg');
import iconPhoto from '@/components/icons/icon-photo.vue';
import { useAccountStore } from '@/stores/AccountStore';
import router from '@/router';
import iconShuffleArrow from './icons/icon-shuffle-arrow.vue';
import { v4 as uuidv4 } from 'uuid';
import { useUtilsStore } from '@/stores/UtilsStore';
import type { Association } from '@/interfaces/userInterface';
const getToken = useAccountStore().getAccessToken;
const showToast = useUtilsStore().showToast;
const currentMdp = ref('');
const newMdp = ref('');
const confirmNewMdp = ref('');
const accountStore = useAccountStore();
const currentUser = ref(useAccountStore().CurrentUser);
const assocciation: Ref<Association | undefined> = ref(undefined);
const userName = ref(currentUser.value?.name);
let debounceTimeout: any;
onMounted(async () => {
if (!currentUser.value) {
router.push('/login');
return;
}
if (currentUser.value?.role === 'organisation') {
assocciation.value = await accountStore.getOrganisationById(currentUser.value.id!);
}
});
watch(() => userName.value, () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
if (userName.value === useAccountStore().CurrentUser?.name) {
return;
}
debounceTimeout = setTimeout(() => {
if (!userName.value || userName.value === '') {
debounceTimeout = undefined;
userName.value = currentUser.value?.name;
showToast('Veuillez entrer un nom valide', false);
return;
}
useAccountStore().modifyUser(currentUser.value!.id!, getToken()!, userName.value);
showToast('Votre nom a été modifié', true);
useAccountStore().CurrentUser!.name = userName.value;
debounceTimeout = undefined;
}, 2000);
});
const changePassword = async () => {
if (!currentMdp.value || !newMdp.value || !confirmNewMdp.value) {
showToast('Veuillez remplir tous les champs', false);
return;
}
if (newMdp.value !== confirmNewMdp.value) {
showToast('Les mots de passe ne correspondent pas', false);
return;
}
const response = await useAccountStore().changePassword(currentUser.value!.id!, getToken()!, currentMdp.value, newMdp.value);
if (response) {
showToast('Votre mot de passe a été modifié', true);
} else {
showToast('Mot de passe actuel incorrect', false);
}
currentMdp.value = '';
newMdp.value = '';
confirmNewMdp.value = '';
}
const changeUserPP = () => {
currentUser.value!.profile_picture = 'https://api.dicebear.com/8.x/fun-emoji/svg?seed=' + uuidv4();
// Clear the previous timeout if it exists
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
// Set a new timeout
debounceTimeout = setTimeout(() => {
useAccountStore().modifyUser(currentUser.value!.id!, getToken()!, undefined, currentUser.value!.profile_picture);
showToast('Votre photo de profil a été modifiée', true);
useAccountStore().CurrentUser!.profile_picture = currentUser.value!.profile_picture;
debounceTimeout = undefined; // Reset the timeout variable
}, 2000);
}
</script>
<template>
<section class="container">
<div v-if="currentUser?.role === 'organisation'" class="asso-imgs">
<img class="cover-img" src="https://cdn2.thecatapi.com/images/66l.jpg" alt="cover-img">
<div class="icon-photo cover">
<iconPhoto />
</div>
<img class="pp" src="https://cdn2.thecatapi.com/images/uk0SrrBbQ.jpg" alt="pp">
<div class="icon-photo pp-icon">
<iconPhoto />
</div>
</div>
<div class="img-user" v-if="currentUser?.role !== 'organisation'">
<img class="pp" :src="currentUser?.profile_picture" alt="cover-img">
<div @click="changeUserPP()" class="icon-shuffle-arrow">
<iconShuffleArrow />
</div>

</div>
<div class="content">
<h3>{{ currentUser?.email }}</h3>
<input class="input name" type="text" v-model="userName">
<textarea v-if="assocciation" name="text" id="text">
{{ assocciation?.description }}
</textarea>
<form @submit.prevent="changePassword()">
<label class="label" for="mdp">Mot de passe actuel</label>
<input v-model="currentMdp" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<label class="label" for="mdp">Nouveau mot de passe</label>
<input v-model="newMdp" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<label class="label" for="mdp">Confirmation du nouveau mot de passe</label>
<input v-model="confirmNewMdp" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<button class="action-button" type="submit">Changer votre mot de passe</button>
</form>
<button class="danger-button">Cloturer mon compte</button>
</div>
</section>

</template>
<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
.asso-imgs {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.img-user {
margin-top: 9rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.icon-shuffle-arrow {
width: 26px;
height: 26px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
padding-right: 1px;
padding-bottom: 1px;
margin-top: -25px;
z-index: 999;
margin-right: -60px;
stroke: #fff;
background-color: var(--color-primary);
transition: transform 0.3s ease-in-out;
&:active {
transform: rotate(-180deg);
transition: transform 0.2s ease-in-out;
}
}
}
.icon-photo {
background-color: var(--color-primary);
stroke: #fff;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
border-radius: 9999px;
width: 26px;
height: 26px;
&.cover {
position: absolute;
top: 85px;
right: 10px;
z-index: 999;
}
&.pp-icon {
position: relative;
margin-top: -25px;
z-index: 999;
margin-right: -60px;
}
}
.cover-img {
width: 100%;
aspect-ratio: 16/5;
object-fit: cover;
margin-top: 78px;
}
.pp {
width: 96px;
aspect-ratio: 1;
object-fit: cover;
border-radius: 9999px;
position: relative;
margin-top: -48px;
}
.content {
width: 100%;
padding: 1rem 2.45rem 0;
textarea {
height: 145px;
}
.input, textarea{
border: 1px solid #94A3B8;
border-radius: 8px;
padding: 12px;
margin-top: 0.5rem;
font-size: 14px;
font-style: normal;
font-weight: 500;
width: 100%;
&::placeholder {
color: #94A3B8;
}
&:focus {
outline: none;
}
&.name {
font-size: 18px;
font-style: normal;
font-weight: 500;
padding: 12px 0 8px 12px;
width: 100%;
}
}
form {
display: flex;
width: 100%;
flex-direction: column;
color: #94A3B8;
.label {
font-size: 12px;
font-weight: 500;
position: relative;
margin-bottom: -18px;
background-color: #fff;
width: fit-content;
margin-left: 13px;
margin-top: 5px;
}
.action-button {
margin-top: 1.5rem;
}
}
.danger-button {
margin-top: 3.5rem;
}
}
}
</style>
139 changes: 139 additions & 0 deletions front/src/components/SearchBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<script setup lang="ts">
import { ref, type Ref} from 'vue';
import iconInfo from './icons/icon-info.vue';
import iconLeftArrow from './icons/icon-left-arrow.vue';
import iconSearch from './icons/icon-search.vue';
import { tr } from 'date-fns/locale';
const inputActive = ref(true);
const searchInput = ref('');
</script>


<template>
<div class="top-bar">
<img src="../assets/logo.svg" alt="logo" v-if="!inputActive">
<div class="search-bar" :class="{ 'active': inputActive, 'base': !inputActive }">
<button @click="inputActive = false">
<iconLeftArrow />
</button>
<input @click="inputActive = true" name="search" type="text" placeholder="Rechercher une cleanwalk" v-model="searchInput" />
<label for="search" @click="inputActive = true">
<iconSearch />
</label>
</div>
<button class="info">
<iconInfo />
</button>
</div>
</template>

<style scoped lang="scss">
.top-bar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 9998;
background-color: var(--color-primary);
display: flex;
padding: 20px 19px 20px;
justify-content: end;
img {
position: absolute;
left: 31px;
width: 104px;
margin-top: 10px;
}
.info {
background-color: #fff;
border-radius: 8px;
border: 1px solid #CBD5E1;
width: 38px;
height: 38px;
margin-left: 8px;
svg {
stroke: #94A3B8;
}
}
.search-bar {
&.base {
background-color: #fff;
border-radius: 8px;
border: 1px solid #CBD5E1;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
input {
opacity: 0;
position: absolute;
width: 44px;
height: 44px;
}
button {
display: none;
}
label {
padding: 0 10px;
}
svg {
stroke: #94A3B8;
width: 24px;
height: 24px;
margin: 5px 1px 0 0;
}
}
&.active {
flex-grow: 1;
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
border-radius: 8px;
border: 1px solid #CBD5E1;
stroke: #CBD5E1;
button {
padding: 0px 5px 0px 10px;
background: none;
}
input {
width: 100%;
border: none;
outline: none;
font-size: 14px;
font-weight: 500;
color: #373646;
background-color: white;
padding: 10px 0;
}
label {
padding-top: 8px;
padding-right: 10px;
}
svg {
width: 20px;
height: 20px;
stroke: #94A3B8;
}
}
}
}
</style>
179 changes: 179 additions & 0 deletions front/src/components/Signup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<script setup lang="ts">
import { ref } from 'vue';
import Toast from './Toast.vue';
import apiHelper from '@/helpers/apiHelper';
import type { ApiResponse } from '@/interfaces/apiResponseInterface';
import { v4 as uuidv4 } from 'uuid';
import { useUtilsStore } from '@/stores/UtilsStore';
import router from '@/router';
const showToast = useUtilsStore().showToast;
const email = ref("");
const name = ref("");
const password = ref("");
const confirmPassword = ref("");
const signup = async ( ) => {
if(!name.value) {
showToast("Veuillez renseigner votre prénom", false);
return;
}
if(!email.value) {
showToast("Veuillez renseigner votre email", false);
return;
}
if(!password.value || !confirmPassword.value) {
showToast("Veuillez renseigner votre mot de passe", false);
return;
}
if(password.value !== confirmPassword.value) {
showToast("Les mots de passe ne correspondent pas", false);
return;
}
const response:ApiResponse = await apiHelper.kyPostWithoutToken( "/users", {
email: email.value,
password: password.value,
name: name.value,
profile_picture: 'https://api.dicebear.com/8.x/fun-emoji/svg?seed=' + uuidv4(),
role_id: 1
});
if(response.success === false) {
showToast(response.data.message as string, false);
return;
} else {
showToast("Votre compte a été créé avec succès", true);
setTimeout(() => {
router.push('/login').then(() => router.go(0));
}, 1000);
}
}
</script>

<template>
<Toast />
<section class="container">

<h1>
Bienvenue sur la plateforme Cleanwalk.org
</h1>

<!-- <GoogleLogin :callback="callback" />
<div class="or">
<div class="line"></div>
<span>ou</span>
<div class="line"></div>
</div> -->
<form @submit.prevent="signup()">
<label class="label" for="email">Comment voulez vous qu'on vous appelle ?</label>
<input v-model="name" class="input" name="name" type="text" placeholder="nom, prenom, pseudo ... .">
<label class="label" for="email">Email</label>
<input v-model="email" class="input" name="mdp" type="email" placeholder="user@domain.fr">
<label class="label" for="password">Mot de passe</label>
<input v-model="password" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<label class="label" for="password2">Mot de passe</label>
<input v-model="confirmPassword" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<button class="action-button" type="submit">S' inscrire</button>
<router-link to="/login" class="go-login">
Vous utilisez déjà cleanwalk.org : <span>Connectez-vous</span>
</router-link>
</form>
</section>
</template>

<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
padding: 0 2rem;
h1 {
font-size: 24px;
font-weight: 500;
margin-bottom: 1rem;
}
}
.go-login {
color: var(--text-color-secondary);
font-size: 12px;
width: 100%;
text-align: left;
margin-top: 2rem;
span {
color: var(--text-color-primary);
font-weight: 500;
cursor: pointer;
text-decoration: underline;
}
}
.or {
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
span {
margin: 0 1rem;
}
.line {
width: 35vw;
height: 1px;
background-color: #747474;
}
}
form {
margin-top: 3rem;
display: flex;
width: 100%;
flex-direction: column;
color: #94A3B8;
.label {
font-size: 12px;
font-weight: 500;
position: relative;
margin-bottom: -18px;
background-color: #fff;
width: fit-content;
margin-left: 13px;
margin-top: 5px;
}
.input {
border: 1px solid #94A3B8;
border-radius: 8px;
padding: 12px;
margin-top: 0.5rem;
font-size: 14px;
font-style: normal;
font-weight: 500;
&::placeholder {
color: #94A3B8;
}
&:focus {
outline: none;
}
}
.action-button {
margin-top: 1.5rem;
}
}
.danger-button {
margin-top: 3.5rem;
}
</style>
177 changes: 177 additions & 0 deletions front/src/components/SignupOrganisation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<script setup lang="ts">
import { ref } from 'vue';
import Toast from './Toast.vue';
import apiHelper from '@/helpers/apiHelper';
import type { ApiResponse } from '@/interfaces/apiResponseInterface';
import { v4 as uuidv4 } from 'uuid';
import { useUtilsStore } from '@/stores/UtilsStore';
import router from '@/router';
const showToast = useUtilsStore().showToast;
const email = ref("");
const name = ref("");
const password = ref("");
const confirmPassword = ref("");
const signup = async ( ) => {
if(!name.value) {
showToast("Veuillez renseigner votre prénom", false);
return;
}
if(!email.value) {
showToast("Veuillez renseigner votre email", false);
return;
}
if(!password.value || !confirmPassword.value) {
showToast("Veuillez renseigner votre mot de passe", false);
return;
}
if(password.value !== confirmPassword.value) {
showToast("Les mots de passe ne correspondent pas", false);
return;
}
const response:ApiResponse = await apiHelper.kyPostWithoutToken( "/users", {
email: email.value,
password: password.value,
name: name.value,
profile_picture: 'https://api.dicebear.com/8.x/fun-emoji/svg?seed=' + uuidv4(),
role_id: 2,
});
if(response.success === false) {
showToast(response.data.message as string, false);
return;
} else {
showToast("Votre compte a été créé avec succès", true);
setTimeout(() => {
router.push('/login').then(() => router.go(0));
}, 1000);
}
}
</script>

<template>
<Toast />
<section class="container">

<h1>
Bienvenue sur la plateforme Cleanwalk.org
</h1>
<!-- <GoogleLogin :callback="callback" />
<div class="or">
<div class="line"></div>
<span>ou</span>
<div class="line"></div>
</div> -->
<form @submit.prevent="signup()">
<label class="label" for="email">Nom de votre association/Organisation ?</label>
<input v-model="name" class="input" name="name" type="text" placeholder="Cleanwalk.org">
<label class="label" for="email">Email</label>
<input v-model="email" class="input" name="mdp" type="email" placeholder="user@domain.fr">
<label class="label" for="password">Mot de passe</label>
<input v-model="password" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<label class="label" for="password2">Mot de passe</label>
<input v-model="confirmPassword" class="input" name="mdp" type="password" placeholder="Votre mot de passe">
<button class="action-button" type="submit">S' inscrire</button>
<router-link to="/login" class="go-login">
Vous utilisez déjà cleanwalk.org : <span>Connectez-vous</span>
</router-link>
</form>
</section>
</template>

<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
padding: 0 2rem;
h1 {
font-size: 24px;
font-weight: 500;
margin-bottom: 1rem;
}
}
.go-login {
color: var(--text-color-secondary);
font-size: 12px;
width: 100%;
text-align: left;
margin-top: 2rem;
span {
color: var(--text-color-primary);
font-weight: 500;
cursor: pointer;
text-decoration: underline;
}
}
.or {
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
span {
margin: 0 1rem;
}
.line {
width: 35vw;
height: 1px;
background-color: #747474;
}
}
form {
margin-top: 3rem;
display: flex;
width: 100%;
flex-direction: column;
color: #94A3B8;
.label {
font-size: 12px;
font-weight: 500;
position: relative;
margin-bottom: -18px;
background-color: #fff;
width: fit-content;
margin-left: 13px;
margin-top: 5px;
}
.input {
border: 1px solid #94A3B8;
border-radius: 8px;
padding: 12px;
margin-top: 0.5rem;
font-size: 14px;
font-style: normal;
font-weight: 500;
&::placeholder {
color: #94A3B8;
}
&:focus {
outline: none;
}
}
.action-button {
margin-top: 1.5rem;
}
}
.danger-button {
margin-top: 3.5rem;
}
</style>
210 changes: 183 additions & 27 deletions front/src/components/SingleCleanwalk.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
<script setup lang="ts">
import type { Cleanwalk } from '@/interfaces/cleanwalkInterface'
import type { Cleanwalk, SingleCleanwalk } from '@/interfaces/cleanwalkInterface'
import iconClock from './icons/icon-clock.vue';
import iconMiniMap from './icons/icon-mini-map.vue';
import iconLeftArrow from './icons/icon-left-arrow.vue';
import iconInfo from './icons/icon-info.vue';
import iconCross from './icons/icon-cross.vue';
import iconAdd from './icons/icon-add.vue';
import iconMinus from './icons/icon-minus.vue';
import { ref } from 'vue';
const currentCleanwalk = {
id: 1,
title: 'Cleanwalk 2',
addess: 'Rue de la paix',
isAsso: false,
organizer: "Qui nettoie si ce n’est Toi "
import { useCleanwalkStore } from '@/stores/CleanwalkStore';
import { onMounted, ref, type Ref } from 'vue';
import { useRoute } from 'vue-router';
import router from '@/router';
import dateHelper from '@/helpers/dateHelper';
import { useAccountStore } from '@/stores/AccountStore';
import LeaveCwPopup from './LeaveCwPopup.vue';
const cleanwalkStore = useCleanwalkStore();
const currenUserId = ref(useAccountStore().CurrentUser?.id);
const token = ref(useAccountStore().getAccessToken());
let currentCleanwalk: Ref<SingleCleanwalk | undefined> = ref(undefined);
onMounted(async () => {
const id = +useRoute().params.id; // + to convert string to number
// id is NaN if it's not a number
if (isNaN(id)) {
router.push('/404');
return;
}
currentCleanwalk.value = await cleanwalkStore.getCleanwalkById(id, useAccountStore().CurrentUser?.id);
if (!currentCleanwalk.value) {
router.push('/404');
}
})
const showLeaveCwPopup = ref(false);
const toogleLeaveCwPopup = () => {
showLeaveCwPopup.value = !showLeaveCwPopup.value;
}
let popupBool = ref(false);
@@ -47,6 +73,69 @@ const cancel = () => {
isAnonyme.value = false;
}
const getDate = () => {
if (currentCleanwalk.value && currentCleanwalk.value.date_begin && currentCleanwalk.value.duration) {
return dateHelper.getCleanwalkWrittenDate(new Date(currentCleanwalk.value.date_begin), currentCleanwalk.value.duration);
}
}
const leaveCleanwalk = () => {
if (!currentCleanwalk.value || !currenUserId.value || !token.value) {
router.push('/login');
return;
}
cleanwalkStore.leaveCleanwalk(currentCleanwalk.value.id, token.value, currenUserId.value);
currentCleanwalk.value.is_user_participant = false;
toogleLeaveCwPopup();
}
const joinCleanwalk = () => {
if (!currentCleanwalk.value || !currenUserId.value || !token.value) {
router.push('/login');
return;
}
cleanwalkStore.joinCleanwalk(currentCleanwalk.value?.id, token.value, counterParticipate.value, currenUserId.value);
currentCleanwalk.value.is_user_participant = true;
tooglePopup();
}
const actionButton = () => {
if (!currentCleanwalk.value || !currenUserId.value || !token.value) {
router.push('/login');
return;
}
if (currentCleanwalk.value.host.author_id === currenUserId.value) {
// edit cleanwalk
return;
}
if (currentCleanwalk.value.is_user_participant === true) {
// leave cleanwalk
toogleLeaveCwPopup();
return;
}
if (currentCleanwalk.value.is_user_participant === false) {
// join cleanwalk
participate();
return;
}
}
const getActionButtonText = (): string => {
if (currentCleanwalk.value?.host.author_id === currenUserId.value) {
return "Editer la cleanwalk";
}
if (currentCleanwalk.value?.is_user_participant === true) {
return "Se désinscrire";
}
if (currentCleanwalk.value?.is_user_participant === false) {
return "Je participe";
}
return "";
}
</script>

<template>
@@ -59,7 +148,7 @@ const cancel = () => {
<iconInfo />
</button>
</div>

<LeaveCwPopup :isVisible="showLeaveCwPopup" :tooglePopup="toogleLeaveCwPopup" :leaveCw="leaveCleanwalk" />
<div class="popup" v-if="popupBool">
<div class="popup-validation">
<div class="cross-container">
@@ -84,7 +173,7 @@ const cancel = () => {
</div>
<div class="button-container">
<button @click="cancel()" class="cancel">Annuler</button>
<button class="button-primary">Valider</button>
<button @click="joinCleanwalk()" class="button-primary">Valider</button>
</div>
</div>

@@ -95,34 +184,49 @@ const cancel = () => {
<img class="cover" src="../assets/desert.png" alt="" />
</div>
<div class="container">
<h1>{{ currentCleanwalk.title }}</h1>
<h1>{{ currentCleanwalk?.name }}</h1>
<div class="date-location">
<div class="top">
<icon-clock />
<div>Date et heure de l’évènement</div>
<div>{{ getDate() }}</div>
</div>
<div class="bot">
<iconMiniMap />
<div>Localité, France</div>
<div>{{ currentCleanwalk?.address }}</div>
</div>
</div>
<div class="map-links">
<!-- Lien vers Google Maps -->
<a :href="`https://www.google.com/maps/?q=${currentCleanwalk?.pos_lat},${currentCleanwalk?.pos_long}`"
target="_blank">
<img src="../assets/googleMap.svg" alt="google map logo">
<h4>Ouvrir dans Google Maps</h4>
</a>

<!-- Lien vers OpenStreetMap -->
<a :href="`https://www.openstreetmap.org/?mlat=${currentCleanwalk?.pos_lat}&mlon=${currentCleanwalk?.pos_long}`"
target="_blank">
<img src="../assets/Openstreetmap_logo.png" alt="google map logo">
<h4>Ouvrir dans OpenStreetMap</h4>
</a>
</div>
<div v-if="currentCleanwalk?.host.author_id === currenUserId">
{{ currentCleanwalk?.participant_count }} participant(s)
</div>
<div class="orga">
<div class="left">
<div>organisé par:</div>
<h2> {{ currentCleanwalk.organizer }} </h2>
<h2> {{ currentCleanwalk?.host?.name }} </h2>
</div>
<div class="right">
<img src="../assets/defaultprofile.png" alt="profile-picture">
<div class="right" v-if="currentCleanwalk?.host?.profile_picture">
<img :src="currentCleanwalk.host.profile_picture" alt="profile-picture">
</div>
</div>
<button class="button-primary" @click="participate()">
Je participe
<button class="button-primary" @click="actionButton()">
{{ getActionButtonText() }}
</button>
<p>
lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam cursus vestibulum interdum. Phasellus libero
nibh, tincidunt sed massa dictum, feugiat interdum mi. Pellentesque finibus cursus quam ut efficitur. Integer
lobortis tortor velit, at suscipit justo scelerisque et. Integer lobortis tortor velit, at suscipit justo
scelerisque et. Integer lobortis tortor velit, at suscipit justo
<p class="description">
{{ currentCleanwalk?.description }}
</p>
</div>

@@ -189,6 +293,13 @@ main {
padding: 0 26px;
font-size: 12px;
.description {
width: 100%;
//le texte ne dois pas depasser et sauter une ligne si besoins
overflow: hidden;
word-wrap: break-word;
}
h1 {
color: var(--text-color-primary);
font-size: 20px;
@@ -220,6 +331,37 @@ main {
}
}
.map-links {
display: flex;
justify-content: space-between;
border: dashed 2px #0cab2e;
padding: 10px;
margin-bottom: 20px;
border-radius: 8px;
a {
display: flex;
align-items: center;
gap: 10px;
img {
width: 30px;
height: 30px;
}
h4 {
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0em;
text-align: center;
text-align: left;
max-width: 6rem;
}
}
}
.orga {
display: flex;
background-color: var(--color-secondary);
@@ -241,6 +383,7 @@ main {
.right {
img {
border-radius: 9999px;
width: 44px;
height: 44px;
object-fit: cover;
@@ -265,6 +408,7 @@ main {
}
}
.popup {
position: fixed;
top: 0;
@@ -283,11 +427,13 @@ main {
overflow: hidden;
width: 90%;
padding: 0 10%;
.cross-container {
display: flex;
justify-content: flex-end;
position: relative;
margin-right: -10%; // to compensate padding :( sorry
.cross {
background-color: transparent;
stroke: var(--text-color-primary);
@@ -300,6 +446,7 @@ main {
padding: 40px 0 10px;
font-size: 12px;
}
h3 {
// styles for h3
font-size: 18px;
@@ -321,15 +468,18 @@ main {
height: 50px;
stroke: #fff;
padding-top: 4px;
&.add {
padding-top: 4px;
svg {
width: 32px;
height: 32px;
}
}
}
div {
// styles for counter div
font-size: 25px;
@@ -344,34 +494,41 @@ main {
}
}
.anonyme {
display: flex;
padding-top: 20px;
visibility: hidden; //provisoire
padding: 0;
input[type="checkbox"] {
// styles for checkbox
margin-right: 5px
}
label {
display: block;
font-size: 12px;
padding-top: 2px;
}
}
.button-container {
display: flex;
padding: 20px 0;
button {
font-weight: 500;
padding: 15px 0;
border-radius: 8px;
&.cancel {
// styles for cancel button
flex-grow: 0.25;
margin-right: 10px;
font-size: 16px;
}
&.button-primary {
// styles for primary button
flex-grow: 0.75;
@@ -380,5 +537,4 @@ main {
}
}
}
</style>
</style>
59 changes: 59 additions & 0 deletions front/src/components/SwithChoice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
const props = defineProps({
categorie1: String,
categorie2: String,
route1: String,
route2: String,
activeCategory: Boolean,
});
// cause router-link doesn't have a scrollToTop method
const scrollToTop = () => {
window.scrollTo(0, 0);
};
</script>

<template>
<div class="container">
<router-link class="router-link" @click="scrollToTop" :to="'/' + route1" :class="{ active: activeCategory === true }">
{{ categorie1 }}
</router-link>
<router-link class="router-link" @click="scrollToTop" :to="'/' + route2" :class="{ active: activeCategory === false }">
{{ categorie2 }}
</router-link>
</div>
</template>

<style scoped lang="scss">
.container {
position: fixed;
z-index: 888;
width: 100%;
left: 0;
top: 78px;
display: flex;
box-shadow: 0px 10px 100px rgba(194, 194, 194, 0.2);
.router-link {
color: var(--color-primary);
font-size: 14px;
font-weight: 500;
width: 50%;
text-align: center;
padding: 13px 0 12px 0;
background-color: #fff;
&::first-letter {
text-transform: uppercase;
}
}
.active {
background-color: #E1F4F8;
border-bottom: 2px solid var(--color-primary);
}
}
</style>
44 changes: 44 additions & 0 deletions front/src/components/Toast.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useUtilsStore } from '@/stores/UtilsStore';
const toast = useUtilsStore().toast;
</script>

<template>
<div class="toast" :class="{ 'toast-success': toast.isSuccess, 'toast-error': !toast.isSuccess, 'is-visible': toast.isVisible }">
<p>{{ toast.message }}</p>
</div>
</template>

<style scoped lang="scss">
.toast {
opacity: 0;
position: fixed;
transform: translateY(-100%);
top: 0;
right: 0;
padding: 5px 20px;
border-radius: 0 0 0 8px;
color: #fff;
font-weight: bold;
z-index: 1000;
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; // Réduisez la durée et ajoutez opacity
pointer-events: none;
&.is-visible {
transform: translateY(0);
opacity: 1;
}
&.toast-success {
background-color: #3ee243;
}
&.toast-error {
background-color: #f44336;
}
}
</style>
45 changes: 22 additions & 23 deletions front/src/components/TopBar.vue
Original file line number Diff line number Diff line change
@@ -2,25 +2,25 @@
import iconLeftArrow from './icons/icon-left-arrow.vue';
import iconInfo from './icons/icon-info.vue';
const props = defineProps({
backUrl: String,
pageName: String
})
</script>

<template>
<div class="top-bar">
<router-link class="back" to="/">
<router-link class="back" :to="backUrl" v-if="backUrl">
<iconLeftArrow />
</router-link>
<img src="../assets/logo.svg" alt="logo">
<button class="info">
<iconInfo />
</button>
<h2>{{ pageName }}</h2>
</div>
</template>

<style scoped lang="scss">
@import '@/assets/base.scss';
.top-bar {
position: sticky;
position: fixed;
top: 0;
left: 0;
width: 100vw;
@@ -31,30 +31,29 @@ import iconInfo from './icons/icon-info.vue';
padding: 20px;
flex-direction: center;
justify-content: space-between;
height: 78px;
.back {
display: flex;
flex-direction: column;
justify-content: end;
padding-bottom: 5px;
position: absolute;
top: 34px;
left: 20px;
svg {
width: 24px;
height: 24px;
}
}
img {
width: 104px;
margin-top: 10px;
h2 {
color: #FFF;
font-size: 18px;
font-style: normal;
font-weight: 700;
padding-top: 1rem;
text-align: center;
width: 100%;
}
.info {
background-color: #fff;
border-radius: 8px;
border: 1px solid #CBD5E1;
width: 38px;
height: 38px;
stroke: #94A3B8;
}
}
</style>
51 changes: 51 additions & 0 deletions front/src/components/cards/CleanwalkListCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import iconMiniMap from '../icons/icon-mini-map.vue';
import iconClock from '../icons/icon-clock.vue';
import type { Cleanwalk } from '@/interfaces/cleanwalkInterface';
import dateHelper from '@/helpers/dateHelper';
//define props
const props = defineProps<{
cleanwalk: Cleanwalk
}>()
</script>

<template>
<div class="cleanwalk">
<div class="title">{{ cleanwalk.name }}</div>
<div class="flex">
<icon-clock />
<div>{{ dateHelper.getCleanwalkWrittenDate(new Date(cleanwalk.date_begin), cleanwalk.duration) }}</div>
</div>
<div class="flex">
<iconMiniMap />
<div>{{ cleanwalk.address }}</div>
</div>
</div>
</template>

<style lang="scss" scoped>
.cleanwalk {
border: 2px solid rgb(155, 155, 155);
border-radius: 12px;
padding: 10px;
background-color: white;
width: 90%;
.title {
font-size: 16px;
font-style: normal;
font-weight: 500;
}
.flex {
display: flex;
align-items: center;
gap: 10px;
}
}
</style>
166 changes: 166 additions & 0 deletions front/src/components/dragDrop.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<template>
<div class="container">
<button v-if="imageSrc && !props.autoUpload" @click.stop="removeImage()" class="cross">X</button>
<div class="drop-area" @dragover.prevent="dragOver" @dragleave.prevent="dragLeave" @drop.prevent="onFileDrop"
@click="fileInputClick" :class="{ 'dragover': isDragOver }">
<input type="file" ref="fileInput" @change="onFileChange" accept="image/jpeg, image/png" style="display: none" />
<icon-photo class="icon-photo" v-if="!imageSrc" />

<img v-if="imageSrc" :src="imageSrc" alt="Preview" class="preview" />
</div>
</div>

</template>

<script lang="ts" setup>
import { ref, type Ref } from 'vue';
import iconPhoto from './icons/icon-photo.vue';
import { useAccountStore } from '@/stores/AccountStore';
import apiHelper from '@/helpers/apiHelper';
import type { ApiResponse } from '@/interfaces/apiResponseInterface';
import { useUtilsStore } from '@/stores/UtilsStore';
const showToast = useUtilsStore().showToast;
const props = defineProps({
format: {
validator: (value: string) => ['card', 'full', 'circle'].includes(value),
default: 'card',
},
autoUpload: {
type: Boolean,
default: false,
}
});
const accountStore = useAccountStore()
const fileInput: Ref<HTMLInputElement | null> = ref(null);
const imageSrc: Ref<string | null> = ref(null);
const isDragOver = ref(false);
const dragOver = () => {
isDragOver.value = true;
};
const dragLeave = () => {
isDragOver.value = false;
};
const onFileDrop = (event: DragEvent): void => {
const files = event.dataTransfer?.files;
if (files) {
processFile(files[0]);
}
};
const fileInputClick = () => {
fileInput.value?.click();
};
const onFileChange = (): void => {
if (fileInput.value?.files && fileInput.value.files.length > 0) {
const file = fileInput.value.files[0];
processFile(file);
if (props.autoUpload) {
handleUpload();
}
}
};
const processFile = (file: File): void => {
if (file && (file.type === "image/jpeg" || file.type === "image/png")) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target?.result) {
imageSrc.value = e.target.result as string;
}
};
reader.readAsDataURL(file);
} else {
alert("Seules les images de type JPEG ou PNG sont autorisées.");
}
};
const handleUpload = async (): Promise<string | undefined> => {
if (fileInput.value?.files?.length && fileInput.value) {
// Utilisez votre fonction d'aide pour uploader l'image
const token = accountStore.getAccessToken();
const Response: ApiResponse = await apiHelper.uploadFile(fileInput.value.files[0], token!);
if (Response.success) {
// showToast("Image uploaded successfully", true);
removeImage();
} else {
showToast(Response.data.message as string, false);
}
return Response.data.img_url as string; //img name is in Response.data.filename
}
showToast("No file selected", false);
removeImage();
return undefined;
};
const removeImage = () => {
imageSrc.value = null;
fileInput.value!.value = '';
};
defineExpose({ handleUpload });
</script>

<style scoped lang="scss">
.container {
width: 100%;
display: flex;
flex-direction: column;
align-items: end;
.cross {
position: absolute;
margin-right: -10px;
margin-top: -10px;
background-color: #000000;
color: #ffffff;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
z-index: 1;
}
}
.drop-area {
width: 100%;
text-align: center;
cursor: pointer;
aspect-ratio: 16/9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #E1F4F8;
border-radius: 8px;
overflow: hidden;
}
.dragover {
background-color: #E1F4F8;
}
.icon-photo {
width: 50px;
height: 50px;
stroke: #000000;
}
.preview {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
}
</style>
5 changes: 0 additions & 5 deletions front/src/components/icons/icon-article.vue

This file was deleted.

6 changes: 6 additions & 0 deletions front/src/components/icons/icon-back-time.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 10C1 11.78 1.52784 13.5201 2.51677 15.0001C3.50571 16.4802 4.91131 17.6337 6.55585 18.3149C8.20038 18.9961 10.01 19.1743 11.7558 18.8271C13.5016 18.4798 15.1053 17.6226 16.364 16.364C17.6226 15.1053 18.4798 13.5016 18.8271 11.7558C19.1743 10.01 18.9961 8.20038 18.3149 6.55585C17.6337 4.91131 16.4802 3.50571 15.0001 2.51677C13.5201 1.52784 11.78 1 10 1C7.48395 1.00947 5.06897 1.99122 3.26 3.74L1 6M1 6V1M1 6H6M10 5V10L14 12" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

</template>
6 changes: 6 additions & 0 deletions front/src/components/icons/icon-burger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg width="18" height="24" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7H17M1 1H17M1 13H17" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

</template>
6 changes: 6 additions & 0 deletions front/src/components/icons/icon-calendar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 1V5M6 1V5M1 9H19M6 13H6.01M10 13H10.01M14 13H14.01M6 17H6.01M10 17H10.01M14 17H14.01M3 3H17C18.1046 3 19 3.89543 19 5V19C19 20.1046 18.1046 21 17 21H3C1.89543 21 1 20.1046 1 19V5C1 3.89543 1.89543 3 3 3Z" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

</template>
6 changes: 6 additions & 0 deletions front/src/components/icons/icon-discover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12Z" stroke="inerith" stroke-width="2"/>
<path d="M10.1805 13.8195C10.2008 13.8398 10.2212 13.8596 10.2419 13.8787L10.0208 13.9792L10.1213 13.7581C10.1404 13.7788 10.1602 13.7992 10.1805 13.8195ZM13.8787 10.2419C13.8596 10.2212 13.8398 10.2008 13.8195 10.1805C13.7992 10.1602 13.7788 10.1404 13.7581 10.1213L13.9792 10.0208L13.8787 10.2419Z" stroke="inerith" stroke-width="3"/>
</svg>
</template>
5 changes: 5 additions & 0 deletions front/src/components/icons/icon-file.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1V7H17M11.5 1H3C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3V19C1 19.5304 1.21071 20.0391 1.58579 20.4142C1.96086 20.7893 2.46957 21 3 21H15C15.5304 21 16.0391 20.7893 16.4142 20.4142C16.7893 20.0391 17 19.5304 17 19V6.5L11.5 1Z" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
6 changes: 6 additions & 0 deletions front/src/components/icons/icon-logout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H9M16 17L21 12M21 12L16 7M21 12H9" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

</template>
5 changes: 5 additions & 0 deletions front/src/components/icons/icon-paper-plane.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 1L14 21L10 12M21 1L1 8L10 12M21 1L10 12" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
7 changes: 7 additions & 0 deletions front/src/components/icons/icon-photo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 4H9.5L7 7H4C3.46957 7 2.96086 7.21071 2.58579 7.58579C2.21071 7.96086 2 8.46957 2 9V18C2 18.5304 2.21071 19.0391 2.58579 19.4142C2.96086 19.7893 3.46957 20 4 20H20C20.5304 20 21.0391 19.7893 21.4142 19.4142C21.7893 19.0391 22 18.5304 22 18V9C22 8.46957 21.7893 7.96086 21.4142 7.58579C21.0391 7.21071 20.5304 7 20 7H17L14.5 4Z" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 16C13.6569 16 15 14.6569 15 13C15 11.3431 13.6569 10 12 10C10.3431 10 9 11.3431 9 13C9 14.6569 10.3431 16 12 16Z" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

</template>
7 changes: 7 additions & 0 deletions front/src/components/icons/icon-right-arrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12L10 8L6 4" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>


</template>
6 changes: 6 additions & 0 deletions front/src/components/icons/icon-settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.22 1H10.78C10.2496 1 9.74086 1.21071 9.36579 1.58579C8.99072 1.96086 8.78 2.46957 8.78 3V3.18C8.77964 3.53073 8.68706 3.87519 8.51154 4.17884C8.33602 4.48248 8.08374 4.73464 7.78 4.91L7.35 5.16C7.04596 5.33554 6.70108 5.42795 6.35 5.42795C5.99893 5.42795 5.65404 5.33554 5.35 5.16L5.2 5.08C4.74107 4.81526 4.19584 4.74344 3.684 4.88031C3.17217 5.01717 2.73555 5.35154 2.47 5.81L2.25 6.19C1.98526 6.64893 1.91345 7.19416 2.05031 7.706C2.18717 8.21783 2.52154 8.65445 2.98 8.92L3.13 9.02C3.43228 9.19451 3.68362 9.44509 3.85905 9.74683C4.03448 10.0486 4.1279 10.391 4.13 10.74V11.25C4.1314 11.6024 4.03965 11.949 3.86405 12.2545C3.68844 12.5601 3.43521 12.8138 3.13 12.99L2.98 13.08C2.52154 13.3456 2.18717 13.7822 2.05031 14.294C1.91345 14.8058 1.98526 15.3511 2.25 15.81L2.47 16.19C2.73555 16.6485 3.17217 16.9828 3.684 17.1197C4.19584 17.2566 4.74107 17.1847 5.2 16.92L5.35 16.84C5.65404 16.6645 5.99893 16.5721 6.35 16.5721C6.70108 16.5721 7.04596 16.6645 7.35 16.84L7.78 17.09C8.08374 17.2654 8.33602 17.5175 8.51154 17.8212C8.68706 18.1248 8.77964 18.4693 8.78 18.82V19C8.78 19.5304 8.99072 20.0391 9.36579 20.4142C9.74086 20.7893 10.2496 21 10.78 21H11.22C11.7504 21 12.2591 20.7893 12.6342 20.4142C13.0093 20.0391 13.22 19.5304 13.22 19V18.82C13.2204 18.4693 13.3129 18.1248 13.4885 17.8212C13.664 17.5175 13.9163 17.2654 14.22 17.09L14.65 16.84C14.954 16.6645 15.2989 16.5721 15.65 16.5721C16.0011 16.5721 16.346 16.6645 16.65 16.84L16.8 16.92C17.2589 17.1847 17.8042 17.2566 18.316 17.1197C18.8278 16.9828 19.2645 16.6485 19.53 16.19L19.75 15.8C20.0147 15.3411 20.0866 14.7958 19.9497 14.284C19.8128 13.7722 19.4785 13.3356 19.02 13.07L18.87 12.99C18.5648 12.8138 18.3116 12.5601 18.136 12.2545C17.9604 11.949 17.8686 11.6024 17.87 11.25V10.75C17.8686 10.3976 17.9604 10.051 18.136 9.74549C18.3116 9.43994 18.5648 9.18621 18.87 9.01L19.02 8.92C19.4785 8.65445 19.8128 8.21783 19.9497 7.706C20.0866 7.19416 20.0147 6.64893 19.75 6.19L19.53 5.81C19.2645 5.35154 18.8278 5.01717 18.316 4.88031C17.8042 4.74344 17.2589 4.81526 16.8 5.08L16.65 5.16C16.346 5.33554 16.0011 5.42795 15.65 5.42795C15.2989 5.42795 14.954 5.33554 14.65 5.16L14.22 4.91C13.9163 4.73464 13.664 4.48248 13.4885 4.17884C13.3129 3.87519 13.2204 3.53073 13.22 3.18V3C13.22 2.46957 13.0093 1.96086 12.6342 1.58579C12.2591 1.21071 11.7504 1 11.22 1Z" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 14C12.6569 14 14 12.6569 14 11C14 9.34315 12.6569 8 11 8C9.34315 8 8 9.34315 8 11C8 12.6569 9.34315 14 11 14Z" stroke="inerith" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
8 changes: 8 additions & 0 deletions front/src/components/icons/icon-shuffle-arrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<svg width="15" height="21" viewBox="0 0 15 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.84817 6.6766C-1 11.5 2.59063 17.4246 8.3182 17.0711H10.36M10.36 17.0711L7.96464 14.0659M10.36 17.0711L8 19.8995M12.9758 14.7701C16.159 10.1608 12.9976 3.99644 7.25939 3.94284L5.22276 3.79801M5.22276 3.79801L7.39889 6.96555M5.22276 3.79801L7.77741 1.1441"
stroke="inerith" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

</template>
40 changes: 25 additions & 15 deletions front/src/components/navBar.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
<script setup lang="ts">
import iconAdd from '@/components/icons/icon-add.vue';
import iconArticle from '@/components/icons/icon-article.vue';
import iconDiscover from '@/components/icons/icon-discover.vue';
import iconMap from '@/components/icons/icon-map.vue';
import iconProfile from '@/components/icons/icon-profile.vue';
import iconBurger from '@/components/icons/icon-burger.vue';
// import { useAccountStore } from '@/stores/AccountStore';
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const currentPage = ref('');
const route = useRoute();
onMounted(() => {
currentPage.value = route.name as string;
console.log('mounted', currentPage.value);
});
@@ -27,22 +26,23 @@ onMounted(() => {
<div>Carte</div>
</router-link>
</li>
<li :class="{ 'active': currentPage === 'add'}">
<li :class="{ 'active': currentPage.includes('add')}">
<router-link to="/add" class="redirect">
<iconAdd />
<div>Ajouter</div>
</router-link>
</li>
<li :class="{ 'active': currentPage === 'article'}">
<router-link to="/article" class="redirect">
<iconArticle />
<div>Articles</div>
<li :class="{ 'active': currentPage === 'articles' || currentPage === 'associations'}">
<router-link to="/articles" class="redirect">
<iconDiscover />
<div>Découvrir</div>
</router-link>
</li>
<li :class="{ 'active': currentPage === 'profile'}" >
<router-link to='profile' class="redirect">
<iconProfile />
<div>Profile</div>
<li :class="{ 'active': currentPage.includes('menu')}" >
<router-link to='/menu' class="redirect">
<iconBurger />
<!-- <img v-else :src="currenUserProfilePicture" class="pp" alt="profile picture" /> -->
<div>Menu</div>
</router-link>
</li>
</ul>
@@ -60,6 +60,7 @@ onMounted(() => {
font-weight: 500;
z-index: 999;
background-color: var(--color-nav-bg);
font-family: "Roboto", sans-serif;
.container {
display: flex;
@@ -78,6 +79,7 @@ onMounted(() => {
justify-content: center;
flex-direction: column;
color: var(--text-color-nav);
width: 100%;
.redirect {
@@ -91,11 +93,19 @@ onMounted(() => {
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.pp {
width: 26px;
height: 26px;
border-radius: 999px;
border: 2px solid #111;
}
}
&.active {
stroke: var(--text-color-nav-active);
fill: var(--text-color-nav-active);
color: var(--text-color-nav-active);
.redirect {
78 changes: 57 additions & 21 deletions front/src/helpers/apiHelper.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,127 @@

import ky from 'ky';
import { HTTPError } from 'ky';

// const apiUrl = '/api'; // This is the proxy defined in vite.config.ts
const apiUrl = import.meta.env.VITE_API_URL;

const apiUrl = '/api'; // Proxi in vite.config
import type { ApiResponse } from '@/interfaces/apiResponseInterface';

const kyGet = async (route: string) => {
const kyGet = async (route: string):Promise<ApiResponse> => {
try {
const response = await ky.get(apiUrl + route, {
const response:Record<string, unknown> = await ky.get(apiUrl + route, {
headers: {
'X-API-Key': import.meta.env.VITE_API_KEY,
},
}).json();

return response;
return { success: true, data: response };
} catch (error) {
console.error('Error fetching data:', error);
return undefined;
return { success: false, data: { message: 'An unknown error occurred' } };
}
};

const kyPost = async (route: string, data: any, access_token: string) => {
const kyPost = async (route: string, data: Record<string, unknown>, access_token: string) => {
try {
const response = await ky.post(apiUrl + route, {
const response:Record<string, unknown> = await ky.post(apiUrl + route, {
json: data,
headers: {
'X-API-Key': import.meta.env.VITE_API_KEY,
'Authorization': 'Bearer ' + access_token,
},
}).json();

return response;
return { success: true, data: response };
} catch (error) {
console.error('Error sending data:', error);
return undefined;
if (error instanceof HTTPError) {
// You can get the error response body as JSON
const errorData: Record<string, unknown> = await error.response.json();
return { success: false, data: errorData };
}
return { success: false, data: { message: 'An unknown error occurred' } };
}
};

const kyPostWithoutToken = async (route: string, data: any) => {
const kyPostWithoutToken = async (route: string, data: Record<string, unknown>): Promise<ApiResponse> => {
try {
const response = await ky.post(apiUrl + route, {
const response: Record<string, unknown> = await ky.post(apiUrl + route, {
json: data,
headers: {
'X-API-Key': import.meta.env.VITE_API_KEY,
},
}).json();

return response;
return { success: true, data: response };
} catch (error) {
console.error('Error sending data:', error);
return undefined;
if (error instanceof HTTPError) {
// You can get the error response body as JSON
const errorData: Record<string, unknown> = await error.response.json();
return { success: false, data: errorData };
}
return { success: false, data: { message: 'An unknown error occurred' } };
}
};

const kyPut = async (route: string, data: any, access_token:string) => {
const kyPut = async (route: string, data:Record<string, unknown>, access_token:string):Promise<ApiResponse> => {
try {
const response = await ky.put(apiUrl + route, {
const response:Record<string, unknown> = await ky.put(apiUrl + route, {
json: data,
headers: {
'X-API-Key': import.meta.env.VITE_API_KEY,
'Authorization': 'Bearer ' + access_token,
},
}).json();
return response;
return { success: true, data: response };
} catch (error) {
console.error('Error updating data:', error);
return undefined;
return { success: false, data: { message: 'An unknown error occurred' } };
}
};

const kyDelete = async (route: string, access_token: string) => {
const kyDelete = async (route: string, data:Record<string, unknown>, access_token: string):Promise<ApiResponse> => {
try {
const response = await ky.delete(apiUrl + route, {
const response:Record<string, unknown> = await ky.delete(apiUrl + route, {
json: data,
headers: {
'X-API-Key': import.meta.env.VITE_API_KEY,
'Authorization': 'Bearer ' + access_token,
},
}).json();

return response;
return { success: true, data: response };
} catch (error) {
console.error('Error deleting data:', error);
return undefined;
return { success: false, data: { message: 'An unknown error occurred' } };
}
};

async function uploadFile(file: File, token: string): Promise<ApiResponse> {
const formData = new FormData();
formData.append('file', file);

try {
const response:Record<string, unknown> = await ky.post(apiUrl + '/upload', {
body: formData,
headers: {
'X-API-Key': import.meta.env.VITE_API_KEY,
'Authorization': 'Bearer ' + token,
},
}).json();

return { success: true, data: response };
} catch (error) {
console.error('Error uploading file:', error);
return { success: false, data: { message: 'An unknown error occurred' } };
}
}
export default {
kyGet,
kyPost,
kyPut,
kyDelete,
kyPostWithoutToken,
uploadFile
};
3 changes: 2 additions & 1 deletion front/src/helpers/dateHelper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { format, add } from 'date-fns';
import { fr } from 'date-fns/locale';

const getCleanwalkWrittenDate = (startDate: Date, duration: number): string => {
const endDate = add(startDate, { minutes: duration });

const writtenDate = format(startDate, 'EEEE dd MMM yyyy');
const writtenDate = format(startDate, 'EEEE dd MMM yyyy', { locale: fr });
const isSameYear = startDate.getFullYear() === new Date().getFullYear();

const startDateString = format(startDate, isSameYear ? 'HH:mm' : 'dd MMM yyyy HH:mm');
51 changes: 44 additions & 7 deletions front/src/helpers/nominatimHelper.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import ky from 'ky';
import type { Coordinate } from '@/interfaces/coordinateInterface'
import { add } from 'date-fns';
import { fi } from 'date-fns/locale';

const nominatimUrl = 'http://localhost:5173/nominatim';
//const nominatimUrl = 'http://localhost:5173/nominatim';
const nominatimUrl = 'https://nominatim.openstreetmap.org';

// countries in which nominatim searches
const countryCodesArray = ['fr', 'be'];

// the language nominatim returns for the places names
const accept_language = ['fr'];

function formatAddressFromJson(addressData: any): string {
// On tente d'extraire les informations pertinentes en considérant différents champs possibles
const houseNumber = addressData.house_number || '';
const road = addressData.road || '';
// On ajoute les nouveaux champs pour plus de flexibilité
const city = addressData.city || '';
const municipality = addressData.municipality || addressData.village || '';
const village = addressData.village || '';
const highway = addressData.highway || '';
const square = addressData.square || '';

// On choisit le niveau de détail le plus spécifique disponible pour la localité
const localDetail = city || village || municipality || '';

// On construit une liste avec les éléments non vides
const parts = [houseNumber, road, highway, square, localDetail].filter(part => part !== '');

// On joint les parties avec un espace pour former une adresse complète
return parts.join(' ').trim();
}

const nominatimRequest = async (url: string, params: Record<string, any>) => {
// the output format
params.format = 'json';
@@ -23,13 +48,11 @@ const nominatimRequest = async (url: string, params: Record<string, any>) => {
const searchParams = new URLSearchParams(params as Record<string, string>);
const fullUrl = `${url}?${searchParams.toString()}`;

console.log(fullUrl);

try {
const res = await ky.get(fullUrl).json<any>();
return res;
} catch (error) {
console.log('Nominatim error: ' + error);
console.error('Nominatim error: ' + error);
return undefined;
}
};
@@ -39,16 +62,30 @@ const nominatimRequest = async (url: string, params: Record<string, any>) => {
* @param searchString The place to look for (state, city, road...)
* @returns The data of the response
*/
const nominatimSearch = async (searchString: string) => {
const nominatimSearch = async (searchString: string): Promise<Coordinate | undefined> => {
const params = {
q: searchString,
countryCodesArray: countryCodesArray,
addressdetails: 1,
accept_language: accept_language,
};

const url = `${nominatimUrl}/search`;
const result = await nominatimRequest(url, params);

if (result) {
const firstResult = result[0];
if (firstResult) {
return {
pos_lat: firstResult.lat,
pos_long: firstResult.lon,
address: formatAddressFromJson(firstResult.address),
city: firstResult.address.city ?? firstResult.address.village ?? firstResult.address.town ?? firstResult.address.municipality,
};
}
}

return nominatimRequest(url, params);
return undefined;
};

/**
@@ -79,7 +116,7 @@ const nominatimReverse = async (lat: number, lon: number) => {
const nominatimReverseWrittenAddress = async (lat: number, lon: number) => {
let location = '';
const result = await nominatimReverse(lat, lon).catch((error) => {
console.log(error);
console.error(error);
});

if (result == null) {
4 changes: 4 additions & 0 deletions front/src/interfaces/apiResponseInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ApiResponse {
success: boolean;
data: Record<string, unknown>;
}
9 changes: 6 additions & 3 deletions front/src/interfaces/articleInterface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export interface Article {
title: string,
content: string,
}
id?: number;
content: string;
title: string;
imageUrl: string;
createdAt: String;
};
8 changes: 8 additions & 0 deletions front/src/interfaces/assoInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Asso {
id?: number,
title: string,
description: string,
imageUrl: string,
createdAt?: String,
coverImageUrl?: string,
};
49 changes: 48 additions & 1 deletion front/src/interfaces/cleanwalkInterface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,56 @@
export interface Cleanwalk {
id: number,
id?: number,
name: string,
description: string,
date_begin: Date,
duration: number,
pos_lat: number,
pos_long: number,
address: string,
img_url?: string,
city?: string,
host? : {
id?: number,
name: string,
profile_picture: string,
role_id: number,
}
}

export interface CleanwalkCreation {
name: string,
description: string,
date_begin: string,
duration: number,
pos_lat: number,
pos_long: number,
address: string,
img_url?: string,
user_id: number,
city: string,
}

export interface SingleCleanwalk {
id: number;
name: string;
pos_lat: number;
pos_long: number;
date_begin: string;
duration: number;
description: string;
address: string;
host: {
author_id: number;
name: string;
role_id: number;
profile_picture: string;
};
participant_count: number;
is_user_participant: boolean;
}

export interface SubscibeToCleanwalk {
cleanwalk_id: number;
user_id: number;
nb_participants: number;
}
6 changes: 6 additions & 0 deletions front/src/interfaces/coordinateInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Coordinate {
pos_long: number,
pos_lat: number,
address: string,
city: string,
}
22 changes: 16 additions & 6 deletions front/src/interfaces/userInterface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
export interface User {
access_token(tokenCookieName: string, access_token: any, tokenCookieExpireTime: string, arg3: string, arg4: string, arg5: boolean): unknown;
id: number,
firstname: string,
lastname: string,
id?: number,
name: string,
email: string,
password: string,
token?: string,
password?: string,
profile_picture: string,
role: "organisation" | "user"
}

export interface Association {
user_id: number,
name: string,
email: string,
profile_picture: string,
description?: string,
banner_img?: string,
web_site?: string,
social_media?: Record<string, string>
}
6 changes: 6 additions & 0 deletions front/src/main.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ import './assets/main.scss'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import vue3GoogleLogin from 'vue3-google-login'
import VueCookies from 'vue-cookies';

import App from './App.vue'
import router from './router'
@@ -10,5 +12,9 @@ const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(vue3GoogleLogin, {
clientId: (import.meta as any).env.VITE_GOOGLE_CLIENT_ID,
})
app.use(VueCookies)

app.mount('#app')
82 changes: 66 additions & 16 deletions front/src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,93 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory, type NavigationGuardNext, type RouteLocationNormalized } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { useAccountStore } from '@/stores/AccountStore';

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),

history: createWebHistory((import.meta as any).env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/test',
name: 'test',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/TestView.vue')
},
{
path: '/cleanwalk',
path: '/cleanwalk/:id',
name: 'cleanwalk',
component: () => import('../views/SingleCleanwalkView.vue')
},
{
path: '/add',
name: 'add',
component: () => import('../views/AddView.vue')
},{
path: '/add/cleanwalk',
name: 'addCleanwalk',
component: () => import('../views/AddCleanwalkView.vue')
},
{
path: '/articles',
name: 'articles',
component: () => import('../views/ArticlesView.vue')
},
{
path: '/article',
name: 'article',
component: () => import('../views/ArticleView.vue')
path: '/associations',
name: 'associations',
component: () => import('../views/AssoListView.vue')
},
{
path: '/profile',
name: 'profile',
path: '/menu',
name: 'menu',
component: () => import('../views/MenuView.vue')
},
{
path: '/menu/profile',
name: 'menuProfile',
component: () => import('../views/ProfileView.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue')
},
{
path: '/signup',
name: 'signup',
component: () => import('../views/SignupView.vue')
},
{
path: '/signup/organisation',
name: 'signupOrganisation',
component: () => import('../views/SignupOrganisationView.vue')
},
{
path: '/logout',
name: 'logout',
component: () => import('../views/SignupView.vue')
},
{
path: '/:catchAll(.*)',
name: 'NotFound',
component: () => import('../views/404.vue')
}
]


})

const routesRequiringAuth = ['profile', 'anotherProtectedRoute'];

router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
const isMobile = window.innerWidth <= 768;
if(!useAccountStore().isLoggedIn) {
await useAccountStore().tokenLogin();
}

if (routesRequiringAuth.includes(to.name as string) && !useAccountStore().isLoggedIn && isMobile) {
next({ name: 'login' }); // Redirige vers la page de login
} else {
next(); // Continue vers la route demandée
}
});

export default router
64 changes: 41 additions & 23 deletions front/src/stores/AccountStore.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
import { defineStore } from 'pinia'
import { ref } from 'vue';
import type {Ref} from 'vue';
import type {User} from '@/interfaces/userInterface';
import type {User, Association} from '@/interfaces/userInterface';
import apiHelper from '@/helpers/apiHelper';
import router from '@/router';
import { inject } from 'vue';
import type {VueCookies} from 'vue-cookies';

import type { ApiResponse } from '@/interfaces/apiResponseInterface';

export const useAccountStore = defineStore('account', () => {
const $cookies = inject<VueCookies>('$cookies');
const tokenCookieName = 'access_token';
const tokenCookieExpireTime = '30d'; // 3m => 3 months (d => days, m => months, y => years)
let CurrentUser: Ref<User|undefined> = ref();
let isLoggedIn = ref(false);
let token: string|undefined = undefined;

async function login(email: string, password: string): Promise<boolean> {
const result = await apiHelper.kyPostWithoutToken('/users/login', {"email":email,"password": password} );
if(result != undefined && result as User != undefined) {
CurrentUser.value = result as User;
$cookies!.set(tokenCookieName, CurrentUser.value.access_token, tokenCookieExpireTime, '', '', true);
// need to create a cookie token
isLoggedIn.value = true;
}
return isLoggedIn.value;
}

function printToken() {
console.log($cookies!.get(tokenCookieName));
const setToken = (token: string) => {
$cookies!.remove(tokenCookieName);
$cookies!.set(tokenCookieName, token, tokenCookieExpireTime);
}
function printUser() {
console.log(CurrentUser.value);

const getOrganisationById = async (organisationId: number) => {
const response:ApiResponse = await apiHelper.kyGet('/users/organisations/' + organisationId);
return response.data as unknown as Association;
}

async function logout() {
@@ -42,20 +33,47 @@ export const useAccountStore = defineStore('account', () => {
}

async function tokenLogin(): Promise<boolean> {
const token = $cookies!.get(tokenCookieName);
const token:string = $cookies!.get(tokenCookieName);
if(token != undefined) {
const result = await apiHelper.kyPost('/users/token-login', {}, token);
if(result != undefined && result as User != undefined) {
CurrentUser.value = result as User;
const response:ApiResponse = await apiHelper.kyPost('/users/token-login', {}, token);
if(response.success === true) {
isLoggedIn.value = true;
const user:User = {
email: response.data.email as string,
name: response.data.name as string,
id: response.data.id as number,
role: response.data.role as 'organisation' | 'user',
profile_picture: response.data.profile_picture as string,
}
CurrentUser.value = user;
} else {
isLoggedIn.value = false;
}
} else {

isLoggedIn.value = false;
}
return isLoggedIn.value;
}

return {login, printToken, printUser, logout, isLoggedIn, tokenLogin}
const changePassword = async (userId:number, token:string, oldPassword: string, newPassword: string) => {
const response:ApiResponse = await apiHelper.kyPut('/users/password/' + userId, {
old_password: oldPassword,
new_password: newPassword
}, token);
return response.success;
}

const getAccessToken = ():string | undefined => {
return $cookies!.get(tokenCookieName);
}

const modifyUser = (userId: number, token: string, name?: string, profile_picture?: string) => {
apiHelper.kyPut('/users/' + userId, {
name: name,
profile_picture: profile_picture,
}, token);
}

return { setToken, logout, isLoggedIn, tokenLogin, CurrentUser, getAccessToken, modifyUser, changePassword, getOrganisationById}
})
80 changes: 62 additions & 18 deletions front/src/stores/CleanwalkStore.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,98 @@
import NominatimHelper from '@/helpers/nominatimHelper';
import apiHelper from '@/helpers/apiHelper';
import type { Cleanwalk } from '@/interfaces/cleanwalkInterface';
import type { Cleanwalk, CleanwalkCreation, SingleCleanwalk } from '@/interfaces/cleanwalkInterface';
import router from '@/router';
import { defineStore } from 'pinia'
import {ref, computed} from 'vue';
import type {Ref} from 'vue';
import { useAccountStore } from './AccountStore';


export const useCleanwalkStore = defineStore('cleanwalk', () => {
const getToken = useAccountStore().getAccessToken;

let cleanwalkIsSelect = ref(false);
const route:string = '/cleanwalks';

const route:string = 'cleanwalks';
let cleanwalksTab: Ref<Cleanwalk[]> = ref([]);

let cleanwalksTab: Ref<Cleanwalk[]|undefined> = ref([]);




async function getAllCleanwalks(): Promise<Cleanwalk[]|undefined> {
const result = await apiHelper.kyGet(route);
if(result != undefined) {
cleanwalksTab.value = result as Cleanwalk[];
async function getAllCleanwalks() {
const route = '/cleanwalks'; // Assure-toi que cette route est correcte et complète
try {
const result = await apiHelper.kyGet(route);
if (result.success && result.data) {
// Convertit les données seulement si elles existent et la requête a réussi
cleanwalksTab.value = result.data as unknown as Cleanwalk[];
} else {
// Log une erreur si la requête a échoué
console.error('Failed to fetch cleanwalks:', result.data);
}
} catch (error) {
// Gestion des erreurs de la requête elle-même
console.error('Request failed:', error);
}
return cleanwalksTab.value;
}

async function getCleanwalkById(id: number): Promise<Cleanwalk|undefined> {
const result = await apiHelper.kyGet(route + '/' + id);
if(result != undefined) {
return result as Cleanwalk;
async function getCleanwalkById(id: number, userId?:number): Promise<SingleCleanwalk|undefined> {
let url = route + '/' + id;
if(userId) {
url += '?user_id=' + userId;
}
const result = await apiHelper.kyGet(url);
if(result.success && result.data) {
return result.data as unknown as SingleCleanwalk;
}
return undefined;
}

async function createCleanwalk(cleanwalk: Cleanwalk, token:string): Promise<Cleanwalk|undefined> {
const result = await apiHelper.kyPost(route, cleanwalk, token);
async function createCleanwalk(cleanwalk: CleanwalkCreation): Promise<CleanwalkCreation|undefined> {
const token = getToken();
if (token === undefined) {
router.push('/login');
return undefined;
}
const result = await apiHelper.kyPost(route, cleanwalk as unknown as Record<string, unknown>, token);
if(result != undefined) {
return result as Cleanwalk;
return result.data as unknown as CleanwalkCreation;
}
return undefined;
}

async function updateCleanwalk(cleanwalk: Cleanwalk, token:string): Promise<Cleanwalk|undefined> {
const result = await apiHelper.kyPut(route + '/' + cleanwalk.id, cleanwalk, token);
const result = await apiHelper.kyPut(route + '/' + cleanwalk.id, cleanwalk as unknown as Record<string, unknown>, token);
if(result != undefined) {
return result as Cleanwalk;
return result.data as unknown as Cleanwalk;
}
return undefined;
}

return {getAllCleanwalks, cleanwalksTab, cleanwalkIsSelect}
async function joinCleanwalk(cleanwalkId: number, token:string, nb_participants:number, user_id:number): Promise<boolean> {
const result = await apiHelper.kyPost(route + '/join', {
cleanwalk_id: cleanwalkId,
user_id: user_id,
nb_person: nb_participants
}, token);
if(result != undefined) {
return result.success;
}
return false;
}

async function leaveCleanwalk(cleanwalkId: number, token:string, user_id:number): Promise<boolean> {
const result = await apiHelper.kyDelete(route + '/leave', {
cleanwalk_id: cleanwalkId,
user_id: user_id
}, token);
if(result != undefined) {
return result.success;
}
return false;
}

return {getAllCleanwalks, cleanwalksTab, getCleanwalkById, createCleanwalk, joinCleanwalk, leaveCleanwalk}

});
26 changes: 26 additions & 0 deletions front/src/stores/UtilsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, type Ref } from 'vue'

export const useUtilsStore = defineStore('utils', () => {

const toast = ref({
message: '',
isSuccess: false,
isVisible: false,
});



const showToast = (error: string, isSuccess: boolean, ) => {
toast.value.isVisible = true;
toast.value.isSuccess = isSuccess;
toast.value.message = error;
setTimeout(() => {
toast.value.isVisible = false;
}, 3000);
}

return {toast, showToast}

})
3 changes: 3 additions & 0 deletions front/src/views/404.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
404
</template>
10 changes: 10 additions & 0 deletions front/src/views/AddCleanwalkView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import navBar from '@/components/navBar.vue';
import TopBar from '@/components/TopBar.vue';
import AddCleanwalk from '@/components/AddCleanwalk.vue';
</script>
<template>
<TopBar back-url="/add" page-name="Ajouter une cleanwalk" />
<AddCleanwalk />
<navBar />
</template>
6 changes: 4 additions & 2 deletions front/src/views/AddView.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script setup lang="ts">
import navBar from '@/components/navBar.vue';
import AddCleanwalk from '@/components/AddCleanwalk.vue';
import TopBar from '@/components/TopBar.vue';
import Add from '@/components/AddChoice.vue';
</script>
<template>
<AddCleanwalk />
<TopBar page-name="Ajouter" />
<Add />
<navBar />
</template>
7 changes: 0 additions & 7 deletions front/src/views/ArticleView.vue

This file was deleted.

12 changes: 12 additions & 0 deletions front/src/views/ArticlesView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
import navBar from '@/components/navBar.vue';
import TopBar from '@/components/TopBar.vue';
import SwithChoice from '@/components/SwithChoice.vue';
import ArticlesList from '@/components/ArticlesList.vue';
</script>
<template>
<TopBar page-name="Articles" />
<SwithChoice categorie1="articles" categorie2="associations" route1="articles" route2="associations" :activeCategory="true" />
<ArticlesList />
<navBar />
</template>
12 changes: 12 additions & 0 deletions front/src/views/AssoListView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
import navBar from '@/components/navBar.vue';
import TopBar from '@/components/TopBar.vue';
import SwithChoice from '@/components/SwithChoice.vue';
import AssoList from '@/components/AssoList.vue';
</script>
<template>
<TopBar pageName="Associations"/>
<SwithChoice categorie1="articles" categorie2="associations" :activeCategory="false" route1="articles" route2="associations" />
<AssoList />
<navBar />
</template>
8 changes: 8 additions & 0 deletions front/src/views/HomeView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<script setup lang="ts">
import MobileHome from '@/components/MobileHome.vue';
import navBar from '@/components/navBar.vue';
import { useCleanwalkStore } from '@/stores/CleanwalkStore';
import { onMounted } from 'vue';
const store = useCleanwalkStore();
onMounted( async () => {
const data = await store.getAllCleanwalks();
});
</script>

<template>
10 changes: 10 additions & 0 deletions front/src/views/LoginView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import Login from '@/components/Login.vue';
</script>

<template>
<main>
<Login />
</main>
</template>
11 changes: 11 additions & 0 deletions front/src/views/MenuView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import navBar from '@/components/navBar.vue';
import Menus from '@/components/Menu.vue';
import TopBar from '@/components/TopBar.vue';
</script>

<template>
<TopBar page-name="Menu" />
<Menus />
<navBar />
</template>
5 changes: 4 additions & 1 deletion front/src/views/ProfileView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script setup lang="ts">
import navBar from '@/components/navBar.vue';
import Profile from '@/components/Profile.vue';
import TopBar from '@/components/TopBar.vue';
</script>
<template>
profile page
<TopBar page-name="Profile" />
<navBar />
<Profile />
</template>
9 changes: 9 additions & 0 deletions front/src/views/SignupOrganisationView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts">
import SignupOrganisation from '@/components/SignupOrganisation.vue';
import SwithChoice from '@/components/SwithChoice.vue';
</script>
<template>
<SwithChoice categorie1="particulier" categorie2="associations/Organisation" route1="signup" route2="signup/organisation" :activeCategory="false" />
<SignupOrganisation />
</template>
10 changes: 10 additions & 0 deletions front/src/views/SignupView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import Signup from '@/components/Signup.vue';
import SwithChoice from '@/components/SwithChoice.vue';
</script>
<template>
<SwithChoice categorie1="particulier" categorie2="associations/Organisation" route1="signup" route2="signup/organisation" :activeCategory="true" />

<Signup />
</template>
2 changes: 1 addition & 1 deletion front/src/views/SingleCleanwalkView.vue
Original file line number Diff line number Diff line change
@@ -6,4 +6,4 @@ import navBar from '@/components/navBar.vue';
<template>
<SingleCleanwalk />
<navBar />
</template>
</template>
77 changes: 0 additions & 77 deletions front/src/views/TestView.vue

This file was deleted.

24 changes: 12 additions & 12 deletions front/vite.config.ts
Original file line number Diff line number Diff line change
@@ -16,18 +16,18 @@ export default defineConfig({
server: {
host: true,
proxy: {
'/api': {
target: 'api',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, ''),
},
'/nominatim': {
target: 'https://nominatim.openstreetmap.org',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/nominatim/, ''),
}
// '/api': {
// target: 'http://127.0.0.1:5000',
// changeOrigin: true,
// secure: false,
// rewrite: (path) => path.replace(/^\/api/, ''),
// },
// '/nominatim': {
// target: 'https://nominatim.openstreetmap.org',
// changeOrigin: true,
// secure: false,
// rewrite: (path) => path.replace(/^\/nominatim/, ''),
// }
},
watch: {
usePolling: true
60 changes: 60 additions & 0 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
server {
listen 80;
server_name cleanwalk.online api.cleanwalk.online uploads.cleanwalk.online www.cleanwalk.online;

# Redirigez tout le trafic HTTP vers HTTPS
location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl;
server_name cleanwalk.online www.cleanwalk.online;

ssl_certificate /etc/letsencrypt/live/cleanwalk.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cleanwalk.online/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

server {
listen 443 ssl;
server_name api.cleanwalk.online;

ssl_certificate /etc/letsencrypt/live/cleanwalk.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cleanwalk.online/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

location / {
proxy_pass http://api:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

server {
listen 443 ssl;
server_name uploads.cleanwalk.online;

ssl_certificate /etc/letsencrypt/live/cleanwalk.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cleanwalk.online/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

location / {
alias /usr/share/nginx/html/uploads/;
autoindex on; # Optionnel : permet de lister les fichiers du répertoire
}
}
7 changes: 0 additions & 7 deletions stash.env.example

This file was deleted.

0 comments on commit 0238285

Please sign in to comment.