diff --git a/.gitignore b/.gitignore index 5710fbaa..6293debb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ # dependencies node_modules +.pnp +.pnp.js +.yarn/install-state.gz # virtual environments mycity-venv/ @@ -16,5 +19,34 @@ __pycache__/ .env.development .env.production +# testing +coverage + +# next.js +.next/ +out/ + +# production +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + # next.js transpiler .swc + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# jest & playwright test results +/test-results diff --git a/backend/app.py b/backend/app.py index 51c94979..5c2c2e27 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,6 +4,7 @@ from chalicelib.issues.issues_routes import issues_routes from chalicelib.tickets.tickets_routes import tickets_blueprint from chalicelib.searching.searching_routes import searching_blueprint +from chalicelib.municipalities.municipalities_routes import municipalities_blueprint app = Chalice(app_name="mycity") cors_config = CORSConfig( @@ -20,6 +21,8 @@ app.register_blueprint(searching_blueprint, "Search", "/search") +app.register_blueprint(municipalities_blueprint, "Municipality", "/municipality") + @app.route("/", cors=True) def index(): diff --git a/backend/chalicelib/municipalities/municipalities_controllers.py b/backend/chalicelib/municipalities/municipalities_controllers.py new file mode 100644 index 00000000..8f653160 --- /dev/null +++ b/backend/chalicelib/municipalities/municipalities_controllers.py @@ -0,0 +1,39 @@ +import boto3 +from botocore.exceptions import ClientError +from chalice import BadRequestError, Response +import json + +dynamodb = boto3.resource("dynamodb") +municipalities_table = dynamodb.Table("municipalities") + + +def format_response(status_code, body): + return Response( + body=json.dumps(body), + status_code=status_code, + headers={ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", + "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key", + }, + ) + + +def get_all_municipalities(): + try: + response = municipalities_table.scan() + municipalities = response.get("Items", []) + + # Note that only the name of the municipality is being fetched + municipalities_list = [ + { + "municipality_id": municipality["municipality_id"], + } + for municipality in municipalities + ] + + return format_response(200, municipalities_list) + + except ClientError as e: + error_message = e.response["Error"]["Message"] + return {"Status": "FAILED", "Error": error_message} diff --git a/backend/chalicelib/municipalities/municipalities_routes.py b/backend/chalicelib/municipalities/municipalities_routes.py new file mode 100644 index 00000000..4c30741d --- /dev/null +++ b/backend/chalicelib/municipalities/municipalities_routes.py @@ -0,0 +1,13 @@ +from chalice import Blueprint, BadRequestError, Response +from chalicelib.municipalities.municipalities_controllers import ( + get_all_municipalities, +) + +municipalities_blueprint = Blueprint(__name__) + + +@municipalities_blueprint.route("/municipalities-list", methods=["GET"], cors=True) +# Note that only the name of the municipality is being fetched +def get_all_municipalities_list(): + municipalities_list = get_all_municipalities() + return municipalities_list diff --git a/backend/chalicelib/profiles/__init__.py b/backend/chalicelib/profiles/__init__.py deleted file mode 100644 index 0095f54a..00000000 --- a/backend/chalicelib/profiles/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# This file can be left empty or used for package-level imports and initialization diff --git a/backend/chalicelib/profiles/profiles_controllers.py b/backend/chalicelib/profiles/profiles_controllers.py deleted file mode 100644 index d878277e..00000000 --- a/backend/chalicelib/profiles/profiles_controllers.py +++ /dev/null @@ -1,106 +0,0 @@ -import boto3 -from botocore.exceptions import ClientError -import hashlib -import re - -dynamodb = boto3.resource("dynamodb") -table = dynamodb.Table("users") - - -def is_valid_email(email): - return re.match(r"[^@]+@[^@]+\.[^@]+", email) is not None - - -def is_valid_cellphone(cellphone): - return re.match(r"^0[0-9]{9}$", cellphone) is not None - - -def is_valid_dob(dob): - return re.match(r"^\d{4}\\d{2}\\d{2}$", dob) is not None - - -def hash_password(password): - return hashlib.md5(password.encode()).hexdigest() - - -def is_unique_email(email): - try: - response = table.scan( - FilterExpression="email = :email", - ExpressionAttributeValues={":email": email}, - ) - return response["Count"] == 0 - except ClientError as e: - return False - - -def update_profile(username, profile_data): - try: - # Fetch the current details of the user - response = table.get_item(Key={"username": username}) - if "Item" not in response: - return {"status": "error", "message": "User not found"} - - current_data = response["Item"] - - email = profile_data.get("email", current_data["email"]) - cellphone = profile_data.get("cellphone", current_data["cellphone"]) - dob = profile_data.get("dob", current_data["dob"]) - firstname = profile_data.get("first", current_data["first"]) - surname = profile_data.get("surname", current_data["surname"]) - password = profile_data.get("password") - municipality_name = profile_data.get( - "municipality_name", current_data["municipality_name"] - ) - - # Check if any required field is missing - if not all([email, cellphone, dob, firstname, surname, municipality_name]): - return {"status": "error", "message": "All profile fields are required"} - - # Validate data - if not is_valid_email(email): - return {"status": "error", "message": "Invalid email format"} - if not is_valid_cellphone(cellphone): - return {"status": "error", "message": "Invalid cellphone format"} - if not is_valid_dob(dob): - return {"status": "error", "message": "Invalid date of birth format"} - if email != current_data["email"] and not is_unique_email(email): - return {"status": "error", "message": "Email already exists"} - - hashed_password = ( - hash_password(password) if password else current_data["password"] - ) - - # Update the user's details - update_expression = "set " - expression_attribute_values = { - ":email": email, - ":cellphone": cellphone, - ":dob": dob, - ":first": firstname, - ":surname": surname, - ":password": hashed_password, - ":municipality_name": municipality_name, - } - - update_expression += "email = :email, " - update_expression += "cellphone = :cellphone, " - update_expression += "dob = :dob, " - update_expression += "first = :first, " - update_expression += "surname = :surname, " - update_expression += "password = :password, " - update_expression += "municipality_name = :municipality_name, " - - update_expression = update_expression.rstrip(", ") - - response = table.update_item( - Key={"username": username}, - UpdateExpression=update_expression, - ExpressionAttributeValues=expression_attribute_values, - ReturnValues="UPDATED_NEW", - ) - - return {"status": "success", "updated_attributes": response.get("Attributes")} - - except ClientError as e: - return {"status": "error", "message": str(e)} diff --git a/backend/chalicelib/profiles/profiles_routes.py b/backend/chalicelib/profiles/profiles_routes.py deleted file mode 100644 index f684f2ba..00000000 --- a/backend/chalicelib/profiles/profiles_routes.py +++ /dev/null @@ -1,28 +0,0 @@ -from chalice import Blueprint, Response -from chalicelib.profiles.profiles_controllers import update_citizen_profile - -profile_routes = Blueprint(__name__) - - -@profile_routes.route( - "/profile/update", methods=["PUT"], content_types=["application/json"] -) -def update_user_profile(): - request = profile_routes.current_request - profile_data = request.json_body - username = request.context["authorizer"]["username"] - - if not username: - return Response(body={"error": "Unauthorized"}, status_code=401) - - result = update_citizen_profile(username, profile_data) - if result["status"] == "error": - return Response(body={"error": result["message"]}, status_code=400) - - return Response( - body={ - "message": "Profile updated successfully", - "updated_attributes": result["updated_attributes"], - }, - status_code=200, - ) diff --git a/backend/chalicelib/tickets/tickets_controllers.py b/backend/chalicelib/tickets/tickets_controllers.py index 4d00f6d0..e08f4d6e 100644 --- a/backend/chalicelib/tickets/tickets_controllers.py +++ b/backend/chalicelib/tickets/tickets_controllers.py @@ -220,6 +220,14 @@ def get_in_my_municipality(tickets_data): "municipality_id", ] for field in required_fields: + if tickets_data == None: + error_response = { + "Error": { + "Code": "Nothing", + "Message": f"No data has been sent", + } + } + raise ClientError(error_response, "NothingSent") if field not in tickets_data: error_response = { "Error": { diff --git a/backend/requirements.txt b/backend/requirements.txt index 5b07d0c3..d6e3c937 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,10 +3,10 @@ bcrypt==4.1.3 black==24.4.2 blessed==1.20.0 blinker==1.8.2 -boto3==1.34.117 -botocore==1.34.111 +boto3==1.34.132 +botocore==1.34.132 certifi==2024.6.2 -chalice==1.31.1 +chalice==1.31.2 chardet==5.2.0 charset-normalizer==3.3.2 click==8.1.7 @@ -21,9 +21,10 @@ jinxed==1.2.1 jmespath==1.0.1 MarkupSafe==2.1.5 mypy-extensions==1.0.0 -packaging==24.0 +packaging==24.1 pathspec==0.12.1 platformdirs==4.2.2 +pur==7.3.2 pyasn1==0.6.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 @@ -31,8 +32,10 @@ python-editor==1.0.4 PyYAML==6.0.1 readchar==4.1.0 requests==2.32.3 -s3transfer==0.10.1 +s3transfer==0.10.2 six==1.16.0 -urllib3==2.2.1 +tomli==2.0.1 +typing_extensions==4.12.2 +urllib3==2.2.2 wcwidth==0.2.13 Werkzeug==3.0.3 diff --git a/frontend/__tests__/components/Dashboard/FaultCardContainer.test.tsx b/frontend/__tests__/components/Dashboard/FaultCardContainer.test.tsx new file mode 100644 index 00000000..0054634e --- /dev/null +++ b/frontend/__tests__/components/Dashboard/FaultCardContainer.test.tsx @@ -0,0 +1,58 @@ +import CitizenLogin from "@/components/Login/CitizenLogin"; +import { fireEvent, render, screen } from "@testing-library/react"; + +describe("Dashboard", () => { + + + /* it("renders an email input", () => { + render(); + const emailInput = screen.getByLabelText("Email"); + + expect(emailInput).toBeInTheDocument(); + expect(emailInput).toHaveAttribute("type", "email"); + }); */ + + it("renders an email input", () => { + render(); + // Use getByPlaceholderText if the placeholder is unique + const emailInput = screen.getByPlaceholderText("example@mail.com"); + + expect(emailInput).toBeInTheDocument(); + expect(emailInput).toHaveAttribute("type", "email"); + }); + + // it("renders a password input", () => { + // render(); + // const passwordInput = screen.getByLabelText("Password"); + + // expect(passwordInput).toBeInTheDocument(); + // expect(passwordInput).toHaveAttribute("type", "password"); + // }); + + + // it("renders a forgot password link", () => { + // render(); + // const forgotPasswordLink = screen.getByText("Forgot password?"); + + // expect(forgotPasswordLink).toBeInTheDocument(); + // }); + + // it("renders a Login button", () => { + // render(); + // const submitButton = screen.getByTestId("submit-btn") + + // expect(submitButton).toBeInTheDocument(); + // }); + + + // test("handler function is called after clicking submit button", () => { + // render(); + // const mockFunction = jest.fn(); + // const loginForm = screen.getByTestId("citizen-login-form"); + // loginForm.addEventListener("submit", mockFunction); + + // fireEvent.submit(loginForm) + // expect(mockFunction).toHaveBeenCalledTimes(1); + // }); + +}); \ No newline at end of file diff --git a/frontend/__tests__/components/Login/CitizenLogin.test.tsx b/frontend/__tests__/components/Login/CitizenLogin.test.tsx index 4cbfe299..a03eda46 100644 --- a/frontend/__tests__/components/Login/CitizenLogin.test.tsx +++ b/frontend/__tests__/components/Login/CitizenLogin.test.tsx @@ -3,15 +3,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; describe("CitizenLogin", () => { - - /* it("renders an email input", () => { - render(); - const emailInput = screen.getByLabelText("Email"); - - expect(emailInput).toBeInTheDocument(); - expect(emailInput).toHaveAttribute("type", "email"); - }); */ - it("renders an email input", () => { render(); // Use getByPlaceholderText if the placeholder is unique @@ -21,38 +12,39 @@ describe("CitizenLogin", () => { expect(emailInput).toHaveAttribute("type", "email"); }); - // it("renders a password input", () => { - // render(); - // const passwordInput = screen.getByLabelText("Password"); + it("renders a password input", () => { + render(); + const passwordInput = screen.getByLabelText(/Password/); - // expect(passwordInput).toBeInTheDocument(); - // expect(passwordInput).toHaveAttribute("type", "password"); - // }); + expect(passwordInput).toBeInTheDocument(); + expect(passwordInput).toHaveAttribute("type", "password"); + }); - // it("renders a forgot password link", () => { - // render(); - // const forgotPasswordLink = screen.getByText("Forgot password?"); + it("renders a forgot password link", () => { + render(); + const forgotPasswordLink = screen.getByText("Forgot password?"); - // expect(forgotPasswordLink).toBeInTheDocument(); - // }); + expect(forgotPasswordLink).toBeInTheDocument(); + }); - // it("renders a Login button", () => { - // render(); - // const submitButton = screen.getByTestId("submit-btn") + it("renders a Login button", () => { + render(); + const submitButton = screen.getByTestId("submit-btn") - // expect(submitButton).toBeInTheDocument(); - // }); + expect(submitButton).toBeInTheDocument(); + }); - // test("handler function is called after clicking submit button", () => { - // render(); - // const mockFunction = jest.fn(); - // const loginForm = screen.getByTestId("citizen-login-form"); - // loginForm.addEventListener("submit", mockFunction); + test("handler function is called after clicking submit button", () => { + render(); + const mockFunction = jest.fn(); + // const mockLogin = handleSignIn.mockResolvedValue({ status: 200 }); + const loginForm = screen.getByTestId("citizen-login-form"); + loginForm.addEventListener("submit", mockFunction); - // fireEvent.submit(loginForm) - // expect(mockFunction).toHaveBeenCalledTimes(1); - // }); + fireEvent.submit(loginForm) + expect(mockFunction).toHaveBeenCalledTimes(1); + }); }); \ No newline at end of file diff --git a/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx b/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx index 081ada73..c142c350 100644 --- a/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx +++ b/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx @@ -3,14 +3,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; describe("MunicipalityLogin", () => { - /*it("renders an email input", () => { - render(); - const emailInput = screen.getByLabelText("Email"); - - expect(emailInput).toBeInTheDocument(); - expect(emailInput).toHaveAttribute("type", "email"); - });*/ - it("renders an email input", () => { render(); // Use getByPlaceholderText if the placeholder is unique @@ -20,36 +12,31 @@ describe("MunicipalityLogin", () => { expect(emailInput).toHaveAttribute("type", "email"); }); - // it("renders textbox to select municipality", () => { - // render(); - // const muniAutocomplete = screen.getByRole("combobox"); - // expect(muniAutocomplete).toHaveAttribute("type", "text"); - // }); + it("renders a password input", () => { + render(); + const passwordInput = screen.getByLabelText(/Municipality Password/); + expect(passwordInput).toHaveAttribute("type", "password"); + }); - // it("renders a password input", () => { - // render(); - // const passwordInput = screen.getByPlaceholderText("Password"); - // expect(passwordInput).toHaveAttribute("type", "password"); - // }); + it("renders a submit button", () => { + render(); - // it("renders a submit button", () => { - // render(); + const submitButton = screen.getByTestId("submit-btn") - // const submitButton = screen.getByRole("button", { name: /submit/i }); + expect(submitButton).toBeInTheDocument(); - // expect(submitButton).toBeInTheDocument(); - // expect(submitButton).toHaveTextContent("Submit"); - // }); + expect(submitButton).toHaveTextContent("Submit"); + }); - // test("handler function is called after clicking submit button", () => { - // render(); - // const mockFunction = jest.fn(); - // const loginForm = screen.getByTestId("municipality-login-form"); - // loginForm.addEventListener("submit", mockFunction); + test("handler function is called after clicking submit button", () => { + render(); + const mockFunction = jest.fn(); + const loginForm = screen.getByTestId("municipality-login-form"); + loginForm.addEventListener("submit", mockFunction); - // fireEvent.submit(loginForm) - // expect(mockFunction).toHaveBeenCalledTimes(1); - // }); + fireEvent.submit(loginForm) + expect(mockFunction).toHaveBeenCalledTimes(1); + }); }); \ No newline at end of file diff --git a/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx b/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx index ebda9322..1c4f27a1 100644 --- a/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx +++ b/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx @@ -3,14 +3,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; describe("ServiceProviderLogin", () => { - /*it("renders an email input", () => { - render(); - const emailInput = screen.getByLabelText("Email"); - - expect(emailInput).toBeInTheDocument(); - expect(emailInput).toHaveAttribute("type", "email"); - });*/ - it("renders an email input", () => { render(); // Use getByPlaceholderText if the placeholder is unique @@ -20,42 +12,34 @@ describe("ServiceProviderLogin", () => { expect(emailInput).toHaveAttribute("type", "email"); }); - // it("renders a password input", () => { - // render(); - // const passwordInput = screen.getByLabelText("Password"); - - // expect(passwordInput).toBeInTheDocument(); - // expect(passwordInput).toHaveAttribute("type", "password"); - // }); - + it("renders a password input", () => { + render(); + const passwordInput = screen.getByLabelText(/Service Provider Password/); - // it("renders a forgot password link", () => { - // render(); + expect(passwordInput).toBeInTheDocument(); + expect(passwordInput).toHaveAttribute("type", "password"); + }); - // const forgotPasswordLink = screen.getByRole("link"); - // expect(forgotPasswordLink).toBeInTheDocument(); - // expect(forgotPasswordLink).toHaveTextContent("Forgot password?"); - // }); + it("renders a submit button", () => { + render(); - // it("renders a submit button", () => { - // render(); + const submitButton = screen.getByTestId("submit-btn") - // const submitButton = screen.getByRole("button", { name: /submit/i }); + expect(submitButton).toBeInTheDocument(); - // expect(submitButton).toBeInTheDocument(); - // expect(submitButton).toHaveTextContent("Submit"); - // }); + expect(submitButton).toHaveTextContent("Submit"); + }); - // test("handler function is called after clicking submit button", () => { - // render(); - // const mockFunction = jest.fn(); - // const loginForm = screen.getByTestId("service-provider-login-form"); - // loginForm.addEventListener("submit", mockFunction); + test("handler function is called after clicking submit button", () => { + render(); + const mockFunction = jest.fn(); + const loginForm = screen.getByTestId("service-provider-login-form"); + loginForm.addEventListener("submit", mockFunction); - // fireEvent.submit(loginForm); - // expect(mockFunction).toHaveBeenCalledTimes(1); - // }); + fireEvent.submit(loginForm); + expect(mockFunction).toHaveBeenCalledTimes(1); + }); }); \ No newline at end of file diff --git a/frontend/__tests__/components/Navbar/Navbar.test.tsx b/frontend/__tests__/components/Navbar/Navbar.test.tsx new file mode 100644 index 00000000..23b047c9 --- /dev/null +++ b/frontend/__tests__/components/Navbar/Navbar.test.tsx @@ -0,0 +1,71 @@ +import NavbarUser from "@/components/Navbar/NavbarUser"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { act } from "react"; + + +describe("Authenticated user Navbar", () => { + + it("renders a dashboard link", () => { + + act(() => { + render(); + }); + + const dashboardLink = screen.getByRole("link", {name:/Dashboard/}); + + expect(dashboardLink).toBeInTheDocument(); + expect(dashboardLink).toHaveAttribute("href", "/dashboard/citizen"); + }); + + + + test("logout button triggers logout event", () => { + + act(() => { + render(); + }); + + const profileAvatar = screen.getByTestId("profile-dropdown-trigger"); + + act(() => { + fireEvent.click(profileAvatar); + + }); + + const logoutButton = screen.getByText(/Log out/); + + + const mockFunction = jest.fn(); + logoutButton.addEventListener("click", mockFunction); + + expect(logoutButton).toBeInTheDocument(); + act(() => { + logoutButton.click(); + }); + + expect(mockFunction).toHaveBeenCalledTimes(1); + }); + + + +}); + +// describe("Unauthenticated user Navbar", () => { + +// it("renders an email input", () => { +// render(); +// // Use getByPlaceholderText if the placeholder is unique +// const emailInput = screen.getByPlaceholderText("example@mail.com"); + +// expect(emailInput).toBeInTheDocument(); +// expect(emailInput).toHaveAttribute("type", "email"); +// }); + +// it("renders a password input", () => { +// render(); +// const passwordInput = screen.getByLabelText(/Password/); + +// expect(passwordInput).toBeInTheDocument(); +// expect(passwordInput).toHaveAttribute("type", "password"); +// }); +// }); \ No newline at end of file diff --git a/frontend/e2e/help.spec.ts b/frontend/e2e/help.spec.ts new file mode 100644 index 00000000..64671def --- /dev/null +++ b/frontend/e2e/help.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; + +test.describe("help menu", () => { + + test("user can access help menu on any page", async ({ page }) => { + await page.goto("http://localhost:3000/"); + await page.waitForTimeout(5000); + + // login + await page.getByTestId("login-btn").click(); + + await page.waitForTimeout(5000); + + await page.getByLabel("Email").fill("janedoe@example.com"); + await page.getByLabel("Password").fill("Password@123"); + + await page.waitForTimeout(5000); + + // click on login button + await page.getByTestId("submit-btn").click(); + + await page.waitForURL("http://localhost:3000/dashboard/citizen"); + + // await page.waitForTimeout(5000); + // page.getByTestId("open-help-menu").click(); + // expect(page.getByTestId("help")).toBeVisible(); + + // await page.waitForTimeout(5000); + // page.getByTestId("close-help-menu").click(); + + await page.waitForTimeout(5000); + + await page.goto("http://localhost:3000/search/citizen"); + + + + //try search + page.getByTestId("searchbox").fill("mak"); + page.getByTestId("filter").click(); + await page.waitForTimeout(1000); + await page.getByText(/Municipality Tickets/).click(); + page.getByTestId("search-btn").click(); + + await page.waitForTimeout(10000); + + // page.getByTestId("open-help-menu").click(); + // expect(page.getByTestId("help")).toBeVisible(); + // await page.waitForTimeout(5000); + // page.getByTestId("close-help-menu").click(); + + // await page.waitForTimeout(5000); + + + await page.goto("http://localhost:3000/settings/citizen"); + page.getByTestId("open-help-menu").click(); + expect(page.getByTestId("help")).toBeVisible(); + // await page.waitForTimeout(5000); + // page.getByTestId("close-help-menu").click(); + + // await page.waitForTimeout(5000); + + }); + +}); \ No newline at end of file diff --git a/frontend/e2e/login.spec.ts b/frontend/e2e/login.spec.ts index 172d0f4d..d1996169 100644 --- a/frontend/e2e/login.spec.ts +++ b/frontend/e2e/login.spec.ts @@ -1,22 +1,35 @@ import { test, expect } from '@playwright/test'; -test("citizen login takes them to dashboard", async ({ page }) => { - await page.goto("http://localhost:3000/"); +test.describe("login", () => { - expect(page.url()).toEqual("http://localhost:3000"); + test("unauthenticated citizen cannot access dashboard", async ({ page }) => { + await page.goto("http://localhost:3000/"); - // click the login link - // await page.getByText("Login").click(); + await page.goto("http://localhost:3000/dashboard/citizen"); - // // fill in email - // await page.getByLabel("Email").fill("james@gmail.com"); + expect(page.url()).not.toEqual("http://localhost:3000/dashboard/citizen"); + }); - // // fill in password - // await page.getByLabel("Password").fill("password"); - // // click on login button - // await page.getByText("Login").click(); - // // expects page url to not have changed - // expect(page.url()).toEqual("http://localhost:3000/dashboard"); -}); + test("citizen is redirected to dashboard on successful login", async ({ page }) => { + await page.goto("http://localhost:3000/"); + + // click the login link + await page.getByText("Log In").click(); + + // fill in email + await page.getByLabel("Email").fill("janedoe@example.com"); + + // fill in password + await page.getByLabel("Password").fill("Password@123"); + + // click on login button + await page.getByText("Log In").click(); + + // expects page url to not have changed + + expect(page.url()).toEqual("http://localhost:3000/dashboard/citizen"); + }); + +}); \ No newline at end of file diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index 7da0d534..f2296895 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -2,12 +2,29 @@ import "@testing-library/jest-dom"; jest.mock('next/navigation', () => ({ usePathname: jest.fn(), - useRouter: jest.fn() + useRouter: jest.fn(() => { return { push: (url: string) => { return true; } } }) })); +jest.mock('./src/services/auth.service', () => ({ + handleSignIn: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }), + handleSignUp: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }), + authenticateClient: jest.fn(() => { return false; }), + handleSignOut: jest.fn(), + // signIn: jest.fn(() => { const isSignedIn = true; return isSignedIn; }), + // fetchAuthSession: jest.fn(), + // autoSignIn: jest.fn() +})); + +jest.mock('./src/utils/authActions'); +jest.mock('./src/hooks/useProfile', () => ({ + useProfile: () => { return { getUserProfile: jest.fn(() => { return { current: {} }; }) } }, +})); + // jest.mock('aws-amplify/auth', () => ({ -// signUp: jest.fn(), -// signIn:jest.fn(), -// signOut:jest.fn() +// signIn: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }), +// signUp: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }), +// signOut: jest.fn(), +// fetchAuthSession: jest.fn(() => { return { session: { tokens: {} } } }), +// // autoSignIn: jest.fn() // })); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index c749a69e..fe3a1358 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,23 +1,22 @@ /** @type {import('next').NextConfig} */ const nextConfig = { env: { - GOOGLE_MAPS_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, USER_POOL_ID: process.env.NEXT_PUBLIC_USER_POOL_ID, USER_POOL_CLIENT_ID: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID, MAPBOX_ACCESS_TOKEN: process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN, - PLACEKIT_API_KEY: process.env.NEXT_PUBLIC_PLACEKIT_API_KEY + PLACEKIT_API_KEY: process.env.NEXT_PUBLIC_PLACEKIT_API_KEY, + API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL }, - images: { - unoptimized: !process.env.PROD_ENV ? true : false, - unoptimized: true, - remotePatterns: [ - { - protocol: "https", - hostname: "i.imgur.com", - }, - ], - }, + // images: { + // unoptimized: process.env.NODE_ENV != "production" ? true : false, + // remotePatterns: [ + // { + // protocol: "https", + // hostname: "i.imgur.com", + // }, + // ], + // }, webpack(config) { // Grab the existing rule that handles SVG imports diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bca014dd..2d881cdc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,6 @@ "@svgr/webpack": "^8.1.0", "@types/react-modal": "^3.16.3", "aws-amplify": "^6.3.6", - "axios": "^1.7.2", "framer-motion": "^11.2.6", "lucide-react": "^0.383.0", "mapbox-gl": "^3.4.0", @@ -20955,7 +20954,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/auto-bind": { "version": "4.0.0", @@ -21519,16 +21519,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -22424,6 +22414,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -23153,6 +23144,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -24424,25 +24416,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -24471,6 +24444,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -27482,6 +27456,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -27490,6 +27465,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -28857,11 +28833,6 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b7810bc1..d87c1c5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest", + "test": "jest --silent", "test:watch": "jest --watch", "test:e2e": "playwright test" }, @@ -20,7 +20,6 @@ "@svgr/webpack": "^8.1.0", "@types/react-modal": "^3.16.3", "aws-amplify": "^6.3.6", - "axios": "^1.7.2", "framer-motion": "^11.2.6", "lucide-react": "^0.383.0", "mapbox-gl": "^3.4.0", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 0d18b5fd..2b3aa8cc 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -12,6 +12,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', outputDir: 'test-results/playwright/results', + timeout: 120000, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ diff --git a/frontend/src/app/api/[...slug]/route.ts b/frontend/src/app/api/[...slug]/route.ts new file mode 100644 index 00000000..d6afc811 --- /dev/null +++ b/frontend/src/app/api/[...slug]/route.ts @@ -0,0 +1,50 @@ +import { NextRequest } from "next/server"; + +const API_BASE_URL = process.env.API_BASE_URL; + +export async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) { + + if (!API_BASE_URL) { + throw new Error("missing api base url"); + } + + const endpointUrl = req.url.replace("http://localhost:3000", API_BASE_URL); + const etag = params.slug.join("-"); + + const res = await fetch(endpointUrl, { + headers: req.headers, + next: { tags: [etag] }, + cache: "force-cache" + }); + + const data = await res.json(); + + return Response.json({ data }); +} + + +export async function POST(req: NextRequest, { params }: { params: { slug: string[] } }) { + + if (!API_BASE_URL) { + throw new Error("missing api base url"); + } + + const endpointUrl = req.url.replace("http://localhost:3000", API_BASE_URL); + const etag = params.slug.join("-"); + + const requestBody = await req.json(); + + const res = await fetch(endpointUrl, { + method: "POST", + headers: req.headers, + body: JSON.stringify(requestBody), + next: { tags: [etag] }, + cache: "force-cache" + }); + + + const data = await res.json(); + + + return Response.json({ data }); +} \ No newline at end of file diff --git a/frontend/src/app/dashboard/citizen/page.tsx b/frontend/src/app/dashboard/citizen/page.tsx index fb93db1e..5bade657 100644 --- a/frontend/src/app/dashboard/citizen/page.tsx +++ b/frontend/src/app/dashboard/citizen/page.tsx @@ -1,18 +1,15 @@ -"use client"; +'use client' import React, { Key, useEffect, useRef, useState } from "react"; import { Tabs, Tab } from "@nextui-org/react"; -import FaultCardContainer from "@/components/FaultCardContainer/FaultCardContainer"; import FaultTable from "@/components/FaultTable/FaultTable"; import FaultMapView from "@/components/FaultMapView/FaultMapView"; import Navbar from "@/components/Navbar/Navbar"; -import { useProfile } from "@/context/UserProfileContext"; -import { FaQuestionCircle, FaTimes } from "react-icons/fa"; +import { FaTimes } from "react-icons/fa"; import { HelpCircle } from "lucide-react"; import DashboardFaultCardContainer from "@/components/FaultCardContainer/DashboardFualtCardContainer"; -import axios from "axios"; - - +import { useProfile } from "@/hooks/useProfile"; +import { getTicket, getTicketsInMunicipality } from "@/services/tickets.service"; export default function CitizenDashboard() { const user = useRef(null); @@ -24,30 +21,26 @@ export default function CitizenDashboard() { useEffect(() => { const fetchData = async () => { try { - const user_data = await userProfile.getUserProfile() - const user_id = user_data.current?.sub - const rspwatchlist = await axios.post('https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/tickets/view?ticket_id=8f4cf09d-754e-4d71-96dc-952173fab07c',{ - username : user_id - }); - const municipality = user_data.current?.municipality - const rspmunicipality = await axios.post('https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/tickets/getinarea',{ - municipality_id : municipality - }); - console.log(user_id) - console.log(rspmunicipality.data) - console.log(municipality) - console.log(rspwatchlist.data) - const flattenedWatchlist = rspwatchlist.data.flat(); - setDashMuniResults(rspmunicipality.data) - setDashWatchResults(flattenedWatchlist) - + const user_data = await userProfile.getUserProfile(); + const user_id = user_data.current?.sub; + const mockTicketId = "8f4cf09d-754e-4d71-96dc-952173fab07c"; + const rspwatchlist = await getTicket(mockTicketId); + const municipality = user_data.current?.municipality; + const rspmunicipality = await getTicketsInMunicipality(municipality); + console.log(user_id); + console.log(rspmunicipality); + console.log(municipality); + const flattenedWatchlist = rspwatchlist.flat(); + console.log(flattenedWatchlist); + setDashMuniResults(Array.isArray(rspmunicipality) ? rspmunicipality : []); + setDashWatchResults(flattenedWatchlist); } catch (error) { - console.log(error) + console.log(error); } }; fetchData(); - }, []); + }, [userProfile]); // Add userProfile to the dependency array const handleTabChange = (key: Key) => { const index = Number(key); @@ -57,8 +50,8 @@ export default function CitizenDashboard() { setIsHelpOpen(!isHelpOpen); }; - const hasStatusFieldMuni = dashMuniResults.some(item => item.Status !== undefined); - const hasStatusFieldWatch = dashWatchResults.some(item => item.Status !== undefined); + const hasStatusFieldMuni = Array.isArray(dashMuniResults) && dashMuniResults.some(item => item.Status !== undefined); + const hasStatusFieldWatch = Array.isArray(dashWatchResults) && dashWatchResults.some(item => item.Status !== undefined); return (
@@ -82,17 +75,22 @@ export default function CitizenDashboard() {

Dashboard

-
+ {/* Persistent Help Icon */} +
+ +
{isHelpOpen && ( -
+
- + + +

Nearest to you

-
-

- Based on your proximity to the issue. -

-
-
- -
- {hasStatusFieldMuni ? ( - - ) : ( +

+ Based on your proximity to the issue. +

+ - )} +

Watchlist

-
-

- Based on your watchlist. -

-
-
- -
- {hasStatusFieldWatch ? ( - - ) : ( + +

+ All of the issues you have added to your watchlist. +

+ + - )} diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index 3cad838e..399fd8f9 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -29,7 +29,7 @@ export default function Home() { {/* Content */}
-
{/* Ensure content is above the background */} +
{/* Ensure content is above the background */}

Be the change in your city
with MyCity. @@ -46,7 +46,7 @@ export default function Home() { - + diff --git a/frontend/src/app/profile/citizen/page.tsx b/frontend/src/app/profile/citizen/page.tsx index 68bf9818..1986146a 100644 --- a/frontend/src/app/profile/citizen/page.tsx +++ b/frontend/src/app/profile/citizen/page.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react"; import EditCitizenProfile from '@/components/EditProfile/UserProfile'; import Navbar from "@/components/Navbar/Navbar"; -import { useProfile } from "@/context/UserProfileContext"; -import { UserData } from "@/types/user.types"; +import { UserData } from "@/types/custom.types"; +import { useProfile } from "@/hooks/useProfile"; export default function CitizenProfile() { diff --git a/frontend/src/app/search/citizen/page.tsx b/frontend/src/app/search/citizen/page.tsx index 077e6bdb..89fde43b 100644 --- a/frontend/src/app/search/citizen/page.tsx +++ b/frontend/src/app/search/citizen/page.tsx @@ -5,17 +5,16 @@ import Navbar from "@/components/Navbar/Navbar"; import SearchTicket from "@/components/Search/SearchTicket"; import SearchMunicipality from "@/components/Search/SearchMunicipality"; import SearchSP from "@/components/Search/SearchSP"; -import axios from "axios"; import { FaFilter } from "react-icons/fa"; import { HelpCircle, X } from "lucide-react"; -import Modal from "react-modal"; import { ThreeDots } from "react-loader-spinner"; +import { searchIssue, searchMunicipality, searchMunicipalityTickets, searchServiceProvider } from "@/services/search.service"; export default function CreateTicket() { const [searchTerm, setSearchTerm] = useState(""); const [searchResults, setSearchResults] = useState([]); const [selectedFilter, setSelectedFilter] = useState< - "myLocation" | "serviceProviders" | "municipalities" + "myLocation" | "serviceProviders" | "municipalities" | "municipalityTickets" >("myLocation"); const [isFilterOpen, setIsFilterOpen] = useState(false); const filterRef = useRef(null); @@ -23,54 +22,52 @@ export default function CreateTicket() { const [municipalityTickets, setMunicipalityTickets] = useState([]); const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [resultsPerPage] = useState(10); + const [searchTime, setSearchTime] = useState(0); + const [showToast, setShowToast] = useState(false); + const [totalResults, setTotalResults] = useState(0); const handleSearch = async () => { try { setHasSearched(true); setLoading(true); - let response = { data: [] }; + const startTime = Date.now(); + let data: any[] = []; + let allTickets = []; switch (selectedFilter) { case "myLocation": - response = await axios.get( - `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/issues?q=${encodeURIComponent( - searchTerm - )}` - ); + data = await searchIssue(searchTerm); break; case "serviceProviders": - response = await axios.get( - `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/service-provider?q=${encodeURIComponent( - searchTerm - )}` - ); + data = await searchServiceProvider(searchTerm); break; case "municipalities": - response = await axios.get( - `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/municipality?q=${encodeURIComponent( - searchTerm - )}` - ); - const municipalityIds = response.data.map( + data = await searchMunicipality(searchTerm); + break; + case "municipalityTickets": + data = await searchMunicipality(searchTerm); + const municipalityIds = data.map( (municipality: any) => municipality.municipality_id ); const ticketsPromises = municipalityIds.map( - (municipalityId: string) => - axios.get( - `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/municipality-tickets?q=${encodeURIComponent( - municipalityId - )}` - ) + (municipalityId: string) => searchMunicipalityTickets(municipalityId) ); const ticketsResponses = await Promise.all(ticketsPromises); - const allTickets = ticketsResponses.flatMap( - (response) => response.data + allTickets = ticketsResponses.flatMap( + (response) => response ); setMunicipalityTickets(allTickets); break; default: break; } - setSearchResults(response.data); + const endTime = Date.now(); + setSearchTime((endTime - startTime) / 1000); // Time in seconds + setTotalResults(selectedFilter === "municipalityTickets" ? allTickets.length : data.length); + setSearchResults(selectedFilter === "municipalityTickets" ? allTickets : data); + setShowToast(true); + setTimeout(() => setShowToast(false), 3000); // Hide toast after 3 seconds setLoading(false); } catch (error) { console.error("Error fetching search results:", error); @@ -79,13 +76,14 @@ export default function CreateTicket() { }; const handleFilterChange = ( - filter: "myLocation" | "serviceProviders" | "municipalities" + filter: "myLocation" | "serviceProviders" | "municipalities" | "municipalityTickets" ) => { setSelectedFilter(filter); setIsFilterOpen(false); setSearchResults([]); setHasSearched(false); setMunicipalityTickets([]); + setCurrentPage(1); // Reset to the first page on filter change }; const handleSearchInputChange = ( @@ -121,12 +119,18 @@ export default function CreateTicket() { }; }, []); + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + + const indexOfLastResult = currentPage * resultsPerPage; + const indexOfFirstResult = indexOfLastResult - resultsPerPage; + const currentResults = searchResults.slice(indexOfFirstResult, indexOfLastResult); + return (
- +
-

Search

+

+ Search +

{isHelpModalOpen && ( -
+
{loading && (
- {" "} - {/* Adjusted margin-top */} - {selectedFilter === "serviceProviders" && ( - - )} - {selectedFilter === "municipalities" && ( - <> - - - - )} + {currentResults.map((result, index) => { + if (selectedFilter === "serviceProviders") { + return ; + } + if (selectedFilter === "municipalities") { + return ; + } + if (selectedFilter === "municipalityTickets") { + return ; + } + return null; + })} +
+ +
)} + {showToast && ( +
+ {totalResults} results found in + {searchTime.toFixed(2)} + seconds +
+ )}
); diff --git a/frontend/src/app/settings/citizen/page.tsx b/frontend/src/app/settings/citizen/page.tsx index 5b7db8a1..6389462b 100644 --- a/frontend/src/app/settings/citizen/page.tsx +++ b/frontend/src/app/settings/citizen/page.tsx @@ -5,11 +5,10 @@ import { useRouter } from "next/navigation"; import Navbar from "@/components/Navbar/Navbar"; import ChangeAccountInfo from "@/components/Settings/citizen/ChangeAccountInfo"; import ChangePassword from "@/components/Settings/citizen/ChangePassword"; -import { useProfile } from "@/context/UserProfileContext"; import { User, HelpCircle, XCircle } from "lucide-react"; -import Image from "next/image"; -import { UserData } from "@/types/user.types"; +import { UserData } from "@/types/custom.types"; +import { useProfile } from "@/hooks/useProfile"; type SubPage = "ChangeAccountInfo" | "ChangePassword" | null; @@ -376,21 +375,24 @@ export default function Settings() {

Settings

- +
{showHelpMenu && ( -
+

Help Menu

This settings page allows you to:

diff --git a/frontend/src/components/CreateTicket/CreateTicketForm.tsx b/frontend/src/components/CreateTicket/CreateTicketForm.tsx index 19adc8aa..6a1118a9 100644 --- a/frontend/src/components/CreateTicket/CreateTicketForm.tsx +++ b/frontend/src/components/CreateTicket/CreateTicketForm.tsx @@ -1,20 +1,14 @@ 'use client' import React, { FormEvent, useEffect, useState } from 'react'; -import { AutocompleteItem, Textarea, Checkbox, Button, Autocomplete } from '@nextui-org/react'; +import { AutocompleteItem, Textarea, Button, Autocomplete } from '@nextui-org/react'; import { cn } from '@/lib/utils'; import { MapboxContextProps } from '@/context/MapboxContext'; -import axios from 'axios'; -import Image from 'next/image'; -import { useProfile } from '@/context/UserProfileContext'; +import { useProfile } from "@/hooks/useProfile"; +import { FaultType } from '@/types/custom.types'; +import { getFaultTypes } from '@/services/tickets.service'; -type FaultType = { - name: string; - icon: string; - multiplier: number; -}; - interface Props extends React.HTMLAttributes { useMapboxProp: () => MapboxContextProps; } @@ -22,25 +16,21 @@ interface Props extends React.HTMLAttributes { export default function CreateTicketForm({ className, useMapboxProp }: Props) { const { selectedAddress } = useMapboxProp(); const { getUserProfile } = useProfile(); - + const [faultTypes, setFaultTypes] = useState([]); - + useEffect(() => { async function fetchFaultTypes() { try { - const response = await axios.get('https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/tickets/fault-types', { - headers: { - 'Content-Type': 'application/json', - }, - }); - - const data = await response.data; - - setFaultTypes(data.map((item: any) => ({ - name: item.asset_id, // asset_id is used as the unique name - icon: item.assetIcon, - multiplier: item.multiplier, - }))); + const data = await getFaultTypes(); + + // setFaultTypes(data.map((item: any) => ({ + // name: item.asset_id, // asset_id is used as the unique name + // icon: item.assetIcon, + // multiplier: item.multiplier, + // }))); + + setFaultTypes(data); } catch (error) { console.error('Error fetching fault types:', error); @@ -120,7 +110,7 @@ export default function CreateTicketForm({ className, useMapboxProp }: Props) { {(faultType) =>
- {faultType.name} + {faultType.name} {faultType.name}
diff --git a/frontend/src/components/EditProfile/UserProfile.tsx b/frontend/src/components/EditProfile/UserProfile.tsx index 0bae8f85..2b3bfa36 100644 --- a/frontend/src/components/EditProfile/UserProfile.tsx +++ b/frontend/src/components/EditProfile/UserProfile.tsx @@ -5,7 +5,7 @@ import NavbarUser from '../Navbar/NavbarUser'; import "react-toastify/dist/ReactToastify.css"; import { Upload } from 'lucide-react'; import { toast, ToastContainer } from 'react-toastify'; -import { UserData } from '@/types/user.types'; +import { UserData } from '@/types/custom.types'; interface Props extends React.HTMLAttributes { data: UserData | null; diff --git a/frontend/src/components/FaultCardContainer/DashboardFualtCardContainer.tsx b/frontend/src/components/FaultCardContainer/DashboardFualtCardContainer.tsx index 2c671eb5..f3d9ea96 100644 --- a/frontend/src/components/FaultCardContainer/DashboardFualtCardContainer.tsx +++ b/frontend/src/components/FaultCardContainer/DashboardFualtCardContainer.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import FaultCardUserView from "@/components/FaultCardUserView/FaultCardUserView"; -import DashboardFaultCardUser from "@/components/FaultCardUser/DashboardFaultCardUser"; import FaultCardUser from "@/components/FaultCardUser/FaultCardUser"; interface CardData { dateClosed: string; @@ -52,7 +51,7 @@ const DashboardFaultCardContainer: React.FC = ({ cardData = const visibleItems = cardData .slice(startIndex, Math.min(startIndex + itemsPerPage, cardData.length)) .map((item, index) => ( - = ({ cardData = description={item.description} image={item.imageURL} createdBy={item.dateOpened} + ticketNumber={item.ticket_id} onClick={() => handleCardClick(item)} /> )); diff --git a/frontend/src/components/FaultCardUser/DashboardFaultCardUser.tsx b/frontend/src/components/FaultCardUser/DashboardFaultCardUser.tsx index 290dd80d..99da0a57 100644 --- a/frontend/src/components/FaultCardUser/DashboardFaultCardUser.tsx +++ b/frontend/src/components/FaultCardUser/DashboardFaultCardUser.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { FaArrowUp, FaEye, FaCommentAlt } from "react-icons/fa"; interface CardData { title: string; @@ -7,12 +6,13 @@ interface CardData { arrowCount: number; commentCount: number; viewCount: number; + ticketNumber: string; description: string; image: string; createdBy: string; } -interface DashboardCardUserProps extends CardData { +interface FaultCardUserProps extends CardData { onClick?: () => void; // Make onClick optional } @@ -23,22 +23,28 @@ function formatNumber(num: number): string { return num.toString(); } -const DashboardFaultCardUser: React.FC = ({ +const FaultCardUser: React.FC = ({ title, address, - arrowCount, - commentCount, - viewCount, image, - createdBy, onClick, }) => { return (
-
+ className="w-80 bg-white bg-opacity-70 cursor-pointer rounded-lg shadow-md overflow-hidden m-2 transform transition-transform duration-300 hover:scale-105" + onClick={onClick} +> + +
+ {image ? ( + {title} + ) : ( +
+ Image Placeholder +
+ )} +
+
{title}

{address}

@@ -48,4 +54,4 @@ const DashboardFaultCardUser: React.FC = ({ ); }; -export default DashboardFaultCardUser; +export default FaultCardUser; diff --git a/frontend/src/components/FaultCardUser/FaultCardUser.tsx b/frontend/src/components/FaultCardUser/FaultCardUser.tsx index e22c6fbe..1ec2492f 100644 --- a/frontend/src/components/FaultCardUser/FaultCardUser.tsx +++ b/frontend/src/components/FaultCardUser/FaultCardUser.tsx @@ -35,29 +35,34 @@ const FaultCardUser: React.FC = ({ }) => { return (
-
- {image ? ( - {title} - ) : ( -
- Image Placeholder -
- )} -
-
-
-
{title}
-

{address}

+ className="w-80 h-auto bg-white bg-opacity-70 cursor-pointer rounded-lg shadow-md overflow-hidden m-2 transform transition-transform duration-300 hover:scale-105" + onClick={onClick} + > +
+ {image ? ( + {title} + ) : ( +
+ Image Placeholder +
+ )} +
+
+
+
{title}
+

{address}

+
+ +
+ + {/*
+
+
{title}
+

{address}

+
+
*/} +
-
-
); }; diff --git a/frontend/src/components/FaultCardUserView/DashboardFaultCardUserView.tsx b/frontend/src/components/FaultCardUserView/DashboardFaultCardUserView.tsx index e69de29b..d92288ce 100644 --- a/frontend/src/components/FaultCardUserView/DashboardFaultCardUserView.tsx +++ b/frontend/src/components/FaultCardUserView/DashboardFaultCardUserView.tsx @@ -0,0 +1,207 @@ +import React, { useState, useEffect } from "react"; +import {FaTimes,FaArrowUp,FaCommentAlt,FaEye,FaExclamationTriangle,FaTicketAlt,FaUser,} from "react-icons/fa"; + +interface FaultCardUserViewProps { + show: boolean; + onClose: () => void; + title: string; + address: string; + arrowCount: number; + commentCount: number; + viewCount: number; + ticketNumber: string; + description: string; + image: string; + createdBy: string; +} + +const FaultCardUserView: React.FC = ({ + show, + onClose, + title, + address, + arrowCount, + commentCount, + viewCount, + ticketNumber, + description, + image, + createdBy, +}) => { + // Retrieve state from local storage or initialize with default values + const getLocalStorageData = () => { + const data = localStorage.getItem(`ticket-${ticketNumber}`); + return data ? JSON.parse(data) : { + arrowCount, + commentCount, + viewCount, + arrowColor: "black", + commentColor: "black", + eyeColor: "black", + }; + }; + + const initialData = getLocalStorageData(); + + const [currentArrowCount, setCurrentArrowCount] = useState(initialData.arrowCount); + const [currentCommentCount, setCurrentCommentCount] = useState(initialData.commentCount); + const [currentViewCount, setCurrentViewCount] = useState(initialData.viewCount); + const [arrowColor, setArrowColor] = useState(initialData.arrowColor); + const [commentColor, setCommentColor] = useState(initialData.commentColor); + const [eyeColor, setEyeColor] = useState(initialData.eyeColor); + + useEffect(() => { + const data = { + arrowCount: currentArrowCount, + commentCount: currentCommentCount, + viewCount: currentViewCount, + arrowColor, + commentColor, + eyeColor, + }; + + localStorage.setItem(`ticket-${ticketNumber}`, JSON.stringify(data)); + }, [currentArrowCount, currentCommentCount, currentViewCount, arrowColor, commentColor, eyeColor, ticketNumber]); + + const handleArrowClick = () => { + if (arrowColor === "black") { + setArrowColor("blue"); + setCurrentArrowCount((prevCount: number) => prevCount + 1); + } else { + setArrowColor("black"); + setCurrentArrowCount((prevCount: number) => prevCount - 1); + } + }; + + const handleCommentClick = () => { + if (commentColor === "black") { + setCommentColor("blue"); + setCurrentCommentCount((prevCount: number) => prevCount + 1); + } else { + setCommentColor("black"); + setCurrentCommentCount((prevCount: number) => prevCount - 1); + } + }; + + const handleEyeClick = () => { + if (eyeColor === "black") { + setEyeColor("blue"); + setCurrentViewCount((prevCount: number) => prevCount + 1); + } else { + setEyeColor("black"); + setCurrentViewCount((prevCount: number) => prevCount - 1); + } + }; + + + if (!show) return null; + + const addressParts = address.split(","); + + return ( +
+
+ +
+ {/* Header Section */} +
+
+ +

{`Ticket ${ticketNumber}`}

+
+
+ + Unaddressed +
+
+ + {/* Fault Type Section */} +
+

Fault Type

+

{title}

+
+ + {/* Description Section */} +
+

Description

+

{description}

+
+ + {/* Image Section - hidden for now! +
+ Fault +
+ */} + + {/* Stats Section */} +
+
+
+ +
+ {currentArrowCount} +
+
+
+ +
+ {currentViewCount} +
+
+
+ +
+ {currentCommentCount} +
+
+ + {/* Address and Created By Section */} +
+
+

Address

+ {addressParts.map((part, index) => ( +

+ {part.trim()} +

+ ))} +
+
+

Created By

+ +

{createdBy}

+
+
+ + {/* Back Button */} +
+ +
+
+
+
+ ); +}; + +export default FaultCardUserView; diff --git a/frontend/src/components/FaultCardUserView/FaultCardUserView.tsx b/frontend/src/components/FaultCardUserView/FaultCardUserView.tsx index 433c7646..d1a8ab92 100644 --- a/frontend/src/components/FaultCardUserView/FaultCardUserView.tsx +++ b/frontend/src/components/FaultCardUserView/FaultCardUserView.tsx @@ -1,13 +1,5 @@ import React, { useState, useEffect } from "react"; -import { - FaTimes, - FaArrowUp, - FaCommentAlt, - FaEye, - FaExclamationTriangle, - FaTicketAlt, - FaUser, -} from "react-icons/fa"; +import {FaArrowUp,FaCommentAlt,FaEye,FaExclamationTriangle,FaTicketAlt} from "react-icons/fa"; interface FaultCardUserViewProps { show: boolean; @@ -33,8 +25,7 @@ const FaultCardUserView: React.FC = ({ viewCount, ticketNumber, description, - image, - createdBy, + image }) => { const getLocalStorageData = () => { const data = localStorage.getItem(`ticket-${ticketNumber}`); diff --git a/frontend/src/components/FaultMapView/FaultMapView.tsx b/frontend/src/components/FaultMapView/FaultMapView.tsx index 8840b0a5..79831e0b 100644 --- a/frontend/src/components/FaultMapView/FaultMapView.tsx +++ b/frontend/src/components/FaultMapView/FaultMapView.tsx @@ -1,4 +1,3 @@ -import Image from 'next/image'; const mapPlaceholder = "https://media.wired.com/photos/59269cd37034dc5f91bec0f1/191:100/w_1280,c_limit/GoogleMapTA.jpg"; @@ -21,7 +20,7 @@ const FaultMapView = () => { {/* Key and Regional Summary Section */}
-
+

Key

@@ -44,7 +43,7 @@ const FaultMapView = () => { Power Outage
-
+

Regional Summary

Population: ~ 2 million

Infrastructure faults: 154

diff --git a/frontend/src/components/Login/CitizenLogin.tsx b/frontend/src/components/Login/CitizenLogin.tsx index f64de26a..ab33cd66 100644 --- a/frontend/src/components/Login/CitizenLogin.tsx +++ b/frontend/src/components/Login/CitizenLogin.tsx @@ -2,9 +2,9 @@ import React, { FormEvent } from 'react'; import { Input, Button } from '@nextui-org/react'; import Link from 'next/link'; import { FcGoogle } from 'react-icons/fc'; -import { handleSignIn } from '@/lib/cognitoActions'; -import { UserRole } from '@/types/user.types'; import { useRouter } from 'next/navigation'; +import { UserRole } from '@/types/custom.types'; +import { handleSignIn } from '@/services/auth.service'; export default function CitizenLogin() { diff --git a/frontend/src/components/Login/MunicipalityLogin.tsx b/frontend/src/components/Login/MunicipalityLogin.tsx index 8112793d..b8fe83bd 100644 --- a/frontend/src/components/Login/MunicipalityLogin.tsx +++ b/frontend/src/components/Login/MunicipalityLogin.tsx @@ -1,9 +1,9 @@ import React, { FormEvent } from "react"; import Link from "next/link"; import { Input, Button, } from "@nextui-org/react"; -import { UserRole } from "@/types/user.types"; -import { handleSignIn } from "@/lib/cognitoActions"; import { useRouter } from 'next/navigation'; +import { UserRole } from "@/types/custom.types"; +import { handleSignIn } from "@/services/auth.service"; export default function MunicipalityLogin() { const router = useRouter(); @@ -13,7 +13,7 @@ export default function MunicipalityLogin() { const form = new FormData(event.currentTarget as HTMLFormElement); try { - const {isSignedIn} = await handleSignIn(form, UserRole.MUNICIPALITY); + const { isSignedIn } = await handleSignIn(form, UserRole.MUNICIPALITY); if (isSignedIn) { router.push("/dashboard"); @@ -82,6 +82,7 @@ export default function MunicipalityLogin() { {/* Social Media Sign Up Options */} diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index fa798c1d..b23ca2cb 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -1,9 +1,9 @@ 'use client' -import { authenticateClient } from "@/lib/cognitoActions"; import React, { useEffect, useState } from "react"; import NavbarUser from "./NavbarUser"; import NavbarGuest from "./NavbarGuest"; +import { authenticateClient } from "@/services/auth.service"; diff --git a/frontend/src/components/Navbar/NavbarGuest.tsx b/frontend/src/components/Navbar/NavbarGuest.tsx index 8138d94d..26a0abfb 100644 --- a/frontend/src/components/Navbar/NavbarGuest.tsx +++ b/frontend/src/components/Navbar/NavbarGuest.tsx @@ -1,5 +1,4 @@ import Link from 'next/link'; -import Image from 'next/image'; import { Building2, Lightbulb, Wrench, Globe } from 'lucide-react'; export default function NavbarGuest() { diff --git a/frontend/src/components/Navbar/NavbarUser.tsx b/frontend/src/components/Navbar/NavbarUser.tsx index 1c8325f3..c9e1a081 100644 --- a/frontend/src/components/Navbar/NavbarUser.tsx +++ b/frontend/src/components/Navbar/NavbarUser.tsx @@ -1,12 +1,11 @@ import React, { useState, useEffect } from 'react'; import Link from 'next/link'; -import Image from 'next/image'; -import { Home, PlusCircle, Bell, Search, Settings, UserCircle } from 'lucide-react'; +import { Home, PlusCircle, Bell, Search } from 'lucide-react'; import { Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react'; -import { useProfile } from '@/context/UserProfileContext'; -import { UserData } from '@/types/user.types'; -import { handleSignOut } from '@/lib/cognitoActions'; +import { useProfile } from '@/hooks/useProfile'; +import { UserData } from '@/types/custom.types'; import { useRouter } from 'next/navigation'; +import { handleSignOut } from '@/services/auth.service'; export default function NavbarUser() { const router = useRouter(); @@ -94,7 +93,7 @@ export default function NavbarUser() { - + - + - Settings + Settings - About us + About us - Log out + Log out diff --git a/frontend/src/components/Search/SearchMunicipality.tsx b/frontend/src/components/Search/SearchMunicipality.tsx index db8ce3f7..36cb1532 100644 --- a/frontend/src/components/Search/SearchMunicipality.tsx +++ b/frontend/src/components/Search/SearchMunicipality.tsx @@ -1,13 +1,7 @@ import React from 'react'; import { UserCircle } from 'lucide-react'; +import { Municipality } from '@/types/custom.types'; -interface Municipality { - municipality_id: string; - name: string; - province: string; - email: string; - contactNumber: string; -} interface SearchMunicipalityProps { municipalities: Municipality[]; @@ -23,7 +17,7 @@ const SearchMunicipality: React.FC = ({ municipalities {municipalities.map((municipality: Municipality, index: number) => (
{/* Municipality text */} -
+
Municipality
diff --git a/frontend/src/components/Search/SearchSP.tsx b/frontend/src/components/Search/SearchSP.tsx index fdadad1d..13554f52 100644 --- a/frontend/src/components/Search/SearchSP.tsx +++ b/frontend/src/components/Search/SearchSP.tsx @@ -1,14 +1,6 @@ import React from "react"; import { Star } from "lucide-react"; -import Image from "next/image"; - -interface ServiceProvider { - companyLogo?: string; - name: string; - contactNumber: string; - email: string; - qualityRating?: number; -} +import { ServiceProvider } from "@/types/custom.types"; interface SearchSPProps { serviceProviders: ServiceProvider[]; @@ -24,7 +16,7 @@ const SearchSP: React.FC = ({ serviceProviders }) => { {serviceProviders.map((sp: ServiceProvider, index: number) => (
{/* Service Provider text */}
diff --git a/frontend/src/components/Search/SearchTicket.tsx b/frontend/src/components/Search/SearchTicket.tsx index 4cd8c8b4..4eee72cb 100644 --- a/frontend/src/components/Search/SearchTicket.tsx +++ b/frontend/src/components/Search/SearchTicket.tsx @@ -1,17 +1,6 @@ import React from 'react'; import { AlertTriangle } from 'lucide-react'; -import { FaArrowUp, FaEye } from 'react-icons/fa'; - -interface Ticket { - //not the only components of a ticket (merely the one we are using) - ticket_id: string; - asset_id: string; - dateOpened: string; - municipality_id: string; - state: string; - upvotes: number; - viewCount: number; -} +import { Ticket } from '@/types/custom.types'; interface SearchTicketProps { tickets: Ticket[]; @@ -25,9 +14,9 @@ const SearchTicket: React.FC = ({ tickets }) => { return (
{tickets.map((ticket: Ticket, index: number) => ( -
+
{/* First Field - Ticket */} -
+
Ticket
diff --git a/frontend/src/components/Settings/citizen/ChangeAccountInfo.tsx b/frontend/src/components/Settings/citizen/ChangeAccountInfo.tsx index a4242138..8da2ebfb 100644 --- a/frontend/src/components/Settings/citizen/ChangeAccountInfo.tsx +++ b/frontend/src/components/Settings/citizen/ChangeAccountInfo.tsx @@ -3,8 +3,8 @@ import { ArrowLeft, Edit2, Lock, User } from "lucide-react"; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import Image from "next/image"; -import { UserData } from "@/types/user.types"; -import { useProfile } from "@/context/UserProfileContext"; +import { UserData } from "@/types/custom.types"; +import { useProfile } from "@/hooks/useProfile"; type ChangeAccountInfoProps = { onBack: () => void; @@ -85,85 +85,85 @@ const ChangeAccountInfo: React.FC = ({ onBack, profileDa // }, []); return ( -
- + +
+ {data?.picture ? ( + Profile + ) : ( + + )} + + +
-
- {data?.picture ? ( - Profile - ) : ( - - )} - +
+

+ Email + +

+

{data?.email}

+
+
+

First Name(s)

+
setFirstname(event.target.value)} + className="border-b-2 border-gray-300 focus:outline-none focus:border-blue-500" /> +
- -
-

- Email - -

-

{data?.email}

-
-
-

First Name(s)

-
- setFirstname(event.target.value)} - className="border-b-2 border-gray-300 focus:outline-none focus:border-blue-500" - /> - -
-
-
-

Surname

-
- setSurname(event.target.value)} - className="border-b-2 border-gray-300 focus:outline-none focus:border-blue-500" - /> - -
-
-
-

- Municipality - -

-

{data?.municipality}

-
-
- +
+
+

Surname

+
+ setSurname(event.target.value)} + className="border-b-2 border-gray-300 focus:outline-none focus:border-blue-500" + /> +
-
+
+

+ Municipality + +

+

{data?.municipality}

+
+
+ +
+ +
); }; diff --git a/frontend/src/components/Settings/citizen/ChangePassword.tsx b/frontend/src/components/Settings/citizen/ChangePassword.tsx index 6075bda1..976de055 100644 --- a/frontend/src/components/Settings/citizen/ChangePassword.tsx +++ b/frontend/src/components/Settings/citizen/ChangePassword.tsx @@ -21,7 +21,8 @@ const ChangePassword: React.FC = ({ onBack }) => { const toggleOldPassword = () => setShowOldPassword(!showOldPassword); const toggleNewPassword = () => setShowNewPassword(!showNewPassword); - const toggleConfirmNewPassword = () => setShowConfirmNewPassword(!showConfirmNewPassword); + const toggleConfirmNewPassword = () => + setShowConfirmNewPassword(!showConfirmNewPassword); const handleVerifyOldPassword = () => { const validatePassword = (password: string) => { @@ -30,11 +31,11 @@ const ChangePassword: React.FC = ({ onBack }) => { /[a-z]/, // Lowercase letter /[0-9]/, // Digit /[!@#$%^&*]/, // Special character - /.{8,}/ // Minimum length of 8 + /.{8,}/, // Minimum length of 8 ]; - return passwordRules.every(rule => rule.test(password)); + return passwordRules.every((rule) => rule.test(password)); }; - + if (validatePassword(oldPassword)) { setIsOldPasswordVerified(true); setIsModalOpen(false); @@ -50,9 +51,9 @@ const ChangePassword: React.FC = ({ onBack }) => { /[a-z]/, // Lowercase letter /[0-9]/, // Digit /[!@#$%^&*]/, // Special character - /.{8,}/ // Minimum length of 8 + /.{8,}/, // Minimum length of 8 ]; - return passwordRules.every(rule => rule.test(password)); + return passwordRules.every((rule) => rule.test(password)); }; const handleSaveChanges = () => { @@ -61,7 +62,9 @@ const ChangePassword: React.FC = ({ onBack }) => { return; } if (!validatePassword(newPassword)) { - setPasswordError("Password must be at least 8 characters long and include uppercase, lowercase, digit, and special character."); + setPasswordError( + "Password must be at least 8 characters long and include uppercase, lowercase, digit, and special character." + ); return; } // Save new password logic @@ -69,6 +72,15 @@ const ChangePassword: React.FC = ({ onBack }) => { alert("Password changed successfully!"); }; + const checkPasswordMatch = (confirmPassword: string) => { + setConfirmNewPassword(confirmPassword); + if (newPassword !== confirmPassword) { + setPasswordError("Passwords do not match"); + } else { + setPasswordError(""); + } + }; + return (
{passwordError &&

{passwordError}

} @@ -153,12 +169,19 @@ const ChangePassword: React.FC = ({ onBack }) => { className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-700" onClick={toggleNewPassword} > - {showNewPassword ? : } + {showNewPassword ? ( + + ) : ( + + )}
-
- {passwordError &&

{passwordError}

} -
- +
+ {passwordError && ( +

{passwordError}

+ )} +
+ +
)} diff --git a/frontend/src/components/Signup/CitizenSignup.tsx b/frontend/src/components/Signup/CitizenSignup.tsx index 6de39ba8..c3e8927b 100644 --- a/frontend/src/components/Signup/CitizenSignup.tsx +++ b/frontend/src/components/Signup/CitizenSignup.tsx @@ -1,13 +1,37 @@ -import React, { FormEvent } from "react"; -import { Input, Button } from "@nextui-org/react"; -import { UserRole } from "@/types/user.types"; -import { handleSignUp } from "@/lib/cognitoActions"; +import React, { FormEvent, useEffect, useState } from "react"; +import { Input, Button, Autocomplete, AutocompleteItem } from "@nextui-org/react"; +import { BasicMunicipality, UserRole } from "@/types/custom.types"; +import { handleSignUp } from "@/services/auth.service"; +import { getMunicipalityList } from "@/services/municipalities.service"; + export default function CitizenSignup() { + const [municipalities, setMunicipalities] = useState([]); + const [selectedMunicipality, setSelectedMunicipality] = useState(""); + + + useEffect(() => { + // Fetch the municipalities when the component mounts + const fetchMunicipalities = async () => { + try { + + const data = await getMunicipalityList(); + setMunicipalities(data); + + } catch (error: any) { + console.log(error.message); + } + }; + + fetchMunicipalities(); + }, []); + + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const form = new FormData(event.currentTarget as HTMLFormElement); + form.set("municipality", selectedMunicipality); // Append selected municipality to the form data try { handleSignUp(form, UserRole.CITIZEN); @@ -19,7 +43,6 @@ export default function CitizenSignup() { }; - return (
- Municipality * } - labelPlacement={"outside"} - classNames={{ - inputWrapper: "h-[3em]", - }} - type="text" + labelPlacement="outside" name="municipality" - autoComplete="new-municipality" - placeholder="City of Tshwane Metropolitan" - required - /> + placeholder="Select a municipality" + fullWidth + defaultItems={municipalities} + disableSelectorIconRotation + isClearable={false} + menuTrigger={"input"} + size={"lg"} + onSelectionChange={(value) => setSelectedMunicipality(value as string)} + > + {(municipality) => ( + + {municipality.municipality_id} + + )} + { event.preventDefault(); - // const form = new FormData(event.currentTarget as HTMLFormElement); - // console.log("Service Area: " + form.get("service-area")) - // const response = await axios.post('https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/auth/signup/company', { - // email: form.get("email"), - // name: form.get("company"), - // service_type: form.get("service-area"), - // password: form.get("password") - // }); - // console.log("Something happened") - // const data = await response.data; - // if (data.Status == 200) { - // sessionStorage.setItem('pid', data.pid) - // sessionStorage.setItem('name', data.name) - // sessionStorage.setItem('service_type', data.service_type) - // router.push("/dashboard/service-provider") - // } - // Handle the submit action here - // console.log(`User Type: Organization, Email: ${email}, Password: ${password}`); }; return ( diff --git a/frontend/src/components/ViewTicket/ViewTicketForm.tsx b/frontend/src/components/ViewTicket/ViewTicketForm.tsx index e7f3c667..85631ed0 100644 --- a/frontend/src/components/ViewTicket/ViewTicketForm.tsx +++ b/frontend/src/components/ViewTicket/ViewTicketForm.tsx @@ -5,8 +5,8 @@ import { Input, Autocomplete, AutocompleteItem, Textarea, Checkbox } from '@next import { BadgeAlert, Ticket } from 'lucide-react'; import { cn } from '@/lib/utils'; import Link from 'next/link'; -import axios from 'axios'; import Image from 'next/image'; +import { getTicket } from '@/services/tickets.service'; interface ReportFaultFormProps extends React.HTMLAttributes {} @@ -28,14 +28,9 @@ export default function ReportFaultForm({ className }: ReportFaultFormProps) { async function fetchticketdata() { try { - const response = await axios.get(`https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/tickets/view?ticket_id=${encodeURIComponent(ticket_id)}`, { - headers: { - 'Content-Type': 'application/json', - }, - }); + const data = await getTicket(ticket_id); - console.log(response); - const data = await response.data; + console.log(data); setTicketData(data.map((item: any) => ({ Upvotes: item.upvotes, // asset_id is used as the unique name diff --git a/frontend/src/context/UserProfileContext.tsx b/frontend/src/context/UserProfileContext.tsx index 2474c649..d8661a75 100644 --- a/frontend/src/context/UserProfileContext.tsx +++ b/frontend/src/context/UserProfileContext.tsx @@ -1,8 +1,8 @@ 'use client' -import { UserData, UserRole } from '@/types/user.types'; -import { fetchUserAttributes, getCurrentUser, updateUserAttributes } from 'aws-amplify/auth'; -import { MutableRefObject, ReactNode, createContext, useContext, useRef } from 'react'; +import { UserData, UserRole } from '@/types/custom.types'; +import { fetchUserAttributes, updateUserAttributes } from 'aws-amplify/auth'; +import { MutableRefObject, ReactNode, createContext, useRef } from 'react'; export interface UserProfileContextProps { @@ -14,10 +14,9 @@ export interface UserProfileContextProps { const UserProfileContext = createContext(undefined); -export const UserProfileProvider: React.FC<{ children: ReactNode }> = ({ children }) => { +export function UserProfileProvider({ children }: { children: ReactNode }) { const userProfile = useRef(null); - const getUserProfile = async () => { //if user profile data already cached, return it if (userProfile.current) { @@ -71,10 +70,4 @@ export const UserProfileProvider: React.FC<{ children: ReactNode }> = ({ childre ); } -export const useProfile = (): UserProfileContextProps => { - const context = useContext(UserProfileContext); - if (!context) { - throw new Error("useProfile must be used within a UserProfileProvider"); - } - return context; -}; \ No newline at end of file +export default UserProfileContext; \ No newline at end of file diff --git a/frontend/src/hooks/useProfile.ts b/frontend/src/hooks/useProfile.ts new file mode 100644 index 00000000..87284bf8 --- /dev/null +++ b/frontend/src/hooks/useProfile.ts @@ -0,0 +1,11 @@ +import UserProfileContext, { UserProfileContextProps } from '@/context/UserProfileContext'; +import { useContext } from 'react'; + +export const useProfile = (): UserProfileContextProps => { + const context = useContext(UserProfileContext); + if (!context) { + throw new Error("useProfile must be used within a UserProfileProvider"); + } + + return context; +}; \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bd0b3ae6..b257f908 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,5 +1,5 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index af4cceda..1380b16d 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,25 +1,39 @@ -import { type NextRequest, NextResponse } from "next/server"; - -import { authenticate } from "./utils/amplifyServerUtils"; - -const SUFFIX_COOKIE_NAME = "mycity.net.za.userpathsuffix"; +import { type NextRequest, NextResponse } from 'next/server'; +import { authenticate } from './utils/amplifyServerUtils'; +import { USER_PATH_SUFFIX_COOKIE_NAME } from './types/custom.types'; export async function middleware(request: NextRequest) { const path = request.nextUrl.pathname; const response = NextResponse.next(); const isAuthenticated = await authenticate({ request, response }); - const isOn = (url: string) => path === url; const startsWith = (url: string) => path.startsWith(url); + //bypass middleware if not in production environment + if (process.env.NODE_ENV != "production") { + if (isOn("/dashboard") || isOn("/profile") || isOn("/settings")) { + const cookie = request.cookies.get(USER_PATH_SUFFIX_COOKIE_NAME); + let userPathSuffix: string = ""; + + if (cookie) { + userPathSuffix = cookie.value; + return NextResponse.redirect(new URL(`${request.nextUrl.pathname}/${userPathSuffix}`, request.nextUrl)); + } + } + + return response; + + } + + //1. IF USER IS LOGGED IN if (isAuthenticated) { //1.1) and is trying to navigate to dashboard/profile/settings, redirect them to the appropriate page according to their user type if (isOn("/dashboard") || isOn("/profile") || isOn("/settings")) { - const cookie = request.cookies.get(SUFFIX_COOKIE_NAME); + const cookie = request.cookies.get(USER_PATH_SUFFIX_COOKIE_NAME); let userPathSuffix: string = ""; if (cookie) { @@ -70,70 +84,3 @@ export const config = { matcher: [ "/((?!api|_next/static|_next/image|.*\\.png$).*)"], }; - - - -//This is the code Kyle used to bypass the authentication middleware to access all frontend pages via the browser. -/* -import { type NextRequest, NextResponse } from "next/server"; -import { authenticate } from "./utils/amplifyServerUtils"; - -const SUFFIX_COOKIE_NAME = "mycity.net.za.userpathsuffix"; - -export async function middleware(request: NextRequest) { - const path = request.nextUrl.pathname; - const response = NextResponse.next(); - - // Bypass all checks for testing purposes - if (process.env.NODE_ENV === 'development') { - return response; - } - - const isAuthenticated = await authenticate({ request, response }); - - const isOn = (url: string) => path === url; - const startsWith = (url: string) => path.startsWith(url); - - // 1. IF USER IS LOGGED IN - if (isAuthenticated) { - // 1.1) and is trying to navigate to dashboard/profile/settings, redirect them to the appropriate page according to their user type - if (isOn("/dashboard") || isOn("/profile") || isOn("/settings")) { - const cookie = request.cookies.get(SUFFIX_COOKIE_NAME); - let userPathSuffix: string = ""; - - if (cookie) { - userPathSuffix = cookie.value; - return NextResponse.redirect(new URL(`${request.nextUrl.pathname}/${userPathSuffix}`, request.nextUrl)); - } else { - // in the event that user path suffix cookie is null for some reason, redirect to home page for now - return NextResponse.redirect(new URL("/", request.nextUrl)); - // should later redirect to some other page or log user out and ask them to relogin - } - } - - // 1.2) and tries to access any of the auth routes, redirect them to homepage - else if (startsWith("/auth")) { - return NextResponse.redirect(new URL("/", request.nextUrl)); - } - } - - // 2. IF USER IS NOT LOGGED IN - else { - // 2.1) and tries to access dashboard/profile/settings, redirect them to login page - if (startsWith("/dashboard") || startsWith("/profile") || startsWith("/settings")) { - return NextResponse.redirect(new URL("/auth/login", request.nextUrl)); - } - } - - return response; -} - -export const config = { - /* - * Match all request paths except for the ones starting with - */ -/* -matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], -}; - -*/ diff --git a/frontend/src/lib/cognitoActions.ts b/frontend/src/services/auth.service.ts similarity index 92% rename from frontend/src/lib/cognitoActions.ts rename to frontend/src/services/auth.service.ts index 8407d12b..8588fd7f 100644 --- a/frontend/src/lib/cognitoActions.ts +++ b/frontend/src/services/auth.service.ts @@ -1,6 +1,6 @@ -import { UserRole } from "@/types/user.types"; -import { SignUpInput, SignUpOutput, autoSignIn, fetchAuthSession, signIn, signOut, signUp } from "aws-amplify/auth"; -import { setUserPathSuffix, removeUserPathSuffix } from "./serverActions"; +import { UserRole } from '@/types/custom.types'; +import { setUserPathSuffix, removeUserPathSuffix } from '@/utils/authActions'; +import { SignUpInput, SignUpOutput, autoSignIn, fetchAuthSession, signIn, signOut, signUp } from 'aws-amplify/auth'; export async function handleSignIn(form: FormData, userRole: UserRole) { diff --git a/frontend/src/services/municipalities.service.ts b/frontend/src/services/municipalities.service.ts new file mode 100644 index 00000000..0a0bd274 --- /dev/null +++ b/frontend/src/services/municipalities.service.ts @@ -0,0 +1,31 @@ +import { BasicMunicipality } from "@/types/custom.types"; +import { revalidateTag } from "next/cache"; + +export async function getMunicipalityList(revalidate?: boolean) { + if (revalidate) { + revalidateTag("municipalities-municipalities-list"); //invalidate the cache + } + + try { + const response = await fetch("/api/municipality/municipalities-list", + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching municipalities: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as BasicMunicipality[]; + + return data; + + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/frontend/src/services/search.service.ts b/frontend/src/services/search.service.ts new file mode 100644 index 00000000..bf13288c --- /dev/null +++ b/frontend/src/services/search.service.ts @@ -0,0 +1,120 @@ +import { revalidateTag } from "next/cache"; + +export async function searchIssue(param:string, revalidate?: boolean) { + if (revalidate) { + revalidateTag("search-issues"); //invalidate the cache + } + + try { + const response = await fetch(`/api/search/issues?q=${encodeURIComponent(param)}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as any[]; + + return data; + + } catch (error) { + throw error; + } +} + + +export async function searchServiceProvider(param:string, revalidate?: boolean) { + if (revalidate) { + revalidateTag("search-service-provider"); //invalidate the cache + } + + try { + const response = await fetch(`/api/search/service-provider?q=${encodeURIComponent(param)}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as any[]; + + return data; + + } catch (error) { + throw error; + } +} + + +export async function searchMunicipality(param:string, revalidate?: boolean) { + if (revalidate) { + revalidateTag("search-municipality"); //invalidate the cache + } + + try { + const response = await fetch(`/api/search/municipality?q=${encodeURIComponent(param)}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as any[]; + + return data; + + } catch (error) { + throw error; + } +} + + +export async function searchMunicipalityTickets(municipalityId:string, revalidate?: boolean) { + if (revalidate) { + revalidateTag("search-municipality-tickets"); //invalidate the cache + } + + try { + const response = await fetch(`/api/search/municipality-tickets?q=${encodeURIComponent(municipalityId)}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as any[]; + + return data; + + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/frontend/src/services/tickets.service.ts b/frontend/src/services/tickets.service.ts new file mode 100644 index 00000000..f255a415 --- /dev/null +++ b/frontend/src/services/tickets.service.ts @@ -0,0 +1,130 @@ +import { revalidateTag } from "next/cache"; +import { FaultType } from "@/types/custom.types"; + +export async function getTicket(ticketId: string, revalidate?: boolean) { + if (revalidate) { + revalidateTag("tickets-view"); //invalidate the cache + } + + try { + const response = await fetch(`/api/tickets/view?ticket_id=${encodeURIComponent(ticketId)}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as any[]; + + return data; + + } catch (error) { + throw error; + } +} + + +// temporary function (request method must be changed to GET) +export async function getTicketsInMunicipality(municipality: string | undefined, revalidate?: boolean) { + if (!municipality) { + throw new Error("Missing municipality"); + } + + if (revalidate) { + revalidateTag("tickets-getinarea"); //invalidate the cache + } + + try { + const response = await fetch("/api/tickets/getinarea", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + municipality_id: municipality + }) + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as any[]; + + return data; + + } catch (error) { + throw error; + } +} + +//// will replace temporary function defined above once request method on api has been changed from POST to GET +// export async function getTicketsInMunicipality(municipality:string, revalidate?: boolean) { +// if (revalidate) { +// revalidateTag("tickets-getinarea"); //invalidate the cache +// } + +// try { +// const response = await fetch("/api/tickets/getinarea", +// { +// headers: { +// "Content-Type": "application/json", +// }, +// } +// ); + +// if (!response.ok) { +// throw new Error(`Error fetching: ${response.statusText}`); +// } + +// const result = await response.json(); + +// const data = result.data as any[]; + +// return data; + +// } catch (error) { +// throw error; +// } +// } + + +export async function getFaultTypes(revalidate?: boolean) { + if (revalidate) { + revalidateTag("tickets-fault-types"); //invalidate the cache + } + + try { + const response = await fetch("/api/tickets/fault-types", + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`); + } + + const result = await response.json(); + + const data = result.data as FaultType[]; + + return data; + + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/frontend/src/types/custom.types.ts b/frontend/src/types/custom.types.ts new file mode 100644 index 00000000..6924a9f3 --- /dev/null +++ b/frontend/src/types/custom.types.ts @@ -0,0 +1,56 @@ +export enum UserRole { + CITIZEN = "CITIZEN", + MUNICIPALITY = "MUNICIPALITY", + PRIVATE_COMPANY = "PRIVATE-COMPANY" +}; + +export interface UserData { + sub: string | undefined; + email: string | undefined; + given_name: string | undefined; + family_name: string | undefined; + picture: string | undefined; + user_role: UserRole | undefined; + municipality: string | undefined; +} + +export const USER_PATH_SUFFIX_COOKIE_NAME = "mycity.net.za.userpathsuffix"; + + +export interface BasicMunicipality { + municipality_id: string; +} + +export interface FaultType { + name: string; + icon: string; + multiplier: number; +} + +export interface Municipality { + municipality_id: string; + name: string; + province: string; + email: string; + contactNumber: string; +} + + +export interface ServiceProvider { + companyLogo?: string; + name: string; + contactNumber: string; + email: string; + qualityRating?: number; +} + +export interface Ticket { + //not the only components of a ticket (merely the one we are using) + ticket_id: string; + asset_id: string; + dateOpened: string; + municipality_id: string; + state: string; + upvotes: number; + viewCount: number; +} \ No newline at end of file diff --git a/frontend/src/types/user.types.ts b/frontend/src/types/user.types.ts deleted file mode 100644 index 4514ac26..00000000 --- a/frontend/src/types/user.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export enum UserRole { - CITIZEN = "CITIZEN", - MUNICIPALITY = "MUNICIPALITY", - PRIVATE_COMPANY = "PRIVATE-COMPANY" -}; - -export interface UserData { - sub: string | undefined; - email: string | undefined; - given_name: string | undefined; - family_name: string | undefined; - picture: string | undefined; - user_role: UserRole | undefined; - municipality: string | undefined; -} \ No newline at end of file diff --git a/frontend/src/utils/amplifyServerUtils.ts b/frontend/src/utils/amplifyServerUtils.ts index 28e14a7a..fab3b1e3 100644 --- a/frontend/src/utils/amplifyServerUtils.ts +++ b/frontend/src/utils/amplifyServerUtils.ts @@ -1,6 +1,6 @@ -import { currentConfig } from "@/config/amplifyCognitoConfig"; -import { NextServer, createServerRunner } from "@aws-amplify/adapter-nextjs"; -import { fetchAuthSession } from "aws-amplify/auth/server"; +import { currentConfig } from '@/config/amplifyCognitoConfig'; +import { NextServer, createServerRunner } from '@aws-amplify/adapter-nextjs'; +import { fetchAuthSession } from 'aws-amplify/auth/server'; export const { runWithAmplifyServerContext } = createServerRunner({ diff --git a/frontend/src/lib/serverActions.ts b/frontend/src/utils/authActions.ts similarity index 71% rename from frontend/src/lib/serverActions.ts rename to frontend/src/utils/authActions.ts index 08621fd3..8695ab5f 100644 --- a/frontend/src/lib/serverActions.ts +++ b/frontend/src/utils/authActions.ts @@ -1,9 +1,7 @@ 'use server' -import { UserRole } from "@/types/user.types"; -import { cookies, } from "next/headers"; - -const SUFFIX_COOKIE_NAME = "mycity.net.za.userpathsuffix"; +import { USER_PATH_SUFFIX_COOKIE_NAME, UserRole } from '@/types/custom.types'; +import { cookies } from 'next/headers'; export const setUserPathSuffix = async (userRole: UserRole) => { let suffix = ""; @@ -22,7 +20,7 @@ export const setUserPathSuffix = async (userRole: UserRole) => { } - cookies().set(SUFFIX_COOKIE_NAME, suffix, { maxAge: 2592000, secure:true, sameSite:"lax" }); //30 days + cookies().set(USER_PATH_SUFFIX_COOKIE_NAME, suffix, { maxAge: 2592000, secure: true, sameSite: "lax" }); //30 days //wait for cookie to be set before continuing const cookieWaitMaxTries = 30; // which is ~ 3seconds @@ -30,7 +28,7 @@ export const setUserPathSuffix = async (userRole: UserRole) => { return new Promise((resolve) => { const interval = setInterval(() => { if (count < cookieWaitMaxTries) { - if (cookies().get(SUFFIX_COOKIE_NAME)) { + if (cookies().get(USER_PATH_SUFFIX_COOKIE_NAME)) { clearInterval(interval); resolve(undefined); } @@ -42,5 +40,5 @@ export const setUserPathSuffix = async (userRole: UserRole) => { export const removeUserPathSuffix = async () => { - cookies().delete(SUFFIX_COOKIE_NAME); + cookies().delete(USER_PATH_SUFFIX_COOKIE_NAME); }; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 31e45e30..77bd9793 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -40,4 +40,4 @@ "exclude": [ "node_modules" ] -} +} \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 75c29b07..00000000 --- a/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - Example - - -

This is an example of a simple HTML page with one paragraph.

- - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b5b58f2f..00000000 --- a/package-lock.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "name": "MyCity", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@react-stately/collections": "^3.10.7", - "@react-stately/list": "^3.10.5", - "react-icons": "^5.2.1" - } - }, - "node_modules/@react-stately/collections": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.7.tgz", - "integrity": "sha512-KRo5O2MWVL8n3aiqb+XR3vP6akmHLhLWYZEmPKjIv0ghQaEebBTrN3wiEjtd6dzllv0QqcWvDLM1LntNfJ2TsA==", - "dependencies": { - "@react-types/shared": "^3.23.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@react-stately/list": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.10.5.tgz", - "integrity": "sha512-fV9plO+6QDHiewsYIhboxcDhF17GO95xepC5ki0bKXo44gr14g/LSo/BMmsaMnV+1BuGdBunB05bO4QOIaigXA==", - "dependencies": { - "@react-stately/collections": "^3.10.7", - "@react-stately/selection": "^3.15.1", - "@react-stately/utils": "^3.10.1", - "@react-types/shared": "^3.23.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@react-stately/selection": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.15.1.tgz", - "integrity": "sha512-6TQnN9L0UY9w19B7xzb1P6mbUVBtW840Cw1SjgNXCB3NPaCf59SwqClYzoj8O2ZFzMe8F/nUJtfU1NS65/OLlw==", - "dependencies": { - "@react-stately/collections": "^3.10.7", - "@react-stately/utils": "^3.10.1", - "@react-types/shared": "^3.23.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz", - "integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@react-types/shared": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz", - "integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", - "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "peer": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-icons": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", - "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index da856afc..00000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "@react-stately/collections": "^3.10.7", - "@react-stately/list": "^3.10.5", - "react-icons": "^5.2.1" - } -}