diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..339ff04 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,3 @@ +.env +.DS_Store +../.DS_Store \ No newline at end of file diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 369adb7..d14048f 100644 Binary files a/backend/__pycache__/main.cpython-311.pyc and b/backend/__pycache__/main.cpython-311.pyc differ diff --git a/backend/__pycache__/supabase_client.cpython-311.pyc b/backend/__pycache__/supabase_client.cpython-311.pyc index 572bd05..c70a168 100644 Binary files a/backend/__pycache__/supabase_client.cpython-311.pyc and b/backend/__pycache__/supabase_client.cpython-311.pyc differ diff --git a/backend/main.py b/backend/main.py index d02c894..8ade5f6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,9 @@ + +import lunchheros - -from src.routes.users.userFunctions import getAllUsers, getUserWithId +from fastapi.encoders import jsonable_encoder +from src.lunchheros.db.dbFetcher import get_encoded_data +from src.routes.users.userFunctions import getAllQueryListData, getAllUsers, getUserWithId from fastapi import FastAPI from supabase_client import supabase_client @@ -20,4 +23,13 @@ def load_all_users(): @app.get("/users/{userId}") def load_current_user(userId): currentUser = getUserWithId(userId) - return { "currentUser" : currentUser} \ No newline at end of file + return { "currentUser" : currentUser} + +@app.get("/button_trigger/{userId}") +def load_current_user(userId: str): + userData = getUserWithId(userId) + queryList = getAllQueryListData() + + test = lunchheros.match.match(userData) + + return { "currentUser" : userData, "query": queryList} \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 56637a6..401a5b1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,6 +9,9 @@ readme = "README.md" python = ">=3.9" pandas = "*" scikit-learn = "^1.3.2" +supabase = "^1.2.0" +fastapi = "^0.104.0" +python-dotenv = "^1.0.0" [tool.poetry.group.dev.dependencies] diff --git a/backend/src/__init__.py b/backend/src/__init__.py index 577f3a6..fde3f74 100644 --- a/backend/src/__init__.py +++ b/backend/src/__init__.py @@ -1 +1,4 @@ -"""Lunchheros: Backend to match people for lunch based on their interests.""" \ No newline at end of file +"""Lunchheros: Backend to match people for lunch based on their interests.""" +from lunchheros import match + +__all__ = ["match"] \ No newline at end of file diff --git a/backend/src/lunchheros/clustering/__init__.py b/backend/src/lunchheros/clustering/__init__.py new file mode 100644 index 0000000..92343a5 --- /dev/null +++ b/backend/src/lunchheros/clustering/__init__.py @@ -0,0 +1 @@ +"""Subpackage related to use the distance matrix to cluster the users into groups.""" \ No newline at end of file diff --git a/backend/src/lunchheros/clustering/userClustering.py b/backend/src/lunchheros/clustering/userClustering.py new file mode 100644 index 0000000..5036d24 --- /dev/null +++ b/backend/src/lunchheros/clustering/userClustering.py @@ -0,0 +1,13 @@ +import numpy as np +from sklearn.cluster import DBSCAN +from scipy.spatial import distance + +def generateUserClusters(distanceMatrix): + """Generate clusters of users based on the distance matrix.""" + + # Apply DBSCAN on the distance matrix + dbscan = DBSCAN(metric='precomputed') + clusters = dbscan.fit_predict(distanceMatrix) + + # clusters now contains the cluster labels for each user + print(clusters) diff --git a/backend/src/lunchheros/db/__init__.py b/backend/src/lunchheros/db/__init__.py new file mode 100644 index 0000000..cabb9a3 --- /dev/null +++ b/backend/src/lunchheros/db/__init__.py @@ -0,0 +1 @@ +"""Subpackage related to matching between users into groups.""" \ No newline at end of file diff --git a/backend/src/lunchheros/db/dbFetcher.py b/backend/src/lunchheros/db/dbFetcher.py new file mode 100644 index 0000000..429af96 --- /dev/null +++ b/backend/src/lunchheros/db/dbFetcher.py @@ -0,0 +1,32 @@ +import numpy as np +import pandas as pd +import json +import os + + + + +async def get_encoded_data(time_slot: int, location: str, data): + print("get_encoded_data") + # Transform data + #print(f"Data {time_slot} {location} {data}") + #df = pd.DataFrame(data) + + + # One-hot encode data + #one_hot_encoded_df = pd.get_dummies(df, columns=['WIP']) # wip - replace with actual column names + #one_hot_encoded_df.to_dict(orient='records') + + #return one_hot_encoded_df + + +def parse_user_id_tolist(test_data): + data = json.loads(test_data) + + # Extract the id values into a python list + ids = [x['id'] for x in data] + + # Convert the python list to a numpy array + numpy_array = np.array(ids).tolist() + + return numpy_array diff --git a/backend/src/lunchheros/match/__init__.py b/backend/src/lunchheros/match/__init__.py index 7cecbcd..7233da5 100644 --- a/backend/src/lunchheros/match/__init__.py +++ b/backend/src/lunchheros/match/__init__.py @@ -1 +1,5 @@ """Subpackage related to matching between users into groups.""" + +from lunchheros.match import matching + +__all__ = ["matching"] \ No newline at end of file diff --git a/backend/src/lunchheros/match/_randomize.py b/backend/src/lunchheros/match/_randomize.py new file mode 100644 index 0000000..fd05658 --- /dev/null +++ b/backend/src/lunchheros/match/_randomize.py @@ -0,0 +1,54 @@ +import random + +from lunchheros.db.dbFetcher import parse_user_id_tolist + + +def _randomize_groups(group_size: int, users: list[str]) -> list[list]: + """Randomize the groups of users. + + Makes groups of size ``group_size`` from the list of users ``users``. + If the number of users is not a multiple of ``group_size``, the remaining + users will be distributed randomly across the groups. + + Args: + group_size: size of the groups + users: list of users id + Returns: + A list of lists, each representing a group of users + """ + + # Shuffle the users randomly + random.shuffle(users) + + # Calculate the number of groups needed + num_groups = len(users) // group_size + + # Calculate the number of users that will be left alone + remaining_users = len(users) % group_size + + # Create the groups + groups = [] + for i in range(num_groups): + group = users[i * group_size : (i + 1) * group_size] + groups.append(group) + + # Distribute the remaining users across the groups + for i in range(remaining_users): + groups[i].append(users[num_groups * group_size + i]) + + return groups + + +def match(userids): + + # convert userids to list + userids = parse_user_id_tolist(userids) + # group size + groupsize = 5 + # randomize the groups + groups = _randomize_groups(groupsize, userids) + # return the groups + return groups + + + diff --git a/backend/src/routes/users/__pycache__/userFunctions.cpython-311.pyc b/backend/src/routes/users/__pycache__/userFunctions.cpython-311.pyc index a7a9ef6..28fa595 100644 Binary files a/backend/src/routes/users/__pycache__/userFunctions.cpython-311.pyc and b/backend/src/routes/users/__pycache__/userFunctions.cpython-311.pyc differ diff --git a/backend/src/routes/users/userFunctions.py b/backend/src/routes/users/userFunctions.py index c2af593..48006f4 100644 --- a/backend/src/routes/users/userFunctions.py +++ b/backend/src/routes/users/userFunctions.py @@ -1,13 +1,16 @@ from supabase_client import supabase_client -users_select = "*, company:companies(*)" +users_select = "*, company:companies(*), users_interests(*, interest(*, category(*))), age_range(*)" def getUserWithId(userId): return supabase_client.table("users").select(users_select).eq("id", userId).single().execute() -from supabase_client import supabase_client def getAllUsers(): return supabase_client.table("users").select(users_select).execute() - \ No newline at end of file + + +def getAllQueryListData(): + return supabase_client.table("waiting_query").select(f"*, user({users_select}), time_range(*), company(*), age_range(*)").execute() + diff --git a/backend/tests/match/test_randomize.py b/backend/tests/match/test_randomize.py new file mode 100644 index 0000000..e3236e7 --- /dev/null +++ b/backend/tests/match/test_randomize.py @@ -0,0 +1,40 @@ +from lunchheros.match._randomize import _randomize_groups + +import json +import numpy as np + +def test_randomize_groups(): + """Test randomize groups.""" + groupsize = 4 + # make list of 21 users with string id + users = [str(i) for i in range(21)] + # randomize groups + groups = _randomize_groups(groupsize, users) + print(groups) + # check that all groups are at least 2 users + assert all(len(group) >= 2 for group in groups) + # check that all groups are at most 4 users + assert all(len(group) <= groupsize + groupsize for group in groups) + + + + +test_data = '[{"id":"b994511a-4b7c-4d52-bb26-baeffde1df7d"},{"id":"6ea2070c-18e4-4946-83a4-c41584b3eb9a"},{"id":"f60d3486-e6da-45d9-8a68-b39634c2c1c2"},{"id":"89bce073-f759-4290-87dc-87c63a54c553"},{"id":"456732ea-1a2c-42ed-8d9a-4ff83cc9391d"},{"id":"4404a8a1-3429-45f0-b516-2656cfd96643"},{"id":"08e11522-0d52-4014-9c47-d0b4f27b6716"},{"id":"6923fe00-70dd-4bf6-8c7a-7a15c6f8482e"},{"id":"797ee2cb-5c90-4a09-b15d-f4902193b618"},{"id":"aa3e50f0-091a-458f-a62a-caa586b77751"},{"id":"cfc5a3ec-9fdb-4d8a-b380-4737f7e4c5ed"},{"id":"460c2346-459b-4eaa-b821-b6778be53b14"},{"id":"3738d56c-615c-4129-b389-34b75e788580"},{"id":"d50d37d3-f718-40ea-b3b3-c54593b9b048"},{"id":"5e933bdf-29ca-45fb-8f45-3e4c4805ca60"},{"id":"683bcf3c-f855-4164-9057-a5266fb685e3"},{"id":"85e9d50a-a110-44c7-a512-9c6e285fde09"},{"id":"dd1b225d-4320-4aa2-8873-c25ca5307fbf"},{"id":"5423ae0b-0535-443b-89ab-9defd6ee6eda"},{"id":"ad53c0f5-6a93-4904-bfec-22bf7da98ef4"},{"id":"3dc8e3eb-be7c-4e1f-a92d-7ce3b3ee42c3"},{"id":"691888e2-536e-4183-ae98-1d03e643eeb4"},{"id":"a8e24543-0251-41d7-9eff-dbb3b2690d23"},{"id":"da7762a0-5172-4d6f-ab2f-9eb7a953bd9e"},{"id":"a9a3c0b6-df0a-4b39-b613-582703201145"},{"id":"25d411a2-5ac9-445f-a33f-25ece3dde23b"},{"id":"1784f97e-9f3b-4059-a8b4-7c23c3c8897d"},{"id":"7819f098-6772-4aa8-895e-d193199514c8"},{"id":"1df69d95-dbf5-4e68-a102-01974ea7939c"},{"id":"abb3dead-c9e4-4dbd-922a-c7fbe8f448a8"},{"id":"654a36f8-7b36-4ec1-a02e-e45c3673442e"},{"id":"9edeb3f4-ee7e-417e-aaee-d7a40ed0f24a"},{"id":"3f1cec4e-b1ea-4d0f-91d6-f3ff964b2bf1"},{"id":"ce821535-9969-4a93-9b8d-a80a6242f998"},{"id":"f154a6c9-e1b0-4b67-97ab-d65c69ebed69"},{"id":"04b91fd2-78cf-4779-bd7f-0c0bca1c5187"},{"id":"447e63ba-a69b-4b0a-9bfd-6c9d8b19026a"},{"id":"2229ba43-1c63-425e-b157-1368399b2d73"},{"id":"ae4b46d4-984d-44ed-9e3c-cf8823bb0ebf"},{"id":"c9339894-43fb-45e6-b7ba-f527eeb4a3ea"},{"id":"7c047b21-be3c-427e-92c2-82b7fd7459d3"},{"id":"ee3ed5cf-718d-4d34-bfee-b54057870f7e"},{"id":"787dd532-a1c5-4d15-a371-35e6b5e6a169"},{"id":"9bb0689f-4018-40af-9224-1d2595e75801"},{"id":"bb8bd4e1-dce6-46d9-b9b1-20b46a0bfd68"},{"id":"ae93a1ab-921c-4062-95d9-fcd11357a4b6"},{"id":"4837001a-73b0-4b18-bfda-caccee8aef7f"},{"id":"d007dc20-8bae-4c58-b634-ec42e334e0b6"},{"id":"c6e4f2c4-5ed2-4d8f-9075-36b3f77abc37"},{"id":"dc1b7985-aa01-4087-9b35-f6fa6dc6e2b0"},{"id":"00b1fc3d-8e79-4558-af44-ec8d0a6bb3c2"},{"id":"50c17ac7-5e41-4476-b8ba-f59da66885b1"},{"id":"65980da4-879c-401d-9be0-71209c92c62b"},{"id":"bc74a884-7603-4824-be06-6c82091eb24e"},{"id":"a9670953-5906-4190-848c-5a27cdaa12a3"},{"id":"6a23c795-3a9f-4c81-80f8-92ff3088ae36"},{"id":"d2a3fcef-2518-47cd-b038-5bb16599fca9"},{"id":"c549f2c8-862c-4f0a-9302-7784e8ce6608"},{"id":"c7d6a357-368c-454b-a0ef-fcf2da5442b7"},{"id":"dca40f83-27a2-456e-855e-8f5b5df6744e"},{"id":"c93e381b-81a2-485d-9337-5ccb3d33da4c"},{"id":"6bc16283-2b9d-4196-82f1-dafd05805388"},{"id":"541fa596-b1d1-4da2-bb50-26950006c484"},{"id":"7ce5d11c-c44f-4aaf-878e-2e4d14e68339"},{"id":"e77ee698-bdf4-4e62-9dcf-23b10df1bec1"},{"id":"9507a229-592b-4c87-ae04-52e25d22a6bd"},{"id":"fc4ae092-ef73-4cc3-840b-70e9940d33f1"},{"id":"ae29079c-2bfc-4ce3-a924-339b88c59bbf"},{"id":"6e591ada-490e-4174-9ab9-dc4879632c27"},{"id":"e7467801-c20c-4359-b99f-d5d132e51fc7"},{"id":"edc783c5-531f-4f21-a9cc-574451847065"},{"id":"60a7563a-b5b1-4924-936f-9c8deb78cd9d"},{"id":"1cdbc0a9-eba0-4926-b94b-99164328dba3"},{"id":"81be34d5-e71b-456e-a913-21fece836215"},{"id":"48add589-f329-49e9-a151-fae9d44e08f4"},{"id":"dca5539f-c660-488e-99cf-5f9258c6c84e"},{"id":"78888bef-a3e8-4ee1-a54d-c8bd14d30898"},{"id":"aba32f5e-d5a3-4466-bcf6-77eda1de5214"},{"id":"55251bbf-21c0-4f62-a560-ca12f8235342"},{"id":"790de1d5-df0a-4570-943c-f0089d561bc3"},{"id":"90864c9f-612b-457b-8e42-8afb46f3f922"},{"id":"8915ac2c-c740-42df-8fa1-0c1d132bb92e"},{"id":"f48d9896-c80b-4745-abc3-50acfde0a63c"},{"id":"b1434b1c-1a6c-441d-9812-90bdaee1215f"},{"id":"5596fbd9-f33a-4fb0-b844-e6a979a35ac7"},{"id":"bdcf15c4-b164-49ed-a42e-2d6be5fba168"},{"id":"0a1f949e-5d8e-4080-9936-de0f94e26271"},{"id":"57aa5b15-4c93-49c7-8e4c-5c0982353749"},{"id":"8d98509f-464a-4c26-9142-f5903c260a51"},{"id":"5fce6888-9570-4f7b-90c5-be2e9f3f7035"},{"id":"e6940d4a-a287-4dc9-b293-60cabc3b4655"},{"id":"666137b1-ffa8-4612-8e99-08e93691e33b"},{"id":"180d1e45-d8ab-4679-a260-037416c8b9ea"},{"id":"7d11ea4e-f72d-4bf5-a71f-57b373435fd1"},{"id":"81940ce9-34c7-4288-b343-99ea4464493c"},{"id":"3fb80679-0120-4383-8047-e3e8da12d69d"},{"id":"e30ffdaf-2467-42ce-a703-034a478b0417"},{"id":"06cbb2ea-96e0-4896-a425-b098957af37a"},{"id":"cb7e5907-68e7-44bd-95c0-06ccebc075db"},{"id":"bbbbaf3c-1d57-4e11-bf88-9bf92d374b97"},{"id":"bf7c7244-ff86-470c-ba95-07f1e21c9682"},{"id":"4117890c-bdd9-487c-9921-d7a5c9d9b08d"},{"id":"5925b106-1578-45a9-bb30-7fafb37cccdb"},{"id":"00d4050b-614b-4be8-aa16-cab95f6e7dca"},{"id":"4938a261-8083-44d2-955f-c00856f8eb6d"},{"id":"798bdf20-1614-4026-9235-ab10ba228dae"},{"id":"b932c46d-3db7-4cd9-8007-6db3cf2b923b"},{"id":"2e9db169-2094-417e-a775-c8a75790c458"},{"id":"fb16e78e-5ccb-4635-b355-a01f0834389c"},{"id":"9a7ae5aa-b788-49fd-8894-9f899f2a2587"},{"id":"3c3a5aca-6c0e-4e9d-90cb-557fef1256c1"},{"id":"31d574de-869d-4098-be2b-addbfdacebcb"},{"id":"bbaa570b-295b-4bbd-bfd4-02e0b754419e"},{"id":"1e7d409b-03e0-46ef-bdc3-cb55807c10a4"},{"id":"46da386a-c562-4eea-9ae9-c6f87700dbc3"},{"id":"5c5ac49c-a9e9-4fbb-84d1-c5e956f30781"},{"id":"7ddd95e3-3a77-4b61-9d0a-3ffaa1c5a95a"},{"id":"36ad540a-b857-433a-8d07-4087e4d0fe4a"},{"id":"ea88696e-7e9b-49e9-8d65-ace6084425d7"},{"id":"05f6a875-9cc5-4185-8352-0afab880ff4d"},{"id":"6fdb38c9-be3c-44df-9a7a-3f73413842b3"},{"id":"f3313ebc-7b1d-4691-9e65-5ff4e549210a"},{"id":"92cdf84d-485b-4bc6-af29-e9853d20ff5d"},{"id":"878fd4ae-c15c-4325-90f5-4674d04c5455"},{"id":"36e6896d-1814-4348-94f8-a83282d1503b"},{"id":"d6fdf1c7-1333-4382-bac9-4a8a37608b02"},{"id":"37c19c16-e97c-4276-8bf9-97ef7c29d747"}]' + + +def test_randomize_with_real(): + """Test randomize groups.""" + # read in the test data + # Load the JSON data into a python list of dictionaries + data = json.loads(test_data) + + # Extract the id values into a python list + ids = [x['id'] for x in data] + + # Convert the python list to a numpy array + numpy_array = np.array(ids) + print(numpy_array) + group_size = 4 + # randomize groups + groups = _randomize_groups(group_size, numpy_array.tolist()) + print(groups) \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..3233e16 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,58 @@ +# --- Backend Build Stage --- +FROM python:3.9-slim as backend + +WORKDIR /app + +# Install Poetry +RUN pip install poetry + +# Copy the Poetry configuration and lock files +COPY backend/pyproject.toml backend/poetry.lock /app/ + +# Install dependencies without creating a virtual environment +RUN poetry config virtualenvs.create false \ + && poetry install --no-dev --no-interaction --no-ansi + +# Install Gunicorn +RUN pip install gunicorn + +# Copy the backend source code into the image +COPY backend /app/ + +# --- Frontend Build Stage --- +FROM node:14 as frontend + +WORKDIR /app + +# Copy the frontend package configuration files +COPY frontend/package*.json /app/ + +# Install dependencies +RUN npm install + +# Copy the frontend source code into the image +COPY frontend /app/ + +# Build the frontend +RUN npm run build + +# --- Nginx Stage --- +FROM nginx:alpine + +# Copy the built frontend files to the Nginx server +COPY --from=frontend /app/build /usr/share/nginx/html + +# Copy the backend app to a directory served by Gunicorn +COPY --from=backend /app /app + +# Set up Gunicorn to serve the backend app +COPY docker/gunicorn.conf /etc/gunicorn/gunicorn.conf + +# Set up Nginx to proxy requests to Gunicorn +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf + +# Expose the necessary ports +EXPOSE 80 + +# Start Gunicorn and Nginx +CMD ["sh", "-c", "gunicorn -c /etc/gunicorn/gunicorn.conf app:app & nginx -g 'daemon off;'"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..97a0431 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,60 @@ +# LunchHeroes Docker Configuration + +This Docker setup is designed to build the Docker image for the LunchHeroes application, which can then be pushed to a Docker registry and deployed on a server. + + +## Preliminary Steps + +1. **Building the Docker Image** + To build the Docker image, navigate to the directory containing the Dockerfile and run the following command: + ```bash + docker push lucca93/lunchheroes:latest + ``` + +2. **Pushing Docker Image:** + - After building the Docker image, push it to your Docker registry using the following command: + ```bash + docker push lucca93/lunchheroes:latest + ``` + + +**Server Hosting:** + - The server is hosted on [Flow Swiss](https://my.flow.swiss/). + +## File Descriptions + +### Dockerfile + +The Dockerfile orchestrates the build process for both the frontend and backend of the LunchHeroes application. Here's a brief overview of each stage in the Dockerfile: + +- **Backend Build Stage:** + - Uses the `python:3.9-slim` base image. + - Installs Poetry for dependency management. + - Copies and installs the backend dependencies using Poetry. + - Installs Gunicorn as the WSGI HTTP server to serve the backend application. + - Copies the backend source code into the image. + +- **Frontend Build Stage:** + - Uses the `node:14` base image. + - Copies and installs the frontend dependencies using npm. + - Copies the frontend source code into the image. + - Builds the frontend assets. + +- **Nginx Stage:** + - Uses the `nginx:alpine` base image. + - Copies the built frontend files to the Nginx server. + - Copies the backend app to a directory served by Gunicorn. + - Sets up Gunicorn to serve the backend app. + - Sets up Nginx to proxy requests to Gunicorn for the `/api` endpoint. + - Exposes port 80. + - Starts Gunicorn and Nginx. + +### gunicorn.conf + +This file contains the Gunicorn configuration. Gunicorn is set up to bind to `0.0.0.0:8000` and uses 3 worker processes. + +### nginx.conf + +This file contains the Nginx configuration. Nginx is set up to serve the static frontend files and to proxy requests to the `/api` endpoint to Gunicorn. + + diff --git a/docker/gunicorn.conf b/docker/gunicorn.conf new file mode 100644 index 0000000..373ea0d --- /dev/null +++ b/docker/gunicorn.conf @@ -0,0 +1,2 @@ +bind = "0.0.0.0:8000" +workers = 3 diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..12ef0ad --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + try_files $uri /index.html; + } + + location /api { + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/frontend/package.json b/frontend/package.json index 522c1d5..793c474 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "react-scroll": "^1.8.3", "@supabase/supabase-js": "^2.38.4", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", diff --git a/frontend/src/App.js b/frontend/src/App.js index 4983ffc..587e81c 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -9,11 +9,12 @@ import Status from './Components/Status'; import {useSelector} from 'react-redux'; // import useSelector from react-redux import { Route, Navigate, Routes } from 'react-router-dom'; // import Route and Navigate from react-router-dom import Scheduling from './Components/Scheduling'; -import { Button } from 'react-scroll'; - import{ supabase } from './supabaseclient'; + + function App() { + // const [isLoaded, setIsLoaded] = useState(false); // useEffect(() => { // dispatch(authenticate()).then(() => setIsLoaded(true)); @@ -43,11 +44,13 @@ function App() { } const user = useSelector(state => state.session.user); + console.log("LOGIN USER", user?.user?.id); + console.log("LOGIN", user); const Wrapper = () => { return (
- @@ -73,29 +76,21 @@ function App() { console.log(user); return ( <> - - - - } path="/" /> - {user ? ( + {user ? ( //routes that will be available ONLY when user is logged in //add additional routes here - } path="/home" /> + } path="/home" /> ) : ( + // } path="/home" /> //will redirect to '/' from any url if no user is logged in //do not add additional routes here - } /> + <> + } /> + } path="/login" /> + )} - } path="/" /> ); diff --git a/frontend/src/Components/Login.js b/frontend/src/Components/Login.js index cc67095..e59c2b7 100644 --- a/frontend/src/Components/Login.js +++ b/frontend/src/Components/Login.js @@ -2,32 +2,36 @@ import React, { useState } from 'react'; import {useDispatch} from 'react-redux'; import { login } from '../store/session'; import './../SCSS/Login.css'; +import { useNavigate } from "react-router-dom"; function Login() { - const [username, setUsername] = useState(''); + const [email, setEmail] = useState(""); const [password, setPassword] = useState(''); const dispatch = useDispatch(); + const nav = useNavigate(); + const handleSubmit = (e) => { e.preventDefault(); // Here you can send the username and password to your server - console.log('Username:', username, 'Password:', password); - dispatch(login(username, password)); - + dispatch(login(email, password)) + .then(() => { + nav("/home"); + }) }; return ( -
+

Login

@@ -44,7 +48,9 @@ function Login() {
- +
diff --git a/frontend/src/Components/ProfileButton.js b/frontend/src/Components/ProfileButton.js index 3d51673..68b475e 100644 --- a/frontend/src/Components/ProfileButton.js +++ b/frontend/src/Components/ProfileButton.js @@ -1,22 +1,17 @@ import React, { useState, useEffect, useRef } from "react"; - import { logout } from "../store/session"; import { useNavigate } from "react-router-dom"; import "../SCSS/navigation.css"; - function ProfileButton({ user }) { const [showMenu, setShowMenu] = useState(false); const ulRef = useRef(); const navigate= useNavigate() - const openMenu = () => { if (showMenu) return; setShowMenu(true); }; - useEffect(() => { if (!showMenu) return; - const closeMenu = (e) => { if (!ulRef.current.contains(e.target)) { setShowMenu(false); @@ -25,21 +20,17 @@ function ProfileButton({ user }) { document.addEventListener("click", closeMenu); return () => document.removeEventListener("click", closeMenu); }, [showMenu]); - const handleLogout = (e) => { e.preventDefault(); logout(); navigate("/"); }; - const navUserProfile = (e) => { e.preventDefault(); navigate('/profile') } - const ulClassName = "profile-dropdown" + (showMenu ? "" : " hidden"); const closeMenu = () => setShowMenu(false); - return ( <> +

Use default settings for a quick match.

+
+
+ +

Adjust settings to find your perfect match.

+
+
+
+ +
-
-
- Group Size: - props.onGroupSizeChange(e.target.value)} - /> - {props.groupSize} Persons -
-
- -
- - - -
- - ); + ); }; - -export default Scheduling; +export default Scheduling; \ No newline at end of file diff --git a/frontend/src/SCSS/Profile.css b/frontend/src/SCSS/Profile.css index 93956fd..9048283 100644 --- a/frontend/src/SCSS/Profile.css +++ b/frontend/src/SCSS/Profile.css @@ -1,26 +1,25 @@ .profile-container { - border: 1px solid #e0e0e0; - padding: 20px; - border-radius: 10px; - max-width: 400px; - margin: 20px auto; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - text-align: center; - } - - .profile-picture { - width: 300px; - height: 300px; - border-radius: 50%; - margin-bottom: 15px; - } - - .profile-detail, .profile-interests { - margin: 10px 0; - } - - .profile-interests ul { - list-style-type: none; - padding: 0; - } - \ No newline at end of file + border: 1px solid #E0E0E0; + padding: 20px; + border-radius: 10px; + max-width: 400px; + margin: 20px auto; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + text-align: center; +} +.profile-container h2 { + color: black +} +.profile-picture { + width: 300px; + height: 300px; + border-radius: 50%; + margin-bottom: 15px; +} +.profile-detail, .profile-interests { + margin: 10px 0; +} +.profile-interests ul { + list-style-type: none; + padding: 0; +} \ No newline at end of file diff --git a/frontend/src/SCSS/navigation.css b/frontend/src/SCSS/navigation.css index 61163ed..017dcbb 100644 --- a/frontend/src/SCSS/navigation.css +++ b/frontend/src/SCSS/navigation.css @@ -1,138 +1,137 @@ .profile-dropdown { + position: absolute; + right: 10px; + /* Changing left% to 'right' for better responsiveness and accessibility */ + top: auto; + /* May need to adjust based on surrounding containers */ + border: 1px solid #D3D3D3; + /* Replacing named colors with hex for precision */ + box-shadow: 0px 0px 10px #D3D3D3; + background-color: #FFFFFF; + width: auto; + /* Making the drop-down take full width on smaller screens */ + padding: 10px 10px; + /* Consolidating paddings into one line */ + max-width: 200px; + /* Giving a max-width to avoid overly-stretched dropdowns on larger screens */ + transition: all 0.3s ease-in-out; + /* Optional: Added transition for smoother display changes */ + border: 1px solid rgba(80, 102, 113, 0.75); + border-radius: 10px; +} +.hidden { + display: none; +} +.nav-icon > img { + height: 10px; +} +.logo-on-nav { + height: 54px !important; +} +.responsive-logo { + max-height: 54px; /* this ensures the image scales down if it's too wide for its container */ + width: auto; /* this maintains the aspect ratio of the image while scaling */ +} +.user-dropdown-menu > button { + background: none; + border: none; + font-size: 12px; + cursor: pointer; + display: flex; + flex-direction: column; +} +.drop-down-sign-out { + color: #E94C0A; +} +.nav-div { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + position: fixed; + top: 0px; + background-color: white; + border-bottom: black 1px solid; + width: 100%; +} +.nav-blank { + width: 55%; +} +.rest-of-nav { + display: flex; + align-items: center; + gap: 15px; + font-size: 12px; + max-height: 15px; +} +.nav-login > button { + background: none; + color: inherit; + border: none; + font-size: 12px; + cursor: pointer; +} +.nav-sign-up > button { + background: black; + color: white; + border-radius: 15px; + height: 25px; + cursor: pointer; +} +.nav-div-user-logged-in { + padding: 0px 10px 5px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 100px; + border-bottom: rgb(232, 232, 232) solid 0.5px; +} +.nav-blank-user-logged-in { + width: 900px; +} +.drop-down-profile-settings { + color: black; +} +.profile-create-user-logged-in { + display: flex; + flex-direction: row; + gap: 15px; +} +.user-profile-dropdown-button { + background: none; + border: none; + cursor: pointer; + display: flex; + gap: 2px; +} +.user-profile-dropdown-button:active, +.user-profile-dropdown-button:focus, +.user-profile-dropdown-button:hover { + /* Added hover here */ + outline: none; + box-shadow: none; + /* Optionally remove or change the box-shadow here to avoid the blue glow on hover */ + background-color: transparent; + /* Added this line to ensure no background color on hover */ +} +.profile-button-picture { + height: 50px !important; + color: lightgray; +} +@media only screen and (max-width: 600px) { + .profile-dropdown { position: absolute; - left: 87%; - top: 3%; + left: 60%; + top: 10%; border: 1px solid lightgrey; box-shadow: 0px 0px 20px lightgrey; background-color: white; - width: 6em; - padding-right: 25px; - padding-top: 10px; - padding-bottom: 10px; - } - - .hidden { - display: none; - } - - .nav-icon > img{ - height: 10px; - } - - .logo-on-nav { - height: 54px !important; - } - - .responsive-logo { - max-height: 54px ; /* this ensures the image scales down if it's too wide for its container */ - width: auto; /* this maintains the aspect ratio of the image while scaling */ -} - - .user-dropdown-menu > button{ - background: none; - border: none; - font-size: 12px; - cursor: pointer; - display: flex; - flex-direction: column; - } - - .drop-down-sign-out{ - color: red; - } - - .nav-div { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - position: fixed; - top: 0px; - background-color: white; - border-bottom: black 1px solid; - width:100% - } - - - .nav-blank { - width: 55% - } - - .rest-of-nav { - display: flex; - align-items: center; - gap: 15px; - font-size: 12px; - max-height: 15px - } - - .nav-login > button{ - background: none; - color: inherit; - border: none; - font-size: 12px; - cursor: pointer; - } - .nav-sign-up > button { - background: black; - color: white; - border-radius: 15px; - height: 25px; - cursor: pointer; - } - - .nav-div-user-logged-in { - padding: 0px 10px 5px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - height: 100px; - border-bottom: rgb(232, 232, 232) solid 0.5px; - } - - - .nav-blank-user-logged-in{ - width: 900px - } - - .drop-down-profile-settings{ - color: black; - } - .profile-create-user-logged-in { - display: flex; - flex-direction: row; - gap: 15px; - } - - - .user-profile-dropdown-button { - background: none; - border: none; - cursor: pointer; - display: flex; - gap: 2px - } - - .profile-button-picture { - height: 50px !important; - color: lightgray + width: 5em; + /* padding-top: 10px; + padding-bottom: 10px; */ } - - - @media only screen and (max-width: 600px) { - .profile-dropdown { - position: absolute; - left: 65%; - top: 5%; - border: 1px solid lightgrey; - box-shadow: 0px 0px 20px lightgrey; - background-color: white; - width: 5em; - padding-top: 10px; - padding-bottom: 10px; - } - .drop-down-sign-out { + .drop-down-sign-out { margin-top: 10px; - } } +} \ No newline at end of file diff --git a/frontend/src/SCSS/scheduling.css b/frontend/src/SCSS/scheduling.css index c162c06..1b766d1 100644 --- a/frontend/src/SCSS/scheduling.css +++ b/frontend/src/SCSS/scheduling.css @@ -1,79 +1,184 @@ .scheduling-container { - width: 100%; - height: 250px; - margin-top: 50px; - margin-bottom: 150px; + width: 80%; /* Adjusted for better desktop presentation */ + max-width: 1200px; /* Added for limiting width on very large screens */ + margin: 50px auto; /* Adjusted to center the container horizontally */ display: flex; flex-direction: column; align-items: center; } - .scheduling-header { - font-size: 35px; - margin-bottom: 50px; + color: #007CAA; + font-size: x-large; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-bottom: 3%; } - - .time-dropdown-guests-slider { display: flex; - gap: 30px; + gap: 2em; align-items: center; - padding: 20px; - background-color: #f6f6f6; - border-radius: 8px; - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); + padding: 1em; + /* background-color: #F6F6F6; */ + /* border-radius: 8px; */ + /* box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); */ + margin-bottom: 3%; + justify-content: space-between; + width: 60%; } - -.time-dropdown, .guests-slider { +.time-dropdown, +.guests-slider { display: flex; flex-direction: column; gap: 10px; } - -.time-dropdown strong, .guests-slider strong { +.time-dropdown strong, +.guests-slider strong { font-size: 20px; - margin-bottom: 10px; + /* margin-bottom: 10px; */ color: #333; } - select { font-size: 18px; padding: 10px 15px; border-radius: 8px; border: 1px solid #ccc; - appearance: none; /* This removes the default OS styling */ + appearance: none; background-color: #fff; background-image: url('data:image/svg+xml;utf8,'); background-repeat: no-repeat; background-position: right 15px center; cursor: pointer; } - -input[type="range"] { +input[type='range'] { width: 100%; cursor: pointer; } - -select, input[type="range"] { - margin-left: 10px; /* added this line */ +select, +input[type='range'] { + margin-left: 10px; } - - .slider { display: flex; align-items: center; gap: 20px; } - - .button-div { width: 100%; display: flex; justify-content: center; - gap: 50px + gap: 50px; + flex-wrap: wrap; +} +.button-div > button { + display: block; + /* Added for centering button */ + width: 100%; + height: 10em; + /* Make the height scalable for different screens */ + margin: 0 auto; + /* Center the button */ + padding: 0.5em 1em; + /* Make the button more clickable */ + border: 1px solid rgba(80, 102, 113, 0.75); + border-radius: 10px; + background: #fff; + color: #007CAA; + text-align: center; + font-size: 1rem; + /* Make the font size scalable */ + transition: all 0.3s ease; + /* Added for smooth hover and focus transitions */ +} +.button-div > button:hover, +.button-div > button:focus { + background: #007CAA; + /* Change background colour when hovered or focused */ + color: #fff; + /* Change text color when hovered or focused */ + border: 1px solid #007CAA; + /* Change border color when hovered or focused */ +} +@media (max-width: 768px) { + .scheduling-container { + width: 100%; /* Adjusted for better mobile presentation */ + padding-bottom: 20px; + } + .scheduling-header { + text-align: center; + } + .time-dropdown-guests-slider { + flex-direction: column; + gap: 20px; + } + .button-div { + flex-direction: column; + align-items: center; + } + .button-div > button { + width: 100%; + height: auto; + padding: 15px; + margin: 10px 0; + } +} +.actions-container { + display: flex; + justify-content: space-around; /* Adjusted for better spacing */ + width: 100%; + margin-top: 20px; +} +.quick-actions, +.advanced-actions { + display: flex; + flex-direction: column; + align-items: center; + width: 45%; /* Adjusted to fit better side by side */ +} +.quick-actions p, +.advanced-actions p { + font-size: auto; + color: #777; + text-align: center; +} +.view-all-container { + margin-top: 30px; + width: 100%; + display: flex; + justify-content: center; +} +.view-all-button { + display: block; + /* Added for centering button */ + width: 95%; + height: 10em; + /* Make the height scalable for different screens */ + margin: 0 auto; + /* Center the button */ + padding: 0.5em 1em; + /* Make the button more clickable */ + border: 1px solid rgba(80, 102, 113, 0.75); + border-radius: 10px; + background: #fff; + color: #007CAA; + text-align: center; + font-size: 1rem; + /* Make the font size scalable */ + transition: all 0.3s ease; + /* Added for smooth hover and focus transitions */ } -.button-div > button{ - width: 250px; - height: 150px; - margin: 50px +.view-all-button:hover, +.view-all-button:focus { + background: #007CAA; + /* Change background colour when hovered or focused */ + color: #fff; + /* Change text color when hovered or focused */ + border: 1px solid #007CAA; + /* Change border color when hovered or focused */ } +.randomize-button { + height: 100px !important; + width: 250px !important; + font-size: 25px; + margin-top: 25px; +} \ No newline at end of file diff --git a/frontend/src/store/session.js b/frontend/src/store/session.js index 09ec8b6..e5ccf60 100644 --- a/frontend/src/store/session.js +++ b/frontend/src/store/session.js @@ -1,7 +1,10 @@ +import { supabase } from "../supabaseclient"; + // constants const SET_USER = "session/SET_USER"; const REMOVE_USER = "session/REMOVE_USER"; + const setUser = (user) => ({ type: SET_USER, payload: user, @@ -30,26 +33,24 @@ export const authenticate = () => async (dispatch) => { }; export const login = (email, password) => async (dispatch) => { - const response = await fetch("/api/auth/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - password, - }), - }); + const response = await supabase.auth.signInWithPassword({ + email: email, + password: password, + options: { + redirectTo: 'https//example.com/welcome' + } + }) + console.log('RESPONSE', response) - if (response.ok) { - const data = await response.json(); + if (response.data.user) { + const data = response.data + console.log('DATA', data) dispatch(setUser(data)); return data; - } else if (response.status < 500) { - const data = await response.json(); - if (data.errors) { - return data.errors; - } + } else if (response.error) { + const data = response.error + console.log(data) + return data.errors; } else { return ["An error occurred. Please try again."]; }