diff --git a/backend/tests/e2e/__init__.py b/backend/tests/e2e/__init__.py new file mode 100644 index 00000000..0095f54a --- /dev/null +++ b/backend/tests/e2e/__init__.py @@ -0,0 +1 @@ +# This file can be left empty or used for package-level imports and initialization diff --git a/backend/tests/e2e/citizen_signup_test.py b/backend/tests/e2e/citizen_signup_test.py new file mode 100644 index 00000000..ae0edc24 --- /dev/null +++ b/backend/tests/e2e/citizen_signup_test.py @@ -0,0 +1 @@ +# test the signup flow for a citizen from start to end diff --git a/backend/tests/integration/create_view_issue_test.py b/backend/tests/integration/create_view_issue_test.py new file mode 100644 index 00000000..bfda1360 --- /dev/null +++ b/backend/tests/integration/create_view_issue_test.py @@ -0,0 +1,4 @@ +# test for example that if a user reports an issue, +# then they can view their newly created issue + +# this is an integration test since it tests the integration between creating an issue and viewing it diff --git a/backend/tests/unit/validate_email_test.py b/backend/tests/unit/validate_email_test.py new file mode 100644 index 00000000..95e94cbc --- /dev/null +++ b/backend/tests/unit/validate_email_test.py @@ -0,0 +1,2 @@ +# let's say a function called validate_email is defined somewhere +# this test file will test if the function works as intended diff --git a/frontend/__tests__/unit/CitizenLogin.test.tsx b/frontend/__tests__/unit/CitizenLogin.test.tsx new file mode 100644 index 00000000..b6fe64e4 --- /dev/null +++ b/frontend/__tests__/unit/CitizenLogin.test.tsx @@ -0,0 +1,33 @@ +import CitizenLogin from "@/components/Login/CitizenLogin"; +import { render, screen } from "@testing-library/react"; + +describe("CitizenLogin", () => { + + + + it("renders an email input", () => { + render(); + const emailInput = screen.getByLabelText("Email"); + expect(emailInput).toHaveAttribute("type", "email"); + }); + + it("renders a password input", () => { + render(); + const passwordInput = screen.getByLabelText("Password"); + expect(passwordInput).toHaveAttribute("type", "password"); + }); + + + it("renders a forgot password link", () => { + render(); + const forgotPasswordLink = screen.getByText("Forgot password?"); + expect(forgotPasswordLink).toBeInTheDocument(); + }); + + + // it("renders a heading", () => { + // const forgotPasswordLink = screen.getByText("Forgot pasword?"); + // expect(forgotPasswordLink).toBeInTheDocument(); + // }); + +}) \ No newline at end of file diff --git a/frontend/__tests__/unit/button.test.tsx b/frontend/__tests__/unit/button.test.tsx deleted file mode 100644 index 89626c0a..00000000 --- a/frontend/__tests__/unit/button.test.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { render, screen } from "@testing-library/react"; - -describe("Button", () => { - it("renders correctly", () => { - //write unit test for button to ensure it renders correctly - }) -}) \ No newline at end of file diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 101e64a7..3bb23d7f 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -1,11 +1,11 @@ import type { Config } from 'jest' import nextJest from 'next/jest.js' - + const createJestConfig = nextJest({ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment dir: './', }) - + // Add any custom config to be passed to Jest const config: Config = { coverageProvider: 'v8', @@ -22,6 +22,6 @@ const config: Config = { // coverageReporters: [['html', {}]], } - + // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async export default createJestConfig(config) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 786d67d1..9935e522 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,12 +10,13 @@ "dependencies": { "@nextui-org/react": "^2.3.6", "framer-motion": "^11.2.6", + "icons": "^1.0.0", + "lucide-react": "^0.381.0", "next": "14.2.3", "react": "^18", "react-dom": "^18" }, "devDependencies": { - "@playwright/test": "^1.44.1", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@types/jest": "^29.5.12", @@ -2936,21 +2937,6 @@ "node": ">=14" } }, - "node_modules/@playwright/test": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", - "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", - "devOptional": true, - "dependencies": { - "playwright": "1.44.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.13.tgz", @@ -7302,6 +7288,12 @@ "node": ">=10.17.0" } }, + "node_modules/icons": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/icons/-/icons-1.0.0.tgz", + "integrity": "sha512-7POum3AHKovjEEXg4ITL2opTzGjnN2UnUIhv5LsUX+SjKBsFMjRyANShrCubt0KQdNdcX2wAym+JXOt6LceuYA==", + "deprecated": "Package no longer supported. Contact support@npmjs.com for more info." + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -9132,6 +9124,14 @@ "node": "14 || >=16.14" } }, + "node_modules/lucide-react": { + "version": "0.381.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.381.0.tgz", + "integrity": "sha512-cft0ywFfHkGprX5pwKyS9jme/ksh9eYAHSZqFRKN0XGp70kia4uqZOTPB+i+O51cqiJlvGLqzMGWnMHaeJTs3g==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -9832,50 +9832,6 @@ "node": ">=8" } }, - "node_modules/playwright": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", - "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", - "devOptional": true, - "dependencies": { - "playwright-core": "1.44.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", - "devOptional": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8eb69799..ccab40ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,18 +8,18 @@ "start": "next start", "lint": "next lint", "test": "jest", - "test:watch": "jest --watch", - "test:e2e": "npx playwright test" + "test:watch": "jest --watch" }, "dependencies": { "@nextui-org/react": "^2.3.6", "framer-motion": "^11.2.6", + "icons": "^1.0.0", + "lucide-react": "^0.381.0", "next": "14.2.3", "react": "^18", "react-dom": "^18" }, "devDependencies": { - "@playwright/test": "^1.44.1", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@types/jest": "^29.5.12", diff --git a/frontend/src/app/globals.scss b/frontend/src/app/globals.scss index 875c01e8..ad9b49ad 100644 --- a/frontend/src/app/globals.scss +++ b/frontend/src/app/globals.scss @@ -17,13 +17,13 @@ } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + // color: rgb(var(--foreground-rgb)); + // background: linear-gradient( + // to bottom, + // transparent, + // rgb(var(--background-end-rgb)) + // ) + // rgb(var(--background-start-rgb)); } @layer utilities { diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index cd398d11..ba044677 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -1,9 +1,27 @@ +'use client' + +import { Button } from "@nextui-org/react"; import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from 'next/navigation'; + +import styles from "./styles.module.scss"; export default function Home() { + const router = useRouter(); + return ( -
+

Hello

+ + {/* The linking or routing examples */} + + Go to signup using method 1 + + + + +

Get started by adding new routes to  diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx new file mode 100644 index 00000000..4b045c5d --- /dev/null +++ b/frontend/src/app/login/page.tsx @@ -0,0 +1,41 @@ +'use client' + +import { Tabs, Tab } from "@nextui-org/react"; +import CitizenLogin from "@/components/Login/CitizenLogin"; +import MunicipalityLogin from "@/components/Login/MunicipalityLogin"; +import ServiceProviderLogin from "@/components/Login/ServiceProviderLogin"; + + +export default function Signup() { + const formHeader: string = "Sign In."; + + return ( +

+ +
+ + {formHeader} + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/app/signup/page.tsx b/frontend/src/app/signup/page.tsx new file mode 100644 index 00000000..0a50ec28 --- /dev/null +++ b/frontend/src/app/signup/page.tsx @@ -0,0 +1,52 @@ +'use client' + +import React, { Key, useState } from "react"; +import { Tabs, Tab } from "@nextui-org/react"; + +import CitizenSignup from "@/components/Signup/CitizenSignup"; +import MunicipalitySignup from "@/components/Signup/MunicipalitySignup"; +import ServiceProviderSignup from "@/components/Signup/ServiceProviderSignup"; + + + +export default function Signup() { + const headers: string[] = ["Get Connected.", "Take Control.", "Be The Change."]; + const [currentFormHeader, setCurrentFormHeader] = useState(headers[0]); + + + const handleTabChange = (key: Key) => { + const index = Number(key); + setCurrentFormHeader(headers[index]); + }; + + + return ( +
+ +
+ + {currentFormHeader} + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/components/Login/CitizenLogin.tsx b/frontend/src/components/Login/CitizenLogin.tsx new file mode 100644 index 00000000..0e4f5253 --- /dev/null +++ b/frontend/src/components/Login/CitizenLogin.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { Input, Button } from '@nextui-org/react'; +import Link from 'next/link'; + + +export default function CitizenLogin() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // Handle the submit action here + console.log(`User Type: Citizen, Email: ${email}, Password: ${password}`); + }; + + return ( +
+
+ + Email} + labelPlacement={"outside"} + classNames={{ + inputWrapper: "h-[3em]", + }} + type="email" + autoComplete="new-email" + placeholder="example@mail.com" + value={email} onChange={(event) => setEmail(event.target.value)} /> + + Password} + labelPlacement={"outside"} + classNames={{ + inputWrapper: "h-[3em]", + }} + type="password" + autoComplete="new-password" + placeholder="Password" + value={password} + onChange={(event) => setPassword(event.target.value)} /> + + Forgot password? + + + {/* Social Media Sign Up Options */} + {/* Render different options based on userType */} +
+
+ ); +} diff --git a/frontend/src/components/Login/MunicipalityLogin.tsx b/frontend/src/components/Login/MunicipalityLogin.tsx new file mode 100644 index 00000000..b9f7dea5 --- /dev/null +++ b/frontend/src/components/Login/MunicipalityLogin.tsx @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import { Input, Button, Autocomplete, AutocompleteItem } from '@nextui-org/react'; +import { Building2, CircleHelp } from 'lucide-react'; + +export default function MunicipalityLogin() { + const [municipality, setMunicipality] = useState(''); + const [password, setPassword] = useState(''); + + + type Municipality = { + id: number | string; + name: string; + }; + + + const municipalities: Municipality[] = [ + { id: 0, name: "City of Ekurhuleni" }, + { id: 1, name: "City of Johannesburg" }, + { id: 2, name: "City of Tshwane" }, + ]; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // console.log(`Province: ${province}, Municipality: ${municipality}, Verification Code: ${verificationCode}`); + }; + + return ( +
+
+ + Select Your Municipality} + labelPlacement="outside" + placeholder="Municipality" + fullWidth + defaultItems={municipalities} + disableSelectorIconRotation + isClearable={false} + type="text" + autoComplete="new-municipality" + menuTrigger={"input"} + size={"lg"} + onChange={(event) => setMunicipality(event.target.value)} + > + {(municipality) => + +
+ + {municipality.name} +
+
} +
+ + MuniCodeTM } + labelPlacement={"outside"} + classNames={{ + inputWrapper: "h-[3em]", + }} + type="password" + autoComplete="new-password" + placeholder="Password" + value={password} + onChange={(event) => setPassword(event.target.value)} /> + + + + +
+ ); +} diff --git a/frontend/src/components/Login/ServiceProviderLogin.tsx b/frontend/src/components/Login/ServiceProviderLogin.tsx new file mode 100644 index 00000000..0ff6aef3 --- /dev/null +++ b/frontend/src/components/Login/ServiceProviderLogin.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { Input, Button, Autocomplete, AutocompleteItem } from '@nextui-org/react'; +import Link from 'next/link'; +import { CircleHelp } from 'lucide-react'; + + +export default function ServiceProviderLogin() { + // const [email, setEmail] = useState(''); + const [company, setCompany] = useState(''); + const [password, setPassword] = useState(''); + + type Company = { + id: number | string; + name: string; + }; + + + const companies: Company[] = [ + { id: 0, name: "Bob's Electronics" }, + { id: 1, name: "West Coast Saviours" }, + { id: 2, name: "Aubrey's Angels" }, + ]; + + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // Handle the submit action here + // console.log(`User Type: Organization, Email: ${email}, Password: ${password}`); + }; + + return ( +
+
+ + Company Name} + labelPlacement="outside" + placeholder="e.g Plumbing" + defaultItems={companies} + fullWidth + disableSelectorIconRotation + isClearable={false} + size={"lg"} + type="text" + autoComplete="new-company" + menuTrigger={"input"} + onChange={(event) => setCompany(event.target.value)} + > + {(company) => {company.name}} + + + Comapny Password} + labelPlacement={"outside"} + classNames={{ + inputWrapper: "h-[3em]", + }} + type="password" + autoComplete="new-password" + placeholder="Company Password" + value={password} + onChange={(event) => setPassword(event.target.value)} /> + + Forgot password? + + + {/* Social Media Sign Up Options */} + {/* Render different options based on userType */} + +
+ ); +} diff --git a/frontend/src/components/Signup/CitizenSignup.tsx b/frontend/src/components/Signup/CitizenSignup.tsx new file mode 100644 index 00000000..fa4c81fa --- /dev/null +++ b/frontend/src/components/Signup/CitizenSignup.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { Input, Button } from '@nextui-org/react'; + + +export default function CitizenSignup() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // Handle the submit action here + console.log(`User Type: Citizen, Email: ${email}, Password: ${password}`); + }; + + return ( +
+
+ + Email} + labelPlacement={"outside"} + classNames={{ + inputWrapper: "h-[3em]", + }} + type="email" + autoComplete="new-email" + placeholder="example@mail.com" + value={email} + onChange={(event) => setEmail(event.target.value)} /> + + Create Password} + labelPlacement={"outside"} + classNames={{ + inputWrapper: "h-[3em]", + }} + type="password" + autoComplete="new-password" + placeholder="Password" + value={password} + onChange={(event) => setPassword(event.target.value)} /> + + + {/* Social Media Sign Up Options */} + {/* Render different options based on userType */} +
+
+ ); +} diff --git a/frontend/src/components/Signup/MunicipalitySignup.tsx b/frontend/src/components/Signup/MunicipalitySignup.tsx new file mode 100644 index 00000000..3601dcb7 --- /dev/null +++ b/frontend/src/components/Signup/MunicipalitySignup.tsx @@ -0,0 +1,115 @@ +import React, { useState } from 'react'; +import { Input, Button, Autocomplete, AutocompleteItem } from '@nextui-org/react'; +import { Building2 } from 'lucide-react'; + +export default function MunicipalitySignup() { + const [province, setProvince] = useState('Gauteng'); + const [municipality, setMunicipality] = useState(''); + const [email, setEmail] = useState(''); + // const [verificationCode, setVerificationCode] = useState('123456'); + // const [showCode, setShowCode] = useState(false); + + type Province = { + id: number | string; + name: string; + }; + + type Municipality = { + id: number | string; + name: string; + }; + + + const provinces: Province[] = [ + { id: 0, name: "Gauteng" }, + { id: 1, name: "KwaZulu-Natal" }, + { id: 2, name: "Western Cape" }, + { id: 3, name: "Eastern Cape" }, + { id: 4, name: "Limpopo" }, + { id: 5, name: "Mpumalanga" }, + { id: 6, name: "Northen Cape" }, + { id: 7, name: "North West" }, + { id: 8, name: "Free State" } + ]; + + const municipalities: Municipality[] = [ + { id: 0, name: "City of Ekurhuleni" }, + { id: 1, name: "City of Johannesburg" }, + { id: 2, name: "City of Tshwane" }, + ]; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // console.log(`Province: ${province}, Municipality: ${municipality}, Verification Code: ${verificationCode}`); + }; + + return ( +
+
+ + + Province} + labelPlacement="outside" + placeholder="Gauteng" + defaultItems={provinces} + fullWidth + disableSelectorIconRotation + isClearable={false} + size={"lg"} + type="text" + autoComplete="new-province" + menuTrigger={"input"} + onChange={(event) => setProvince(event.target.value)} + > + {(province) => {province.name}} + + + + Select Your Municipality} + labelPlacement="outside" + placeholder="Municipality" + fullWidth + defaultItems={municipalities} + disableSelectorIconRotation + isClearable={false} + menuTrigger={"input"} + size={"lg"} + type="text" + autoComplete="new-municipality" + onChange={(event) => setMunicipality(event.target.value)} + > + {(municipality) => + +
+ + {municipality.name} +
+
} +
+ + setEmail(e.target.value)} + /> + + + + +
+
+ ); +} diff --git a/frontend/src/components/Signup/ServiceProviderSignup.tsx b/frontend/src/components/Signup/ServiceProviderSignup.tsx new file mode 100644 index 00000000..7ff90899 --- /dev/null +++ b/frontend/src/components/Signup/ServiceProviderSignup.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { Input, Button, Autocomplete, AutocompleteItem } from '@nextui-org/react'; +import { Upload } from 'lucide-react'; + + +export default function ServiceProviderSignup() { + const [email, setEmail] = useState(''); + const [serviceArea, setServiceArea] = useState(''); + + + + type ServiceArea = { + id: number | string; + name: string; + }; + + + const serviceAreas: ServiceArea[] = [ + { id: 0, name: "Plumbing" }, + { id: 1, name: "Cleaning" }, + { id: 2, name: "Pest Control" }, + { id: 3, name: "Landscaping" }, + { id: 4, name: "Electrical Services" } + ]; + + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // Handle the submit action here + // console.log(`User Type: Organization, Email: ${email}, Password: ${password}`); + }; + + return ( +
+
+ + setEmail(e.target.value)} + /> + + +
+ Add Company Branding + +
+
+ +
+ + Upload a file or drag and drop. + +
+
+ + + Service Area} + labelPlacement="outside" + placeholder="e.g Plumbing" + defaultItems={serviceAreas} + fullWidth + disableSelectorIconRotation + isClearable={false} + size={"lg"} + type="text" + autoComplete="new-service-area" + menuTrigger={"input"} + onChange={(event) => setServiceArea(event.target.value)} + > + {(serviceArea) => {serviceArea.name}} + + + + setEmail(e.target.value)} + /> + + + + +
+
+ ); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 587bed14..c335abe5 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,7 +19,7 @@ ], "baseUrl": "./", "paths": { - "@/*": ["./src/*"], + "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],