├── README.md ├── .DS_Store ├── .prettierrc ├── backend ├── .prettierrc ├── nest-cli.json ├── tsconfig.build.json ├── src │ ├── app.service.ts │ ├── auth │ │ ├── guards │ │ │ └── jwt-auth.guard.ts │ │ ├── encryption.config.ts │ │ ├── auth.module.ts │ │ ├── jwt.strategy.ts │ │ ├── auth.controller.ts │ │ └── auth.service.ts │ ├── error │ │ ├── not-logged-in-error.ts │ │ ├── unknown-email-error.ts │ │ ├── incorrect-password-error.ts │ │ └── email-taken-error.ts │ ├── entities │ │ ├── tags.entity.ts │ │ ├── benefits.entity.ts │ │ ├── side-effects.entity.ts │ │ ├── things-to-know.entity.ts │ │ ├── user.entity.ts │ │ └── contraceptive.entity.ts │ ├── user │ │ ├── user.module.ts │ │ └── user.service.ts │ ├── types │ │ └── user.ts │ ├── main.ts │ ├── contraceptive │ │ ├── contraceptive.module.ts │ │ ├── contraceptive.service.spec.ts │ │ ├── contraceptive.controller.spec.ts │ │ ├── contraceptive.controller.ts │ │ └── contraceptive.service.ts │ ├── decorators │ │ └── currentuser.decorator.ts │ ├── app.controller.spec.ts │ ├── app.controller.ts │ └── app.module.ts ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts ├── .env.dev ├── tsconfig.json ├── .eslintrc.js ├── package.json └── README.md ├── frontend ├── .DS_Store ├── classes │ └── response-classes.ts ├── public │ ├── favicon.ico │ ├── home-image.png │ ├── bookmark.svg │ ├── right-arrow.svg │ ├── menu.svg │ ├── implant.svg │ ├── eye.svg │ ├── x.svg │ ├── desktop-icons │ │ ├── desktop-dropdown.svg │ │ ├── search.svg │ │ ├── desktop-bookmark.svg │ │ ├── profile.svg │ │ └── settings.svg │ ├── bookmark-nav-bar.svg │ ├── facebook.svg │ ├── google.svg │ ├── vercel.svg │ ├── quick-access.svg │ ├── questionnaire.svg │ ├── take-questionnaire.svg │ ├── welcome.svg │ ├── depressed.svg │ ├── acne.svg │ └── doctor.svg ├── components │ ├── Pill.tsx │ ├── Menubar.tsx │ ├── Layout.tsx │ ├── slide.module.scss │ ├── PillRow.tsx │ ├── Card.tsx │ ├── Toast.tsx │ ├── NavBar.tsx │ └── Sidebar.tsx ├── next-env.d.ts ├── pages │ ├── _app.tsx │ ├── index.tsx │ ├── test.tsx │ ├── _document.js │ ├── home.tsx │ ├── welcome.tsx │ ├── implant.tsx │ └── signin.tsx ├── next.config.js ├── templates │ ├── contraceptives │ │ ├── tabs │ │ │ ├── StyledComponents │ │ │ │ ├── Readme.md │ │ │ │ └── index.tsx │ │ │ ├── AdditionalInfo.tsx │ │ │ ├── PracticalQuestions.tsx │ │ │ ├── Mechanism.tsx │ │ │ ├── Effect.tsx │ │ │ ├── Overview.tsx │ │ │ ├── Use.tsx │ │ │ └── Efficacy.tsx │ │ └── index.tsx │ ├── mediaSizes.tsx │ └── TabBar.tsx ├── styles │ ├── globals.css │ └── Home.module.css ├── tsconfig.json ├── babel.config.js ├── hooks │ └── UseWindowSize.tsx ├── package.json ├── README.md └── api-client │ └── index.ts ├── tsconfig.json ├── database ├── create-database.sh ├── parse-form.py └── contraceptives.csv ├── .eslintrc.js ├── LICENSE ├── package.json └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # knowyouroptions -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/knowyouroptions/main/.DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /frontend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/knowyouroptions/main/frontend/.DS_Store -------------------------------------------------------------------------------- /frontend/classes/response-classes.ts: -------------------------------------------------------------------------------- 1 | export class Redirect { 2 | redirect!: string; 3 | } 4 | -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/knowyouroptions/main/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/home-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/knowyouroptions/main/frontend/public/home-image.png -------------------------------------------------------------------------------- /backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/auth/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/.env.dev: -------------------------------------------------------------------------------- 1 | DB_URL=postgres://postgres:mysecretpassword@localhost:5432/my_database 2 | JWT_SECRET=fakepassword 3 | JWT_EXPIRATION=2629800000 4 | ENCRYPTION_KEY=e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61 5 | DOMAIN=http://localhost:3001 6 | -------------------------------------------------------------------------------- /backend/src/error/not-logged-in-error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class NotLoggedInError extends HttpException { 4 | constructor() { 5 | super('User is not logged in.', HttpStatus.FORBIDDEN); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/components/Pill.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Pill = styled.div` 4 | border: 1px solid #1da3aa; 5 | border-radius: 0.25rem; 6 | color: #1da3aa; 7 | margin-right: 1rem; 8 | `; 9 | 10 | export default Pill; 11 | -------------------------------------------------------------------------------- /backend/src/error/unknown-email-error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class UnknownEmailError extends HttpException { 4 | constructor() { 5 | super('The email provided cannot be found.', HttpStatus.CONFLICT); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/entities/tags.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, BaseEntity } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Tag extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | label: string; 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/error/incorrect-password-error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class IncorrectPasswordError extends HttpException { 4 | constructor() { 5 | super('The password provided is incorrect.', HttpStatus.CONFLICT); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import { AppProps } from 'next/app'; 3 | import { ReactElement } from 'react'; 4 | 5 | function MyApp({ Component, pageProps }: AppProps): ReactElement { 6 | return ; 7 | } 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /backend/src/entities/benefits.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, BaseEntity } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Benefit extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | description: string; 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/entities/side-effects.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, BaseEntity } from 'typeorm'; 2 | 3 | @Entity() 4 | export class SideEffect extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | description: string; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/public/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/src/error/email-taken-error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class EmailIsTakenError extends HttpException { 4 | constructor() { 5 | super( 6 | 'The email provided is already associated with an existing account.', 7 | HttpStatus.CONFLICT, 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/entities/things-to-know.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, BaseEntity } from 'typeorm'; 2 | 3 | @Entity() 4 | export class ThingToKnow extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | title: string; 10 | 11 | @Column() 12 | description: string; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/auth/encryption.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | 3 | const config = dotenv.config({ 4 | path: process.env.NODE_ENV !== 'production' ? '.env.dev' : '.env', 5 | }); 6 | 7 | export const EncryptionTransformerConfig = { 8 | key: config.parsed.ENCRYPTION_KEY, 9 | algorithm: 'aes-256-cbc', 10 | ivLength: 16, 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | webpack: (config, options) => { 5 | config.module.rules.push({ 6 | test: /\.svg$/, 7 | use: ['@svgr/webpack'], 8 | }); 9 | 10 | // Important: return the modified config 11 | return config; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/public/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | import Image from 'next/image'; 4 | import styles from '../styles/Home.module.css'; 5 | import HomePage from './home'; 6 | import Implant from './implant'; 7 | 8 | const Home: NextPage = () => { 9 | return ; 10 | }; 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/StyledComponents/Readme.md: -------------------------------------------------------------------------------- 1 | # Styled Components for Tab screens 2 | 3 | This will contain styling for repeatedly used elements such as < h3 > tags and repeatedly used syling such as rows. 4 | 5 | This will make it easier to change the overall look of the tabs, by simply changing the components in this file while 6 | making the look of each tab the same. 7 | -------------------------------------------------------------------------------- /backend/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { User } from '../entities/user.entity'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | 6 | @Module({ 7 | imports: [TypeOrmModule.forFeature([User])], 8 | providers: [UserService], 9 | exports: [UserService], 10 | }) 11 | export class UserModule {} 12 | -------------------------------------------------------------------------------- /frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: Roboto, din-2014, -apple-system, BlinkMacSystemFont, Segoe UI, 6 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 7 | sans-serif; 8 | height: 100%; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/public/implant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /backend/src/types/user.ts: -------------------------------------------------------------------------------- 1 | export type UserInfo = SignInInfo & { 2 | readonly name: string; 3 | }; 4 | 5 | export type SignInInfo = { 6 | readonly email: string; 7 | readonly password: string; 8 | }; 9 | 10 | export type AuthenticatedUser = { 11 | readonly id: number; 12 | readonly name?: string; 13 | readonly email: string; 14 | readonly accessToken: string; 15 | }; 16 | 17 | export type UserAuthPayload = { 18 | readonly userId: number; 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/public/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "jsx": "react", 15 | "esModuleInterop": true 16 | }, 17 | 18 | "exclude": ["**/node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/components/Menubar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useState } from 'react'; 2 | import Sidebar from './Sidebar'; 3 | import NavBar from './NavBar'; 4 | import { size } from '../templates/mediaSizes'; 5 | import useWindowSize from '../hooks/UseWindowSize'; 6 | const Menubar = (): ReactElement => { 7 | const windowSize = useWindowSize(); 8 | 9 | if (!windowSize.width) return ; 10 | 11 | return windowSize.width < size.laptop + 1 ? : ; 12 | }; 13 | 14 | export default Menubar; 15 | -------------------------------------------------------------------------------- /frontend/public/desktop-icons/desktop-dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import * as cookieParser from 'cookie-parser'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.enableCors({ 8 | credentials: true, 9 | exposedHeaders: ['set-cookie'], 10 | allowedHeaders: ['Content-Type', 'X-Requested-With'], 11 | origin: ['http://localhost:3000'], 12 | }); 13 | app.use(cookieParser()); 14 | await app.listen(3001); 15 | } 16 | bootstrap(); 17 | -------------------------------------------------------------------------------- /backend/src/contraceptive/contraceptive.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ContraceptiveService } from './contraceptive.service'; 3 | import { ContraceptiveController } from './contraceptive.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Contraceptive } from '../entities/contraceptive.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Contraceptive])], 9 | providers: [ContraceptiveService], 10 | controllers: [ContraceptiveController], 11 | }) 12 | export class ContraceptiveModule {} 13 | -------------------------------------------------------------------------------- /backend/src/decorators/currentuser.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { User } from '../entities/user.entity'; 3 | 4 | /** 5 | * Decorator to access the currently authenticated user 6 | * Use this in method parameters only when the JwtAuthGuard is used 7 | */ 8 | export const CurrentUser = createParamDecorator( 9 | async (relations: string[], ctx: ExecutionContext) => { 10 | const request = ctx.switchToHttp().getRequest(); 11 | return await User.findOne(request.user.id, { relations }); 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/pages/test.tsx: -------------------------------------------------------------------------------- 1 | import { API } from '../api-client'; 2 | import useSWR from 'swr'; 3 | import Sidebar from '../components/Sidebar'; 4 | import styled from 'styled-components'; 5 | 6 | const Background = styled.div` 7 | background-color: #ef8b6f; 8 | width: 100vw; 9 | height: 100vh; 10 | position: relative; 11 | `; 12 | 13 | const Test = () => { 14 | const { data } = useSWR('/', async () => API.helloWorld.get()); 15 | 16 | return ( 17 | 18 | 19 | {data} 20 | 21 | ); 22 | }; 23 | 24 | export default Test; 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/contraceptive/contraceptive.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ContraceptiveService } from './contraceptive.service'; 3 | 4 | describe('ContraceptiveService', () => { 5 | let service: ContraceptiveService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ContraceptiveService], 10 | }).compile(); 11 | 12 | service = module.get(ContraceptiveService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import MenuBar from './Menubar'; 2 | import styled from 'styled-components'; 3 | import { ReactElement } from 'react'; 4 | import { colors } from '../templates/mediaSizes'; 5 | const PageContainer = styled.div` 6 | width: 100vw; 7 | height: 100vh; 8 | background-color: ${colors.homepageBackground}; 9 | `; 10 | 11 | interface LayoutProps { 12 | children: JSX.Element | string; 13 | } 14 | 15 | const Layout = ({ children }: LayoutProps): ReactElement => { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default Layout; 25 | -------------------------------------------------------------------------------- /backend/src/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { EncryptionTransformerConfig } from 'src/auth/encryption.config'; 2 | import { Entity, Column, PrimaryGeneratedColumn, BaseEntity } from 'typeorm'; 3 | import { EncryptionTransformer } from 'typeorm-encrypted'; 4 | 5 | @Entity() 6 | export class User extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column() 11 | email: string; 12 | 13 | @Column({ 14 | type: 'varchar', 15 | nullable: false, 16 | transformer: new EncryptionTransformer(EncryptionTransformerConfig), 17 | }) 18 | password: string; 19 | 20 | @Column({ nullable: true }) 21 | name: string; 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/contraceptive/contraceptive.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ContraceptiveController } from './contraceptive.controller'; 3 | 4 | describe('ContraceptiveController', () => { 5 | let controller: ContraceptiveController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ContraceptiveController], 10 | }).compile(); 11 | 12 | controller = module.get(ContraceptiveController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/components/slide.module.scss: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes slide { 2 | 100% { 3 | left: 0; 4 | } 5 | } 6 | 7 | .slide { 8 | position: absolute; 9 | left: -300px; 10 | -webkit-animation: slide 0.5s forwards; 11 | animation: slide 0.5s forwards; 12 | } 13 | 14 | @keyframes slide { 15 | 100% { 16 | left: 0px; 17 | } 18 | } 19 | 20 | .close { 21 | position: absolute; 22 | left: 0px; 23 | -webkit-animation: close 0.5s forwards; 24 | animation: close 0.5s forwards; 25 | } 26 | 27 | @-webkit-keyframes close { 28 | 100% { 29 | left: -350px; 30 | } 31 | } 32 | 33 | @keyframes close { 34 | 100% { 35 | left: -350px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/public/bookmark-nav-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | 'next/babel', 5 | { 6 | 'class-properties': { 7 | loose: true, 8 | }, 9 | }, 10 | ], 11 | ], 12 | plugins: [ 13 | [ 14 | '@babel/plugin-proposal-decorators', 15 | { 16 | legacy: true, 17 | }, 18 | ], 19 | ['@babel/plugin-proposal-private-property-in-object', { loose: true }], 20 | ['@babel/plugin-proposal-private-methods', { loose: true }], 21 | [ 22 | 'styled-components', 23 | { 24 | ssr: true, 25 | displayName: true, 26 | preprocess: false, 27 | }, 28 | ], 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /database/create-database.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | #!/bin/bash 4 | set -e 5 | 6 | SERVER="my_database_server"; 7 | PW="mysecretpassword"; 8 | DB="my_database"; 9 | 10 | echo "echo stop & remove old docker [$SERVER] and starting new fresh instance of [$SERVER]" 11 | (docker kill $SERVER || :) && \ 12 | (docker rm $SERVER || :) && \ 13 | docker run --name $SERVER -e POSTGRES_PASSWORD=$PW \ 14 | -e PGPASSWORD=$PW \ 15 | -p 5432:5432 \ 16 | -d postgres 17 | 18 | # wait for pg to start 19 | echo "sleep wait for pg-server [$SERVER] to start"; 20 | SLEEP 3; 21 | 22 | # create the db 23 | echo "CREATE DATABASE $DB ENCODING 'UTF-8';" | docker exec -i $SERVER psql -U postgres 24 | echo "\l" | docker exec -i $SERVER psql -U postgres -------------------------------------------------------------------------------- /frontend/public/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: ['./packages/*/tsconfig.json'], 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | ], 13 | ignorePatterns: ['.eslintrc.js'], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/public/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/public/desktop-icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/AdditionalInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Description, DescriptionBold, Subtitle } from './StyledComponents'; 4 | 5 | const Section = styled.div` 6 | margin-bottom: 2rem; 7 | `; 8 | 9 | export interface AdditionalProps { 10 | info: Array>; 11 | } 12 | 13 | const AdditionalInfo = ({ info }: AdditionalProps) => { 14 | return ( 15 | <> 16 | Things to notice about this method: 17 | {info.map((section: Array) => { 18 | return ( 19 |
20 | {section[0]} 21 | {section[1]} 22 |
23 | ); 24 | })} 25 | 26 | ); 27 | }; 28 | 29 | export default AdditionalInfo; 30 | -------------------------------------------------------------------------------- /backend/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { UserModule } from '../user/user.module'; 6 | import { PassportModule } from '@nestjs/passport'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | 9 | @Module({ 10 | imports: [ 11 | JwtModule.register({ 12 | secret: process.env.JWT_SECRET, 13 | signOptions: { expiresIn: process.env.JWT_EXPIRATION }, 14 | }), 15 | PassportModule.register({ 16 | defaultStrategy: 'jwt', 17 | property: 'user', 18 | session: false, 19 | }), 20 | UserModule, 21 | ], 22 | controllers: [AuthController], 23 | providers: [AuthService, JwtStrategy], 24 | exports: [AuthService, JwtModule, PassportModule], 25 | }) 26 | export class AuthModule {} 27 | -------------------------------------------------------------------------------- /frontend/components/PillRow.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import Pill from './Pill'; 4 | 5 | const PillContainer = styled.div` 6 | display: flex; 7 | flex-direction: row; 8 | `; 9 | 10 | const PillContainerStyled = styled(PillContainer)` 11 | & > div { 12 | font-weight: 500; 13 | padding: 0.25rem 0.5rem; 14 | } 15 | `; 16 | 17 | const PillRow = ({ 18 | className, 19 | pillTitles, 20 | }: { 21 | className?: string; 22 | pillTitles: string[]; 23 | }): ReactElement => { 24 | const Container = 25 | className === undefined ? PillContainerStyled : PillContainer; 26 | return ( 27 | 28 | {pillTitles.map( 29 | (title: string): ReactElement => ( 30 | {title} 31 | ), 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | export default PillRow; 38 | -------------------------------------------------------------------------------- /frontend/hooks/UseWindowSize.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useState } from 'react'; 2 | // Hook - https://usehooks.com/useWindowSize/ 3 | const useWindowSize = () => { 4 | const initialState: { 5 | width: number | undefined; 6 | height: number | undefined; 7 | } = { 8 | width: undefined, 9 | height: undefined, 10 | }; 11 | 12 | const [windowSize, setWindowSize] = useState(initialState); 13 | 14 | useEffect(() => { 15 | // Handler to call on window resize 16 | function handleResize() { 17 | // Set window width/height to state 18 | setWindowSize({ 19 | width: window.innerWidth, 20 | height: window.innerHeight, 21 | }); 22 | } 23 | 24 | window.addEventListener('resize', handleResize); 25 | handleResize(); 26 | 27 | return () => window.removeEventListener('resize', handleResize); 28 | }, []); 29 | 30 | return windowSize; 31 | }; 32 | 33 | export default useWindowSize; 34 | -------------------------------------------------------------------------------- /frontend/public/desktop-icons/desktop-bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/src/contraceptive/contraceptive.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Param, Delete } from '@nestjs/common'; 2 | import { ContraceptiveService } from './contraceptive.service'; 3 | import { Contraceptive } from 'src/entities/contraceptive.entity'; 4 | 5 | @Controller('contraceptive') 6 | export class ContraceptiveController { 7 | constructor(private contraceptiveService: ContraceptiveService) {} 8 | 9 | @Get() 10 | async getContraceptives() { 11 | return this.contraceptiveService.getContraceptives(); 12 | } 13 | 14 | @Get(':name') 15 | public getContraceptive(@Param('name') name: string) { 16 | return this.contraceptiveService.getContraceptive(name); 17 | } 18 | 19 | @Post() 20 | async postContraceptive(@Body() contraceptive: Contraceptive) { 21 | return this.contraceptiveService.postContraceptive(contraceptive); 22 | } 23 | @Delete(':name') 24 | public deleteContraceptive(@Param('name') name: string) { 25 | return this.contraceptiveService.deleteContraceptive(name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^4.7.0", 13 | "@nestjs/common": "^8.2.3", 14 | "@types/styled-components": "^5.1.15", 15 | "antd": "^4.16.13", 16 | "axios": "^0.22.0", 17 | "class-transformer": "0.3.1", 18 | "class-validator": "^0.13.1", 19 | "next": "11.1.2", 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2", 22 | "sass": "^1.43.2", 23 | "styled-components": "^5.3.1", 24 | "swr": "^1.0.1" 25 | }, 26 | "devDependencies": { 27 | "@babel/plugin-proposal-decorators": "^7.16.0", 28 | "@babel/plugin-proposal-private-property-in-object": "^7.16.0", 29 | "@babel/preset-react": "^7.16.0", 30 | "@svgr/webpack": "^5.5.0", 31 | "@types/react": "17.0.27", 32 | "eslint": "^7.32.0", 33 | "eslint-config-next": "11.1.2", 34 | "typescript": "4.4.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /backend/src/contraceptive/contraceptive.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Contraceptive } from 'src/entities/contraceptive.entity'; 4 | import { Repository } from 'typeorm'; 5 | export class ContraceptiveService { 6 | constructor( 7 | @InjectRepository(Contraceptive) 8 | private readonly contraceptiveRepository: Repository, 9 | ) {} 10 | 11 | public async getContraceptives(): Promise { 12 | const contraceptiveList = await this.contraceptiveRepository.find(); 13 | return contraceptiveList; 14 | } 15 | 16 | public async getContraceptive(name: string) { 17 | const contraceptive = this.contraceptiveRepository.findOne({ 18 | where: [{ name: name }], 19 | }); 20 | return contraceptive; 21 | } 22 | 23 | public async postContraceptive(contraceptive) { 24 | return this.contraceptiveRepository.insert(contraceptive); 25 | } 26 | public async deleteContraceptive(name: string) { 27 | return this.contraceptiveRepository.delete({ name: name }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sandbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /frontend/templates/mediaSizes.tsx: -------------------------------------------------------------------------------- 1 | // from https://jsramblings.com/how-to-use-media-queries-with-styled-components/ 2 | // thx Stefan <3 very cool 3 | 4 | export const size = { 5 | mobileS: 320, 6 | mobileM: 375, 7 | mobileL: 425, 8 | tablet: 768, 9 | laptop: 1024, 10 | laptopL: 1440, 11 | desktop: 2560, 12 | }; 13 | 14 | export const device = { 15 | mobileS: `(min-width: ${size.mobileS}px)`, 16 | mobileM: `(min-width: ${size.mobileM}px)`, 17 | mobileL: `(min-width: ${size.mobileL}px)`, 18 | tablet: `(min-width: ${size.tablet}px)`, 19 | laptop: `(min-width: ${size.laptop}px)`, 20 | laptopL: `(min-width: ${size.laptopL}px)`, 21 | desktop: `(min-width: ${size.desktop}px)`, 22 | desktopL: `(min-width: ${size.desktop}px)`, 23 | }; 24 | 25 | export const maxDevice = { 26 | mobileS: `(max-width: ${size.mobileS}px)`, 27 | mobileM: `(max-width: ${size.mobileM}px)`, 28 | mobileL: `(max-width: ${size.mobileL}px)`, 29 | tablet: `(max-width: ${size.tablet}px)`, 30 | laptop: `(max-width: ${size.laptop}px)`, 31 | laptopL: `(max-width: ${size.laptopL}px)`, 32 | desktop: `(max-width: ${size.desktop}px)`, 33 | desktopL: `(max-width: ${size.desktop}px)`, 34 | }; 35 | 36 | export const colors = { 37 | homepageBackground: '#ef8b6f', 38 | homepageNavBarDropdown: '#6abdc1', 39 | }; 40 | -------------------------------------------------------------------------------- /backend/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport'; 2 | import { JwtFromRequestFunction, Strategy } from 'passport-jwt'; 3 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 4 | import { AuthService } from './auth.service'; 5 | import { UserAuthPayload } from '../types/user'; 6 | import { User } from '../entities/user.entity'; 7 | import { ConfigService } from '@nestjs/config'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private readonly authService: AuthService, 13 | private readonly configService: ConfigService, 14 | ) { 15 | super({ 16 | jwtFromRequest: cookieExtractor, 17 | secretOrKey: configService.get('JWT_SECRET'), 18 | }); 19 | } 20 | 21 | async validate(payload: UserAuthPayload): Promise { 22 | // TODO: payload validation 23 | const user = await this.authService.validateUser(payload); 24 | if (!user) { 25 | throw new HttpException( 26 | 'User is not authenticated', 27 | HttpStatus.UNAUTHORIZED, 28 | ); 29 | } 30 | return user; 31 | } 32 | } 33 | 34 | const cookieExtractor: JwtFromRequestFunction = (req) => { 35 | let jwt = null; 36 | 37 | if (req && req.cookies) { 38 | jwt = req.cookies['auth_token']; 39 | } 40 | 41 | return jwt; 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | import { ServerStyleSheet } from 'styled-components'; 3 | 4 | export default class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const sheet = new ServerStyleSheet(); 7 | const originalRenderPage = ctx.renderPage; 8 | try { 9 | ctx.renderPage = () => 10 | originalRenderPage({ 11 | enhanceApp: (App) => (props) => 12 | sheet.collectStyles(), 13 | }); 14 | const initialProps = await Document.getInitialProps(ctx); 15 | return { 16 | ...initialProps, 17 | styles: ( 18 | <> 19 | {initialProps.styles} 20 | {sheet.getStyleElement()} 21 | 22 | ), 23 | }; 24 | } finally { 25 | sheet.seal(); 26 | } 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 | 33 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { JwtAuthGuard } from './auth/guards/jwt-auth.guard'; 4 | import { CurrentUser } from './decorators/currentuser.decorator'; 5 | import { User } from './entities/user.entity'; 6 | 7 | @Controller() 8 | export class AppController { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @Get() 12 | getHello(): string { 13 | return this.appService.getHello(); 14 | } 15 | 16 | /* 17 | NOTES: 18 | - JwtAuthGuard is an AuthGuard that restricts this route to only authenticated users 19 | - Currently, this means that anyone making a GET request to /name/ has to have 20 | a JWT auth_token in their cookies, attached to the request 21 | - JwtAuthGuard takes the request, verifies the JWT in the cookies, and injects 22 | the current user into the Request (see JwtStrategy) 23 | - AuthGuards can be used at the route level, or the controller level 24 | 25 | - @CurrentUser() is a decorator that allows us to access the currently authenticated 26 | user in a route that uses the JwtAuthGuard 27 | */ 28 | /** 29 | * Returns the name of the currently logged-in user 30 | * 31 | * @param user 32 | */ 33 | @Get('name') 34 | @UseGuards(JwtAuthGuard) 35 | async name(@CurrentUser() user: User): Promise { 36 | return user.name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ConfigModule } from '@nestjs/config'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { Tag } from './entities/tags.entity'; 7 | import { Benefit } from './entities/benefits.entity'; 8 | import { SideEffect } from './entities/side-effects.entity'; 9 | import { ThingToKnow } from './entities/things-to-know.entity'; 10 | import { Contraceptive } from './entities/contraceptive.entity'; 11 | import { User } from './entities/user.entity'; 12 | import { AuthModule } from './auth/auth.module'; 13 | import { UserModule } from './user/user.module'; 14 | import { ContraceptiveModule } from './contraceptive/contraceptive.module'; 15 | @Module({ 16 | imports: [ 17 | ConfigModule.forRoot({ 18 | envFilePath: [ 19 | process.env.NODE_ENV !== 'production' ? '.env.dev' : '.env', 20 | ], 21 | isGlobal: true, 22 | }), 23 | TypeOrmModule.forRoot({ 24 | type: 'postgres', 25 | url: process.env.DB_URL, 26 | entities: [Tag, Benefit, SideEffect, ThingToKnow, Contraceptive, User], 27 | synchronize: true, // TODO: synchronize true should not be used in a production environment 28 | }), 29 | AuthModule, 30 | UserModule, 31 | ContraceptiveModule, 32 | ], 33 | controllers: [AppController], 34 | providers: [AppService], 35 | }) 36 | export class AppModule {} 37 | -------------------------------------------------------------------------------- /frontend/public/desktop-icons/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/PracticalQuestions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | Description, 5 | DescriptionBold, 6 | List, 7 | ListItem, 8 | Subtitle, 9 | } from './StyledComponents'; 10 | 11 | const Cost = styled(DescriptionBold)` 12 | color: #1da3aa; 13 | margin-bottom: 0rem; 14 | `; 15 | 16 | const CostDescription = styled(Description)` 17 | margin-top: 0rem; 18 | `; 19 | 20 | export interface PracticalProps { 21 | access: Array; 22 | administration: string; 23 | lowPrice: number; 24 | highPrice: number; 25 | costInfo: string; 26 | } 27 | 28 | const PracticalQuestions = ({ 29 | access, 30 | administration, 31 | lowPrice, 32 | highPrice, 33 | costInfo, 34 | }: PracticalProps) => { 35 | return ( 36 | <> 37 | Where to access? 38 | {access[0]} 39 | 40 | {access.map((description: string, index: number) => { 41 | return ( 42 | index !== 0 && {description} 43 | ); 44 | })} 45 | 46 | Who will administer this method? 47 | {administration} 48 | How much could it cost? 49 | 50 | $ {lowPrice} — $ {highPrice}. 51 | 52 | {costInfo} 53 | 54 | ); 55 | }; 56 | 57 | export default PracticalQuestions; 58 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/Mechanism.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | Description, 5 | DescriptionBold, 6 | HighlightDescription, 7 | List, 8 | ListItem, 9 | Subtitle, 10 | } from './StyledComponents'; 11 | 12 | // Styling 13 | 14 | const Warning = styled(DescriptionBold)` 15 | font-style: italic; 16 | `; 17 | 18 | // Components 19 | 20 | export declare interface MechanismProps { 21 | mechanism: string; 22 | healthRisk: Array; 23 | whoCantUse: Array; 24 | warning?: string; 25 | } 26 | 27 | const Mechanism = ({ 28 | mechanism, 29 | healthRisk, 30 | whoCantUse, 31 | warning = '', 32 | }: MechanismProps): ReactElement => { 33 | // TODO: add highlighting keywords for healthRisk 34 | return ( 35 | <> 36 | How it works? 37 | {mechanism} 38 | Health Risk 39 | 40 | 41 | 42 | Who can't use? 43 | Medical history / illness 44 | {whoCantUse[0]} 45 | 46 | {whoCantUse.map((description: string, index: number) => { 47 | return ( 48 | index !== 0 && {description} 49 | ); 50 | })} 51 | 52 | {warning !== '' && {warning}} 53 | 54 | ); 55 | }; 56 | 57 | export default Mechanism; 58 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /frontend/public/desktop-icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/templates/TabBar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import { device } from './mediaSizes'; 4 | 5 | const HighlightedTab = styled.h2` 6 | border: 0rem; 7 | border-bottom: 0.2rem #5f034c; 8 | border-style: solid; 9 | cursor: pointer; 10 | font-size: 1rem; 11 | font-weight: bold; 12 | margin: 0 0.5rem; 13 | padding: 1rem 0.5rem; 14 | white-space: nowrap; 15 | `; 16 | 17 | const Row = styled.div` 18 | box-shadow: 0rem 0.75rem 1rem -0.5rem lightgrey; 19 | column-gap: 1rem; 20 | display: flex; 21 | flex-direction: row; 22 | flex-wrap: nowrap; 23 | justify-content: space-between; 24 | overflow-y: hidden; 25 | padding: 0 1rem; 26 | margin: 0.5rem 0; 27 | wrap: no-wrap; 28 | 29 | @media ${device.laptop} { 30 | padding: 0 4rem; 31 | } 32 | `; 33 | 34 | const Tab = styled.h2` 35 | cursor: pointer; 36 | font-size: 1rem; 37 | margin: 0 1rem; 38 | opacity: 0.2; 39 | padding: 1rem 0; 40 | white-space: nowrap; 41 | `; 42 | 43 | const TabBar = ({ 44 | tabs, 45 | tabIndex, 46 | setTabIndex, 47 | className, 48 | }: { 49 | tabs: Array; 50 | tabIndex: number; 51 | setTabIndex: Function; 52 | className?: string; 53 | }): ReactElement => { 54 | return ( 55 | 56 | {tabs.map((tab: string, index: number) => { 57 | const highlightedTab = {tab}; 58 | const regularTab = ( 59 | setTabIndex(index)}> 60 | {tab} 61 | 62 | ); 63 | return index === tabIndex ? highlightedTab : regularTab; 64 | })} 65 | 66 | ); 67 | }; 68 | 69 | export default TabBar; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knowyouroptions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npm run start-backend & npm run start-frontend", 9 | "install": "(cd ./frontend && npm install) & (cd ./backend && npm install)", 10 | "start-db": "cd ./database && ./create-database.sh", 11 | "start-frontend": "cd ./frontend && npm run dev", 12 | "start-backend": "cd ./backend && npm run start:dev", 13 | "clean-start": "npm run start-db && npm run start-frontend && npm run start-backend", 14 | "lint": "eslint '*/**/*.{ts,tsx}' --quiet --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/sandboxnu/knowyouroptions.git" 19 | }, 20 | "author": "", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/sandboxnu/knowyouroptions/issues" 24 | }, 25 | "homepage": "https://github.com/sandboxnu/knowyouroptions#readme", 26 | "devDependencies": { 27 | "@svgr/webpack": "^5.5.0", 28 | "@typescript-eslint/eslint-plugin": "^4.33.0", 29 | "@typescript-eslint/parser": "^4.33.0", 30 | "eslint": "^7.32.0", 31 | "eslint-config-prettier": "^8.3.0", 32 | "husky": "^4.3.8", 33 | "prettier": "^2.4.1", 34 | "pretty-quick": "^3.1.1", 35 | "tslint": "^5.20.1", 36 | "typescript": "^3.9.10" 37 | }, 38 | "dependencies": { 39 | "@nestjs/jwt": "^8.0.0", 40 | "next": "^11.1.2", 41 | "pg": "^8.7.1", 42 | "react": "^17.0.2", 43 | "react-dom": "^17.0.2", 44 | "typeorm-encrypted": "^0.6.0" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "pretty-quick --staged" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { Card as AntdCard } from 'antd'; 2 | import { ReactElement } from 'react'; 3 | import styled from 'styled-components'; 4 | import { useRouter } from 'next/router'; 5 | import SvgRightArrow from '../public/right-arrow.svg'; 6 | import { size, maxDevice } from '../templates/mediaSizes'; 7 | 8 | const StyledCard = styled(AntdCard)` 9 | background-color: white; 10 | border-radius: 0.5rem; 11 | cursor: pointer; 12 | display: flex; 13 | flex-direction: column; 14 | font-size: 0.8rem; 15 | padding: 1rem; 16 | 17 | margin-right: 3%; 18 | 19 | @media ${maxDevice.laptop} { 20 | flex-grow: 1; 21 | width: 45%; 22 | max-width: 100%; 23 | margin: 0.25rem; 24 | } 25 | 26 | @media (min-width: ${size.laptop + 1}px) { 27 | &:nth-child(3) { 28 | max-width: 35%; 29 | margin-right: 0; 30 | } 31 | 32 | max-width: 28%; 33 | padding: 2rem; 34 | } 35 | `; 36 | 37 | const Title = styled.h2` 38 | margin-top: 0.25rem; 39 | margin-bottom: 0; 40 | `; 41 | 42 | const Description = styled.p` 43 | margin: 0.5rem 0; 44 | color: gray; 45 | font-size: 1.9vh; 46 | `; 47 | 48 | const RightArrow = styled(SvgRightArrow)` 49 | align-self: flex-end; 50 | margin-top: auto; 51 | `; 52 | 53 | const Card = ({ 54 | title, 55 | description, 56 | link = '', 57 | }: { 58 | title: string; 59 | description: string; 60 | link?: string; 61 | }): ReactElement => { 62 | const router = useRouter(); 63 | const StyledCardProps = { 64 | onClick: () => router.push(link), 65 | }; 66 | return ( 67 | 75 | {title} 76 | {description} 77 | 78 | 79 | ); 80 | }; 81 | 82 | export default Card; 83 | -------------------------------------------------------------------------------- /frontend/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, ReactElement, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import SvgX from '../public/x.svg'; 4 | 5 | const Ok = styled.button` 6 | align-self: center; 7 | background-color: #911d7a; 8 | border-color: transparent; 9 | border-radius: 0.25rem; 10 | border-width: 0; 11 | color: white; 12 | cursor: pointer; 13 | display: flex; 14 | font-family: Roboto; 15 | padding: 1rem 2rem; 16 | margin-bottom: 2rem; 17 | width: fit-content; 18 | `; 19 | 20 | const ToastContainer = styled.div` 21 | background-color: white; 22 | border: 0.1rem solid #6c6c6c; 23 | border-radius: 0.5rem; 24 | bottom: 0; 25 | box-shadow: #50505026 0 0 0 20rem; 26 | display: flex; 27 | flex-direction: column; 28 | height: fit-content; 29 | justify-content: space-around; 30 | left: 0; 31 | margin: auto; 32 | padding: 1rem; 33 | position: absolute; 34 | right: 0; 35 | top: 0; 36 | transition-duration: 0.5s; 37 | width: 95%; 38 | z-index: 1; 39 | `; 40 | 41 | const ToastText = styled.p` 42 | align-self: center; 43 | color: #6c6c6c; 44 | font-family: din-2014; 45 | font-size: 1.3rem; 46 | margin-bottom: 2rem; 47 | margin-top: 3rem; 48 | text-align: center; 49 | width: 65%; 50 | `; 51 | 52 | const XIcon = styled(SvgX)` 53 | align-self: flex-end; 54 | cursor: pointer; 55 | display: flex; 56 | flex: none; 57 | justify-self: flex-start; 58 | height: 1rem; 59 | `; 60 | 61 | const Toast = ({ 62 | text, 63 | onClose, 64 | closeText = '', 65 | }: { 66 | text: string; 67 | onClose: MouseEventHandler; 68 | closeText?: string; 69 | }): ReactElement => { 70 | return ( 71 | 72 | 73 | {text} 74 | {closeText ? closeText : 'Ok'} 75 | 76 | ); 77 | }; 78 | 79 | export default Toast; 80 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/Effect.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Column, Description, Row } from './StyledComponents'; 4 | 5 | // Styles 6 | 7 | const BenefitsIconWrapper = styled.div` 8 | align-items: center; 9 | display: flex; 10 | `; 11 | 12 | const Container = styled(Column)` 13 | align-items: center; 14 | text-align: center; 15 | width: 30%; 16 | `; 17 | 18 | const Label = styled(Description)` 19 | margin-left: 1rem; 20 | `; 21 | 22 | const RowWrapped = styled(Row)` 23 | flex-wrap: wrap; 24 | justify-content: space-between; 25 | row-gap: 1rem; 26 | `; 27 | 28 | const SideEffectsIconContainer = styled.div` 29 | align-items: center; 30 | display: flex; 31 | height: 6vh; 32 | // height: 5px; 33 | `; 34 | 35 | const SideEffectsLabel = styled(Description)` 36 | margin: 0; 37 | `; 38 | 39 | // Components 40 | 41 | export interface EffectProps { 42 | benefitsInfos: [ReactElement, string][]; 43 | sideEffectsInfos: [ReactElement, string][]; 44 | } 45 | 46 | const Effect = ({ benefitsInfos, sideEffectsInfos }: EffectProps) => { 47 | return ( 48 | <> 49 |

Non-contraceptive benefits

50 | 51 | {benefitsInfos.map((infos) => { 52 | const [icon, label] = infos; 53 | return ( 54 | 55 | {icon} 56 | 57 | 58 | ); 59 | })} 60 | 61 |

Side effects

62 | 63 | {sideEffectsInfos.map((infos) => { 64 | const [icon, label] = infos; 65 | return ( 66 | 67 | {icon} 68 | {label} 69 | 70 | ); 71 | })} 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default Effect; 78 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/Overview.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { Column, Row } from './StyledComponents'; 3 | import { device } from '../../mediaSizes'; 4 | import styled from 'styled-components'; 5 | import PillRow from '../../../components/PillRow'; 6 | 7 | // Styling 8 | const CategoryRow = styled(Row)` 9 | column-gap: 2rem; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | margin: 2rem 0; 13 | row-gap: 1rem; 14 | width: 100%; 15 | `; 16 | 17 | const CategoryTitle = styled.h3` 18 | color: #bebebe; 19 | font-size: 1rem; 20 | font-weight: normal; 21 | margin: 0; 22 | `; 23 | 24 | const CategoryValue = styled.p` 25 | color: #4b4b4b; 26 | font-size: 1.5rem; 27 | font-weight: 500; 28 | margin: 0; 29 | white-space: nowrap; 30 | 31 | @media ${device.laptop} { 32 | margin: 0.5rem 0; 33 | } 34 | `; 35 | 36 | const Description = styled.p` 37 | color: #7c7c7c; 38 | 39 | @media ${device.laptop} { 40 | margin: 2rem 0rem; 41 | width: 33vw; 42 | } 43 | `; 44 | 45 | // Components 46 | const Category = ({ 47 | title, 48 | value, 49 | }: { 50 | title: string; 51 | value: string; 52 | }): ReactElement => { 53 | return ( 54 | 55 | {value} 56 | {title} 57 | 58 | ); 59 | }; 60 | 61 | export interface OverviewProps { 62 | description: string; 63 | // info: string[]<[category: string, value: string]> 64 | info: Array; 65 | pillTitles: string[]; 66 | } 67 | 68 | const Overview = ({ 69 | description, 70 | info, 71 | pillTitles, 72 | }: OverviewProps): ReactElement => { 73 | return ( 74 | <> 75 | 76 | {info.map( 77 | ([category, value]: string[]): ReactElement => ( 78 | 79 | ), 80 | )} 81 | 82 | 83 | {description} 84 | 85 | ); 86 | }; 87 | 88 | export default Overview; 89 | -------------------------------------------------------------------------------- /frontend/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import Card from '../components/Card'; 3 | import styled from 'styled-components'; 4 | import Image from 'next/image'; 5 | import homepagePic from '../public/home-image.png'; 6 | import Layout from '../components/Layout'; 7 | import { colors, size, maxDevice } from '../templates/mediaSizes'; 8 | 9 | const HomeContainer = styled.div` 10 | display: flex; 11 | flex-direction: column; 12 | padding: 2rem; 13 | background-color: ${colors.homepageBackground}; 14 | .title-below { 15 | @media (min-width: ${size.laptop + 1}px) { 16 | display: block; 17 | } 18 | } 19 | .title-above { 20 | @media ${maxDevice.laptop} { 21 | display: block; 22 | } 23 | } 24 | `; 25 | 26 | const HomeTitle = styled.h1` 27 | font-family: din-2014; 28 | display: none; 29 | `; 30 | 31 | const Row = styled.div` 32 | display: flex; 33 | flex-direction: row; 34 | justify-content: flex-start; 35 | flex-wrap: wrap; 36 | padding-top: 1rem; 37 | width: 100%; 38 | `; 39 | 40 | const ImageContainer = styled(Row)` 41 | margin: 1rem 0rem 1rem 0rem; 42 | max-width: 75vh; 43 | `; 44 | const ImageContent = styled(Image)``; 45 | 46 | const Home = (): ReactElement => { 47 | return ( 48 | 49 | 50 | Home 51 | 52 | 53 | 54 | Home 55 | 56 | 61 | 66 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default Home; 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | package-lock.json 107 | 108 | .idea/ 109 | .DS_Store 110 | -------------------------------------------------------------------------------- /frontend/api-client/index.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosInstance, Method } from 'axios'; 2 | import { plainToClass } from 'class-transformer'; 3 | import { ClassType } from 'class-transformer/ClassTransformer'; 4 | import { Redirect } from '../classes/response-classes'; 5 | 6 | // Return type of array item, if T is an array 7 | type ItemIfArray = T extends (infer I)[] ? I : T; 8 | export const API_URL = 'http://localhost:3001'; 9 | 10 | class APIClient { 11 | private axios: AxiosInstance; 12 | 13 | /** 14 | * Send HTTP and return data, optionally serialized with class-transformer (helpful for Date serialization) 15 | * @param method HTTP method 16 | * @param url URL to send req to 17 | * @param responseClass Class with class-transformer decorators to serialize response to 18 | * @param body body to send with req 19 | */ 20 | private async req( 21 | method: Method, 22 | url: string, 23 | responseClass?: ClassType>, 24 | body?: any, 25 | ): Promise; 26 | private async req( 27 | method: Method, 28 | url: string, 29 | responseClass?: ClassType, 30 | body?: any, 31 | ): Promise { 32 | const res = ( 33 | await this.axios.request({ 34 | method, 35 | url, 36 | data: body, 37 | withCredentials: true, 38 | }) 39 | ).data; 40 | return responseClass ? plainToClass(responseClass, res) : res; 41 | } 42 | 43 | signIn = { 44 | post: async (body: { 45 | email: string; 46 | password: string; 47 | }): Promise => { 48 | return this.req('POST', `${API_URL}/sign-in`, Redirect, body); 49 | }, 50 | }; 51 | 52 | signUp = { 53 | post: async (body: { 54 | email: string; 55 | password: string; 56 | name: string; 57 | }): Promise => { 58 | return this.req('POST', `${API_URL}/sign-up`, Redirect, body); 59 | }, 60 | }; 61 | 62 | user = { 63 | getName: async (): Promise => { 64 | return this.req('GET', `${API_URL}/name`); 65 | }, 66 | }; 67 | 68 | helloWorld = { 69 | get: async (): Promise => { 70 | return this.req('GET', ''); 71 | }, 72 | }; 73 | 74 | constructor(baseURL = '') { 75 | this.axios = Axios.create({ baseURL: baseURL }); 76 | } 77 | } 78 | 79 | export const API = new APIClient(API_URL); 80 | -------------------------------------------------------------------------------- /backend/src/entities/contraceptive.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | OneToMany, 6 | BaseEntity, 7 | } from 'typeorm'; 8 | import { Benefit } from './benefits.entity'; 9 | import { SideEffect } from './side-effects.entity'; 10 | import { Tag } from './tags.entity'; 11 | import { ThingToKnow } from './things-to-know.entity'; 12 | 13 | type TimeUnits = 'years' | 'months' | 'days' | 'weeks'; 14 | 15 | @Entity() 16 | export class Contraceptive extends BaseEntity { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column() 21 | name: string; 22 | 23 | @Column() 24 | usePatternLowBound: number; 25 | 26 | @Column() 27 | usePatternHighBound: number; 28 | 29 | @Column() 30 | usePatternUnits: TimeUnits; 31 | 32 | @Column() 33 | effectiveRate: number; 34 | 35 | @Column() 36 | costMin: number; 37 | 38 | @Column() 39 | costMax: number; 40 | 41 | @Column() 42 | accessibility: string; 43 | 44 | @OneToMany((type) => Tag, (tag) => tag.id) 45 | tags: Tag[]; 46 | 47 | @Column() 48 | description: string; 49 | 50 | @Column() 51 | use: string; 52 | 53 | @Column() 54 | inCaseOfProblem: string; 55 | 56 | @Column({ type: 'text', array: true, default: [] }) 57 | whenItStartsToWork: string[]; 58 | 59 | @Column() 60 | howToStop: string; 61 | 62 | @Column() 63 | howToStopMethod: string; 64 | 65 | @Column() 66 | howToStopDurationText: string; 67 | 68 | @Column() 69 | howLongUntilFertility: string; 70 | 71 | @OneToMany((type) => Benefit, (benefit) => benefit.id) 72 | benefits: Benefit[]; 73 | 74 | @OneToMany((type) => SideEffect, (sideEffect) => sideEffect.id) 75 | sideEffects: SideEffect[]; 76 | 77 | @Column() 78 | howItWorks: string; 79 | 80 | @Column() 81 | healthRisks: string; 82 | 83 | @Column({ type: 'text', array: true, default: [] }) 84 | whoCantUse: string[]; 85 | 86 | @Column({ type: 'text', array: true, default: [] }) 87 | whereToAccess: string[]; 88 | 89 | @Column() 90 | whoAdministers: string; 91 | 92 | @Column() 93 | costDescription: string; 94 | 95 | @Column() 96 | warning: string; 97 | 98 | @OneToMany((type) => ThingToKnow, (thingToKnow) => thingToKnow.id) 99 | thingsToKnow: ThingToKnow[]; 100 | } 101 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/Use.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import PillRow from '../../../components/PillRow'; 4 | import { CategoryValue, Column, Description } from './StyledComponents'; 5 | 6 | // Styling 7 | const CategoryRow = styled.div` 8 | display: flex; 9 | flex-direction: row; 10 | flex-wrap: wrap; 11 | margin-bottom: 2rem; 12 | width: 100%; 13 | `; 14 | 15 | const CategoryTitle = styled.h3` 16 | color: #808080; 17 | font-size: 1rem; 18 | font-weight: normal; 19 | margin: 0; 20 | `; 21 | 22 | const HowOftenTitle = styled.h3` 23 | margin-top: 2rem; 24 | `; 25 | 26 | // Components 27 | const Category = ({ 28 | className, 29 | title, 30 | unit, 31 | value, 32 | }: { 33 | className?: string; 34 | title: string; 35 | unit: string; 36 | value: number; 37 | }): ReactElement => { 38 | return ( 39 | 40 | {title} 41 | 42 | 43 | ); 44 | }; 45 | 46 | const CategoryLeft = styled(Category)` 47 | border-right: 2px dashed lightgray; 48 | margin-right: 2rem; 49 | padding-right: 2rem; 50 | `; 51 | 52 | export interface UseProps { 53 | careFreeFor: [number, string]; 54 | howToUseDesc: string; 55 | howToUsePills: string[]; 56 | ifMissedRoutineDesc: string; 57 | lastsUpTo: [number, string]; 58 | } 59 | 60 | const Use = ({ 61 | careFreeFor, 62 | howToUseDesc, 63 | howToUsePills, 64 | ifMissedRoutineDesc, 65 | lastsUpTo, 66 | }: UseProps): ReactElement => { 67 | return ( 68 | <> 69 |

How to use it?

70 | 71 | {howToUseDesc} 72 | How often do I have to remember it? 73 | 74 | 79 | 84 | 85 |

What if I missed once in the routine or made any mistake?

86 | {ifMissedRoutineDesc} 87 | 88 | ); 89 | }; 90 | 91 | export default Use; 92 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/StyledComponents/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React, { ReactElement } from 'react'; 3 | 4 | const CategoryValueNumber = styled.span` 5 | color: #1da3aa; 6 | font-family: din-2014; 7 | font-size: 2.5rem; 8 | font-weight: 100; 9 | margin: 0 0.5rem 0 0; 10 | `; 11 | 12 | const CategoryValueP = styled.p` 13 | font-size: 1.25rem; 14 | font-weight: bold; 15 | margin: 0; 16 | `; 17 | 18 | const CategoryValue = ({ 19 | className, 20 | unit, 21 | value, 22 | }: { 23 | className?: string; 24 | unit: string; 25 | value: string; 26 | }): ReactElement => { 27 | return ( 28 | <> 29 | 30 | {value} 31 | {unit} 32 | 33 | 34 | ); 35 | }; 36 | 37 | const Column = styled.div` 38 | display: flex; 39 | flex-direction: column; 40 | `; 41 | 42 | const Description = styled.p` 43 | color: #7c7c7c; 44 | `; 45 | 46 | const DescriptionBold = styled(Description)` 47 | color: black; 48 | font-weight: bold; 49 | `; 50 | 51 | const Highlight = styled.span` 52 | color: #1da3aa; 53 | margin: 0rem; 54 | `; 55 | 56 | const HighlightDescription = ({ 57 | className, 58 | description, 59 | }: { 60 | className?: string; 61 | description: string[]; 62 | }) => { 63 | return ( 64 | <> 65 | {description.map((phrase: string, index: number) => { 66 | return index % 2 === 0 ? ( 67 | phrase 68 | ) : ( 69 | 70 | {phrase} 71 | 72 | ); 73 | })} 74 | 75 | ); 76 | }; 77 | 78 | const List = styled.ul` 79 | padding-left: 1.25rem; 80 | 81 | li::marker { 82 | color: #1da3aa; 83 | } 84 | `; 85 | 86 | const ListItem = styled.li` 87 | color: #7c7c7c; 88 | `; 89 | 90 | const Row = styled.div` 91 | display: flex; 92 | flex-direction: row; 93 | `; 94 | 95 | const Subtitle = styled.h3` 96 | margin-bottom: 0.5rem; 97 | `; 98 | 99 | export { 100 | CategoryValue, 101 | Column, 102 | Description, 103 | DescriptionBold, 104 | Highlight, 105 | HighlightDescription, 106 | List, 107 | ListItem, 108 | Row, 109 | Subtitle, 110 | }; 111 | -------------------------------------------------------------------------------- /backend/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User } from 'src/entities/user.entity'; 3 | import { EmailIsTakenError } from 'src/error/email-taken-error'; 4 | import { IncorrectPasswordError } from 'src/error/incorrect-password-error'; 5 | import { UnknownEmailError } from 'src/error/unknown-email-error'; 6 | import { SignInInfo, UserInfo } from 'src/types/user'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Repository } from 'typeorm'; 9 | 10 | @Injectable() 11 | export class UserService { 12 | constructor( 13 | @InjectRepository(User) private readonly userRepository: Repository, 14 | ) {} 15 | 16 | /** 17 | * Creates a user in the database with the given user information 18 | * @param userInfo the information of the user to create 19 | * @return the created User entity 20 | * @throws EmailIsTakenError if the email is taken 21 | */ 22 | public async createUser(userInfo: UserInfo): Promise { 23 | const existingUser = await this.userRepository.findOne({ 24 | email: userInfo.email, 25 | }); 26 | 27 | if (existingUser) { 28 | throw new EmailIsTakenError(); 29 | } else { 30 | const user = this.userRepository.create(); 31 | user.email = userInfo.email; 32 | user.name = userInfo.name; 33 | user.password = userInfo.password; 34 | 35 | await this.userRepository.save(user); 36 | return user; 37 | } 38 | } 39 | 40 | /** 41 | * Returns the user associated with the specified sign-in info 42 | * @param info sign in information 43 | * @throws IncorrectPasswordError wrong password 44 | * @throws UnknownEmailError email not found 45 | */ 46 | public async getUser(info: SignInInfo): Promise { 47 | const existingUser = await this.userRepository.findOne({ 48 | email: info.email, 49 | }); 50 | 51 | if (existingUser) { 52 | if (info.password !== existingUser.password) { 53 | throw new IncorrectPasswordError(); 54 | } else { 55 | return existingUser; 56 | } 57 | } else { 58 | throw new UnknownEmailError(); 59 | } 60 | } 61 | 62 | /** 63 | * Returns the user associated with the given ID 64 | * @param id user ID 65 | */ 66 | public async getById(id: number): Promise { 67 | const existingUser = await this.userRepository.findOne({ 68 | id: id, 69 | }); 70 | 71 | return existingUser || undefined; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /backend/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Post, 7 | Query, 8 | Res, 9 | UnauthorizedException, 10 | } from '@nestjs/common'; 11 | import { AuthService } from './auth.service'; 12 | import { SignInInfo, UserInfo } from '../types/user'; 13 | import { Response } from 'express'; 14 | import { ConfigService } from '@nestjs/config'; 15 | 16 | @Controller() 17 | export class AuthController { 18 | private readonly domain: string; 19 | 20 | constructor( 21 | private readonly authService: AuthService, 22 | private readonly configService: ConfigService, 23 | ) { 24 | this.domain = configService.get('DOMAIN'); 25 | } 26 | 27 | @Post('/sign-in') 28 | async signIn(@Body() signInInfo: SignInInfo) { 29 | // TODO: *should* sanitize signInInfo 30 | const result = await this.authService.signIn(signInInfo); 31 | 32 | return { 33 | redirect: this.domain + `/login/entry?token=${result.accessToken}`, 34 | }; 35 | } 36 | 37 | @Post('/sign-up') 38 | async signUp(@Body() userInfo: UserInfo) { 39 | const result = await this.authService.signUp(userInfo); 40 | return { 41 | redirect: this.domain + `/login/entry?token=${result.accessToken}`, 42 | }; 43 | } 44 | 45 | // NOTE: Although the two routes below are on the backend, 46 | // they are meant to be visited by the browser so a cookie can be set 47 | @Get('/login/entry') 48 | async loginAndAttachCookie( 49 | @Res() res: Response, 50 | @Query('token') token: string, 51 | ): Promise { 52 | const isVerified = await this.authService.verifyAsync(token); 53 | 54 | if (!isVerified) { 55 | throw new UnauthorizedException(); 56 | } 57 | 58 | const payload = this.authService.decodeToken(token); 59 | 60 | if (payload === null || payload === undefined) { 61 | console.error('Decoded JWT is invalid'); 62 | throw new HttpException('JWT is invalid', 500); 63 | } 64 | 65 | await this.enter(res, payload.userId); 66 | } 67 | 68 | // Set cookie and redirect to proper page 69 | private async enter(res: Response, userId: number) { 70 | // Expires in 30 days 71 | const authToken = await this.authService.createAuthToken({ 72 | userId: userId, 73 | }); 74 | 75 | //TODO: if authToken undefined 76 | 77 | res 78 | .cookie('auth_token', authToken, { 79 | httpOnly: true, 80 | maxAge: this.authService.getTokenMaxAge(), 81 | secure: false, // true only sends cookies with requests over HTTPS 82 | }) 83 | .redirect(302, '/'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knowyouroptions", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.5.1", 25 | "@nestjs/config": "^1.0.2", 26 | "@nestjs/core": "^7.5.1", 27 | "@nestjs/jwt": "^8.0.0", 28 | "@nestjs/passport": "^8.0.1", 29 | "@nestjs/platform-express": "^7.5.1", 30 | "@nestjs/typeorm": "^7.1.5", 31 | "cookie-parser": "^1.4.6", 32 | "dotenv": "^8.6.0", 33 | "express": "^4.17.1", 34 | "passport": "^0.4.1", 35 | "passport-jwt": "^4.0.0", 36 | "passport-local": "^1.0.0", 37 | "pg": "^8.7.1", 38 | "reflect-metadata": "^0.1.13", 39 | "rimraf": "^3.0.2", 40 | "rxjs": "^6.6.3", 41 | "typeorm": "^0.2.39", 42 | "typeorm-encrypted": "^0.6.0" 43 | }, 44 | "devDependencies": { 45 | "@nestjs/cli": "^7.5.1", 46 | "@nestjs/schematics": "^7.1.3", 47 | "@nestjs/testing": "^7.5.1", 48 | "@types/express": "^4.17.13", 49 | "@types/jest": "^26.0.15", 50 | "@types/node": "^14.14.6", 51 | "@types/passport-jwt": "^3.0.6", 52 | "@types/passport-local": "^1.0.34", 53 | "@types/supertest": "^2.0.10", 54 | "@typescript-eslint/eslint-plugin": "^4.6.1", 55 | "@typescript-eslint/parser": "^4.6.1", 56 | "eslint": "^7.12.1", 57 | "eslint-config-prettier": "7.2.0", 58 | "eslint-plugin-prettier": "^3.1.4", 59 | "jest": "^26.6.3", 60 | "prettier": "^2.1.2", 61 | "supertest": "^6.0.0", 62 | "ts-jest": "^26.4.3", 63 | "ts-loader": "^8.0.8", 64 | "ts-node": "^9.0.0", 65 | "tsconfig-paths": "^3.9.0", 66 | "typescript": "^4.0.5" 67 | }, 68 | "jest": { 69 | "moduleFileExtensions": [ 70 | "js", 71 | "json", 72 | "ts" 73 | ], 74 | "rootDir": "src", 75 | "testRegex": ".*\\.spec\\.ts$", 76 | "transform": { 77 | "^.+\\.(t|j)s$": "ts-jest" 78 | }, 79 | "collectCoverageFrom": [ 80 | "**/*.(t|j)s" 81 | ], 82 | "coverageDirectory": "../coverage", 83 | "testEnvironment": "node" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/pages/welcome.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Description, 3 | DescriptionBold, 4 | } from '../templates/contraceptives/tabs/StyledComponents'; 5 | import React, { ReactElement, useEffect, useState } from 'react'; 6 | import styled from 'styled-components'; 7 | import SvgWelcomeImage from '../public/welcome.svg'; 8 | import { API } from '../api-client'; 9 | import { useRouter } from 'next/router'; 10 | 11 | const BottomContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | padding: 1rem 2rem; 15 | `; 16 | 17 | const Container = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | min-height: 100vh; 21 | `; 22 | 23 | const Hi = styled.h1` 24 | margin-bottom: 1rem; 25 | `; 26 | 27 | const ImageContainer = styled.div` 28 | align-items: center; 29 | background-color: #ffbba8; 30 | display: flex; 31 | justify-content: center; 32 | padding: 4rem 0rem; 33 | `; 34 | 35 | const Name = styled.span` 36 | color: #7e1a6a; 37 | `; 38 | 39 | const Row = styled.div` 40 | display: flex; 41 | flex-direction: row; 42 | justify-content: space-between; 43 | justify-self: flex-end; 44 | padding: 0 2rem; 45 | margin-top: auto; 46 | margin-bottom: 1.5rem; 47 | `; 48 | 49 | const Skip = styled(DescriptionBold)` 50 | cursor: pointer; 51 | font-size: 0.75rem; 52 | `; 53 | 54 | const Survey = styled.button` 55 | background-color: #7e1a6a; 56 | border-color: transparent; 57 | border-radius: 0.25rem; 58 | border-width: 0; 59 | color: white; 60 | cursor: pointer; 61 | padding: 1rem 2rem; 62 | `; 63 | 64 | const WelcomeDescription = styled(Description)` 65 | font-size: 0.75rem; 66 | margin-top: 0rem; 67 | `; 68 | 69 | const Welcome = (): ReactElement => { 70 | const router = useRouter(); 71 | const [name, setName] = useState(''); 72 | useEffect(() => { 73 | if (!name) { 74 | getName(); 75 | } 76 | }); 77 | const getName = async () => { 78 | try { 79 | const user = await API.user.getName(); 80 | setName(user); 81 | } catch (e) { 82 | // If user is not signed in (expects not-logged-in error) 83 | router.push('/signin'); 84 | } 85 | }; 86 | return ( 87 | 88 | 89 | 90 | 91 | 92 | 93 | Hi, {name} 94 | 95 | 96 | Please tell us more about yourself so we can find the best 97 | contraception for your lifestyle. 98 | 99 | 100 | 101 | router.push('/')}>Skip for now 102 | router.push('/survey')}>I'd love to 103 | 104 | 105 | ); 106 | }; 107 | 108 | export default Welcome; 109 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/tabs/Efficacy.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { 3 | CategoryValue, 4 | Column, 5 | Description, 6 | HighlightDescription, 7 | Row, 8 | } from './StyledComponents'; 9 | import styled from 'styled-components'; 10 | 11 | // Styles 12 | 13 | const AlphaList = styled.ol` 14 | color: #7c7c7c; 15 | padding-left: 1.25rem; 16 | `; 17 | 18 | const CategoryValueStyled = styled(CategoryValue)` 19 | font-size: 1rem; 20 | `; 21 | 22 | const HighlightedDescription = styled(HighlightDescription)` 23 | color: black; 24 | font-weight: bold; 25 | `; 26 | 27 | const IconContainer = styled(Row)` 28 | column-gap: 1rem; 29 | `; 30 | 31 | const StopIconDescription = styled(Description)` 32 | margin: 0.5rem 0 0 0; 33 | `; 34 | 35 | const StopIconWrapper = styled.div` 36 | align-items: center; 37 | display: flex; 38 | height: 5vh; 39 | `; 40 | 41 | // Components 42 | 43 | const StopIcons = ({ icon, label }: { icon: ReactElement; label: string }) => { 44 | return ( 45 | <> 46 | 47 | {icon} 48 | {label} 49 | 50 | 51 | ); 52 | }; 53 | 54 | export interface EfficacyProps { 55 | backToFertilityDesc: string; 56 | howToStopDesc: string; 57 | pregnancyPreventionRate: number; 58 | stopInfos: [ReactElement, string][]; 59 | whenItStartsToWorkInfos: Array; 60 | } 61 | 62 | const Efficacy = ({ 63 | backToFertilityDesc, 64 | howToStopDesc, 65 | pregnancyPreventionRate, 66 | stopInfos, 67 | whenItStartsToWorkInfos, 68 | }: EfficacyProps): ReactElement => { 69 | return ( 70 | <> 71 |

How well does it prevent pregnancy?

72 | 76 | 77 | Less than {100 - pregnancyPreventionRate} in 100 women will get pregnant 78 | during the first year on this method. 79 | 80 |

When it starts to work?

81 | 82 | {whenItStartsToWorkInfos.map((desc, index) => { 83 | let StyledLi = styled.li``; 84 | if (index === 0) { 85 | StyledLi = styled.li` 86 | margin-bottom: 1rem; 87 | `; 88 | } 89 | return ( 90 | 91 | 92 | 93 | ); 94 | })} 95 | 96 | 97 |

How can I stop it?

98 | {howToStopDesc} 99 | 100 | {stopInfos.map(([icon, label]) => { 101 | return ; 102 | })} 103 | 104 |

Getting back to fertility

105 | {backToFertilityDesc} 106 | 107 | ); 108 | }; 109 | 110 | export default Efficacy; 111 | -------------------------------------------------------------------------------- /frontend/public/quick-access.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /database/parse-form.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pandas as pd 4 | import requests 5 | import sys 6 | 7 | df = pd.read_csv('contraceptives.csv') 8 | 9 | # Drop metadata columns 10 | df.drop(['Timestamp', 'Username', 'Unit for previous question.1'], axis=1, inplace=True) 11 | 12 | # TODO: This data should be incorporated later 13 | # I believe how to stop it(How) is already in the data under howToStop 14 | 15 | 16 | # Rename columns to match entity 17 | df.rename({ 18 | "Name of Contraceptive": "name", 19 | "Description of Contraceptive": "description", 20 | "Accessibility": "accessibility", 21 | "How to use it?": "use", 22 | "Tags (separate with commas)": "tags", 23 | "Care-free for": "usePatternLowBound", 24 | "Unit for previous question": "usePatternUnits", 25 | "Lasts up to": "usePatternHighBound", 26 | "What if I missed once in the routine or made any mistake?": "inCaseOfProblem", 27 | "% Effectiveness": "effectiveRate", 28 | "When it starts to work?": "whenItStartsToWork", 29 | "How Can I stop it?(When)": "howToStop", 30 | "How Can I stop it(How)": "howToStopMethod", 31 | 'How Can I Stop it(How Long)':"howToStopDurationText", 32 | "Getting Back to Fertility ": "howLongUntilFertility", 33 | "Non-contraceptive benefits": "benefits", 34 | "Side effects": "sideEffects", 35 | "How it works?": "howItWorks", 36 | "Health Risk": "healthRisks", 37 | "Where to access": "whereToAccess", 38 | "Who will administer this method?": "whoAdministers", 39 | "Cost (lower bound in $)": "costMin", 40 | "Cost (upper bound in $)": "costMax", 41 | "This form of birth control may not be suitable if you": "whoCantUse", 42 | "Additional Cost Information": "costDescription", 43 | "Things to notice about this method": "thingsToKnow", 44 | "Warning":"warning", 45 | }, axis=1, inplace=True) 46 | 47 | def split_to_dict(string: str, key_label: str, sep: str=','): 48 | listed = string.split(sep) 49 | mapped = map(lambda s : {"id": None, key_label: s}, listed) 50 | return list(mapped) 51 | 52 | def split_things_to_know(string: str): 53 | 54 | # Get the 'things' 55 | things = string.split("\n") 56 | 57 | def thing_to_dict(thing: str): 58 | # Separate the title and description 59 | separated = thing.split(":") 60 | if len(separated) != 2: 61 | raise Exception("Improperly formatted things to know") 62 | return {"id": None, "title": separated[0], "description": separated[1]} 63 | 64 | mapped = map(thing_to_dict, things) 65 | return list(mapped) 66 | 67 | dict = df.to_dict("records") 68 | for contraceptive in dict: 69 | # Formatting 70 | 71 | 72 | contraceptive["whereToAccess"] = contraceptive["whereToAccess"].split("\n") 73 | contraceptive["whenItStartsToWork"] = contraceptive["whenItStartsToWork"].split("\n") 74 | contraceptive["sideEffects"] = split_to_dict(contraceptive["sideEffects"], "description", sep="\n") 75 | contraceptive["benefits"] = split_to_dict(contraceptive["benefits"], "description", sep="\n") 76 | contraceptive["tags"] = split_to_dict(contraceptive["tags"], "label", sep=",") 77 | contraceptive["whoCantUse"] = contraceptive["whoCantUse"].split("\n") 78 | contraceptive["thingsToKnow"] = split_things_to_know(contraceptive["thingsToKnow"]) 79 | 80 | # POST 81 | args = sys.argv 82 | database_url = "http://localhost:3001/contraceptive" 83 | r = requests.post(database_url, data=contraceptive) 84 | print(str(r.status_code) + ": " + contraceptive["name"]) 85 | 86 | -------------------------------------------------------------------------------- /frontend/public/questionnaire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/public/take-questionnaire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /database/contraceptives.csv: -------------------------------------------------------------------------------- 1 | "Timestamp","Username","Name of Contraceptive","Description of Contraceptive","Accessibility","How to use it?","Tags (separate with commas)","Care-free for","Unit for previous question","Lasts up to","Unit for previous question","What if I missed once in the routine or made any mistake?","% Effectiveness","When it starts to work?","How Can I stop it?(When)","How Can I stop it(How)","How Can I Stop it(How Long)","Getting Back to Fertility ","Non-contraceptive benefits","Side effects","How it works?","Health Risk","This form of birth control may not be suitable if you","Warning","Where to access","Who will administer this method?","Cost (lower bound in $)","Cost (upper bound in $)","Additional Cost Information","Things to notice about this method" 2 | "2022/03/01 8:01:06 AM EST","heyman.b@husky.neu.edu","implant","The implant is a tiny, flexible rod (the size of a matchstick) that is inserted under the skin of your upper arm to prevent pregnancy. It is a long-acting hormonal methods.","Operation by doctor","It is inserted by a doctor or nurse under the skin of your upper arm. Once it’s in, you can’t feel it unless you try to find it with your fingers.","Use of hormones, Scalpel included","3","Years","5","Years","Talk with your doctor first and try to avoid having sex or use another contraceptive method until you confirm remedial actions with your doctor.","99","A. If the implant is fitted during the first 5 days of your menstrual cycle you'll be immediately protected against becoming pregnant. 3 | B. If it's fitted on any other day of your menstrual cycle, you'll need to use additional contraceptives (such as condoms) for the first week.","The implant can be removed at any time by a trained doctor or nurse.","A trained doctor or nurses will make a tiny cut in your skin to gently pull the implant out.","The process only takes a few minutes to remove, and a local anesthetic will be used.","Once the implant is removed your ability to get pregnant quickly returns.","It doesn't interrupting sex. 4 | Safe with breastfeeding 5 | Your fertility will return to normal as soon as the implant is taken out.","Headache 6 | Breast tenderness 7 | Acne 8 | Spotting (in the first 6-12 months) 9 | Lighter to no period after a while 10 | Mood swing / Depression","The implant releases the hormone progestogen into your bloodstream, which prevents the release of an egg each month (ovulation) to prevent pregnancy.","Serious problems with Nexplanon are rare, but they include arm pain that lasts for longer than a few days, an infection in the arm that needs medicine, or a scar on your arm where the implant goes.","can't use an Estrogen-based method 11 | have arterial disease or a history of a heart disease or stroke 12 | have liver disease 13 | have breast cancer or have had it in the past 14 | have unexplained bleeding in between periods or after sex","Tell your doctor or nurse if you have any unexpected symptoms while using Nexplanon.","Contraception clinics 15 | Sexual health clinics 16 | GP surgeries","Put in by doctor or nurse.","0","1300","Price may vary from geographic regions and health insurers. But the good news is that implants are totally free (or low cost) with most health insurance plans, Medicaid, and some other government programs.","Needle phobia: Needles will be included in the inserting process. If you don't feel comfortable with that, please inform your doctor in advance. 17 | Is it compatible with your religious beliefs or cultural practices?: Some forms of birth control are considered a violation of certain religious rights or cultural traditions. Weigh the risks and benefits of a birth control method against your personal convictions." -------------------------------------------------------------------------------- /backend/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { 4 | AuthenticatedUser, 5 | SignInInfo, 6 | UserAuthPayload, 7 | UserInfo, 8 | } from 'src/types/user'; 9 | import { UserService } from 'src/user/user.service'; 10 | import { User } from '../entities/user.entity'; 11 | import { ConfigService } from '@nestjs/config'; 12 | 13 | @Injectable() 14 | export class AuthService { 15 | private readonly maxAge: number; 16 | constructor( 17 | private readonly usersService: UserService, 18 | private jwtService: JwtService, 19 | private configService: ConfigService, 20 | ) { 21 | this.maxAge = this.configService.get('JWT_EXPIRATION'); 22 | } 23 | 24 | /** 25 | * 26 | * @param info 27 | */ 28 | public async signIn(info: SignInInfo): Promise { 29 | const user = await this.usersService.getUser(info); 30 | 31 | // Create temporary auth token, so we can route them to /login/ 32 | const token = await this.createAuthToken({ userId: user.id }, 60); 33 | 34 | return { 35 | id: user.id, 36 | email: user.email, 37 | name: user.name, 38 | accessToken: token, 39 | }; 40 | } 41 | 42 | public async signUp(userInfo: UserInfo): Promise { 43 | this.verifyPasswordStrength(userInfo.password); 44 | 45 | const user = await this.usersService.createUser(userInfo); 46 | 47 | // Create temporary auth token, so we can route them to /login/ 48 | const token = await this.createAuthToken({ userId: user.id }, 60); 49 | 50 | return { 51 | id: user.id, 52 | email: user.email, 53 | name: user.name, 54 | accessToken: token, 55 | }; 56 | } 57 | 58 | /** 59 | * Creates an auth token based on the given payload 60 | * If no expiry time is given, tokens expire after the maxAge property in this service 61 | * Otherwise, tokens expire after the specified number of seconds 62 | * @param payload payload being turned into auth token 63 | * @param expiresIn expiry time in seconds 64 | */ 65 | public async createAuthToken(payload: UserAuthPayload, expiresIn?: number) { 66 | const token = await this.jwtService.signAsync(payload, { 67 | expiresIn: expiresIn ? expiresIn : this.maxAge, 68 | }); 69 | 70 | if (token === null || token === undefined) { 71 | throw new HttpException('Invalid JWT Token', 500); 72 | } 73 | 74 | return token; 75 | } 76 | 77 | /** 78 | * Returns the payload decoded from the given auth token. 79 | * Does not verify signature (not secure) 80 | * 81 | * @param authToken User Authorization Token 82 | */ 83 | public decodeToken(authToken: string): UserAuthPayload { 84 | return this.jwtService.decode(authToken) as UserAuthPayload; 85 | } 86 | 87 | /** 88 | * Returns the payload decoded from the given auth token. 89 | * Verifies the signature (secure) 90 | * 91 | * @param authToken User Authorization Token 92 | */ 93 | public async verifyAsync(authToken: string): Promise { 94 | return this.jwtService.verifyAsync(authToken); 95 | } 96 | 97 | /** 98 | * TODO: Implement this 99 | * @param password 100 | */ 101 | private verifyPasswordStrength(password: string) { 102 | return undefined; 103 | } 104 | 105 | /** 106 | * Returns the max age of an auth token in seconds. 107 | */ 108 | public getTokenMaxAge(): number { 109 | return this.maxAge; 110 | } 111 | 112 | /** 113 | * Gets the user that was specified in the JWT Payload, based on their ID 114 | * @param payload JWT Payload including user ID 115 | */ 116 | async validateUser(payload: UserAuthPayload): Promise { 117 | const user = await this.usersService.getById(payload.userId); 118 | if (!user) { 119 | throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); 120 | } 121 | return user; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /frontend/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import SvgBookmarkIcon from '../public/desktop-icons/desktop-bookmark.svg'; 4 | import SvgSettingsIcon from '../public/desktop-icons/settings.svg'; 5 | import SvgSearchIcon from '../public/desktop-icons/search.svg'; 6 | import SvgProfileIcon from '../public/desktop-icons/profile.svg'; 7 | import { Menu } from 'antd'; 8 | import { colors } from '../templates/mediaSizes'; 9 | import Link from 'next/link'; 10 | 11 | class QuickLink { 12 | title: string; 13 | url: string; 14 | 15 | constructor(title: string, url: string) { 16 | this.title = title; 17 | this.url = url; 18 | } 19 | } 20 | 21 | const quickLinks = [ 22 | new QuickLink('Cervical Cap', 'https://www.google.com'), 23 | new QuickLink('Condom', 'https://www.google.com'), 24 | new QuickLink('Copper IUD', 'https://www.google.com'), 25 | new QuickLink('Diaphragm', 'https://www.google.com'), 26 | new QuickLink('Hormonal IUD', 'https://www.google.com'), 27 | new QuickLink('Implant', '/implant'), 28 | new QuickLink('Patch', 'https://www.google.com'), 29 | new QuickLink('Pill', 'https://www.google.com'), 30 | new QuickLink('Ring', 'https://www.google.com'), 31 | new QuickLink('Shot', 'https://www.google.com'), 32 | new QuickLink('Spermicide', 'https://www.google.com'), 33 | new QuickLink('Sterilization', 'https://www.google.com'), 34 | ]; 35 | 36 | const DropdownColumns = styled.ul` 37 | position: absolute; 38 | background-color: white; 39 | margin-left: -75px; 40 | padding: 25px; 41 | margin-top: 32px; 42 | columns: 2; 43 | column-gap: 25px; 44 | border-top-style: solid; 45 | border-top-width: 1px; 46 | border-top-color: ${colors.homepageNavBarDropdown}; 47 | z-index: 5; 48 | `; 49 | 50 | const StyledMenu = styled(Menu)` 51 | padding: 50px; 52 | `; 53 | 54 | const MenuItem = styled(Menu.Item)` 55 | list-style: none; 56 | 57 | a:hover { 58 | color: ${colors.homepageNavBarDropdown}; 59 | text-decoration: underline; 60 | } 61 | `; 62 | 63 | const ArrowDropdown = styled.div` 64 | width: 0; 65 | height: 0; 66 | border-left: 5px solid transparent; 67 | border-right: 5px solid transparent; 68 | border-bottom: 5px solid ${colors.homepageNavBarDropdown}; 69 | position: absolute; 70 | margin-top: 27px; 71 | margin-left: 40px; 72 | `; 73 | 74 | const MenuHeading = ({ 75 | title, 76 | links = [], 77 | }: { 78 | title: string; 79 | links?: QuickLink[]; 80 | }): ReactElement => { 81 | const [dropdown, setDropdown] = useState(false); 82 | 83 | const linkToItem = (link: QuickLink) => { 84 | return ( 85 | 86 | 87 | {link.title} 88 | 89 | 90 | ); 91 | }; 92 | 93 | return ( 94 | setDropdown(true)} 96 | onMouseLeave={() => setDropdown(false)} 97 | > 98 | {title} 99 | {dropdown && title == 'Quick Access' ? ( 100 |
101 | 102 | 103 | {links?.map((link: QuickLink) => linkToItem(link))} 104 | 105 |
106 | ) : null} 107 |
108 | ); 109 | }; 110 | 111 | const MenuElements = styled.div` 112 | display: flex; 113 | width: 80%; 114 | justify-content: space-evenly; 115 | `; 116 | 117 | const NavMenu = styled.div` 118 | height: 85px; 119 | display: flex; 120 | flex-wrap: no-wrap; 121 | justify-content: space-between; 122 | align-items: center; 123 | background: white; 124 | z-index: 3; 125 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 126 | `; 127 | 128 | const MenuIcons = styled.div` 129 | width: 20%; 130 | margin-right: 30px; 131 | display: flex; 132 | align-items: flex-end; 133 | flex-wrap: no-wrap; 134 | justify-content: space-between; 135 | `; 136 | 137 | const ICON_HEIGHT = '30px'; 138 | 139 | const SearchIcon = styled(SvgSearchIcon)` 140 | height: ${ICON_HEIGHT}; 141 | width: auto; 142 | `; 143 | 144 | const BookmarkIcon = styled(SvgBookmarkIcon)` 145 | height: ${ICON_HEIGHT}; 146 | width: auto; 147 | `; 148 | 149 | const SettingsIcon = styled(SvgSettingsIcon)` 150 | height: ${ICON_HEIGHT}; 151 | width: auto; 152 | `; 153 | 154 | const ProfileIcon = styled(SvgProfileIcon)` 155 | height: ${ICON_HEIGHT}; 156 | width: auto; 157 | `; 158 | 159 | const Logo = styled(SvgSettingsIcon)` 160 | height: ${ICON_HEIGHT}; 161 | width: auto; 162 | margin-left: 30px; 163 | `; 164 | 165 | const NavBar = (): ReactElement => { 166 | return ( 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ); 182 | }; 183 | 184 | export default NavBar; 185 | -------------------------------------------------------------------------------- /frontend/public/welcome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/templates/contraceptives/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect, useState } from 'react'; 2 | import Efficacy, { EfficacyProps } from './tabs/Efficacy'; 3 | import Effect, { EffectProps } from './tabs/Effect'; 4 | import Overview, { OverviewProps } from './tabs/Overview'; 5 | import Use, { UseProps } from './tabs/Use'; 6 | import TabBar from '../TabBar'; 7 | import Mechanism, { MechanismProps } from './tabs/Mechanism'; 8 | import PracticalQuestions, { PracticalProps } from './tabs/PracticalQuestions'; 9 | import AdditionalInfo, { AdditionalProps } from './tabs/AdditionalInfo'; 10 | import styled from 'styled-components'; 11 | import DownOutlined from '@ant-design/icons/DownOutlined'; 12 | import { Column, Row } from './tabs/StyledComponents'; 13 | import SvgBookmark from '../../public/bookmark.svg'; 14 | import SvgDesktopBookmark from '../../public/desktop-icons/desktop-bookmark.svg'; 15 | import SvgDesktopDropdown from '../../public/desktop-icons/desktop-dropdown.svg'; 16 | import { size, device, maxDevice } from '../mediaSizes'; 17 | import Pill from '../../components/Pill'; 18 | 19 | // styled 20 | const Container = styled.div` 21 | padding: 1rem; 22 | `; 23 | 24 | const Body = styled(Container)` 25 | @media ${device.laptop} { 26 | padding: 1rem 3rem; 27 | width: 65vw; 28 | } 29 | `; 30 | 31 | const Bookmark = styled(SvgBookmark)` 32 | fill: white; 33 | position: relative; 34 | stroke: black; 35 | top: 2px; 36 | @media ${device.laptop} { 37 | display: none; 38 | } 39 | `; 40 | 41 | const BookmarkDesktop = styled(SvgBookmark)` 42 | fill: white; 43 | position: relative; 44 | stroke: black; 45 | top: 2px; 46 | @media max-width:${size.mobileL} { 47 | display: none; 48 | } 49 | `; 50 | 51 | const DownArrow = styled(DownOutlined)` 52 | position: absolute; 53 | bottom: 1rem; 54 | left: 50%; 55 | @media ${device.laptop} { 56 | display: absolute; 57 | } 58 | `; 59 | 60 | const Header = styled(Container)` 61 | background-color: #febba8; 62 | display: flex; 63 | flex-direction: row; 64 | 65 | @media ${device.laptop} { 66 | height: 57vh; 67 | position: relative; 68 | } 69 | `; 70 | 71 | const SvgCircle = styled.div` 72 | background-color: white; 73 | border-radius: 50%; 74 | padding: 0.7rem 0.9rem 0.6rem 0.9rem; 75 | 76 | :hover { 77 | cursor: pointer; 78 | 79 | ${Bookmark} { 80 | fill: purple; 81 | } 82 | } 83 | @media ${device.laptop} { 84 | display: none; 85 | } 86 | `; 87 | 88 | const SvgCircleSecond = styled(SvgCircle)``; 89 | 90 | const SvgColumn = styled(Column)` 91 | margin-left: auto; 92 | 93 | ${SvgCircleSecond} { 94 | margin-top: 0.5rem; 95 | } 96 | `; 97 | 98 | const SvgDesktopColumn = styled(Column)` 99 | margin-left: 0.5rem; 100 | justify: left; 101 | 102 | ${SvgCircleSecond} { 103 | margin-top: 0.5rem; 104 | } 105 | 106 | @media ${device.laptop} { 107 | bottom: 0; 108 | justify: left; 109 | position: absolute; 110 | } 111 | `; 112 | 113 | const SvgRow = styled(Row)` 114 | margin-left: auto; 115 | margin-top: auto; 116 | 117 | ${SvgCircleSecond} { 118 | margin-left: 0.5rem; 119 | } 120 | `; 121 | 122 | const SvgDesktopRow = styled(Row)` 123 | display: inline; 124 | @media ${maxDevice.laptop} { 125 | display: none; 126 | } 127 | `; 128 | 129 | const Title = styled.h1` 130 | margin: 0; 131 | 132 | @media ${device.laptop} { 133 | display: none; 134 | } 135 | `; 136 | 137 | const TitleDesktop = styled.h1` 138 | display: inline; 139 | font-size: 40px; 140 | margin: 0; 141 | padding: 0.5rem; 142 | 143 | @media ${maxDevice.laptop} { 144 | display: none; 145 | } 146 | `; 147 | 148 | const PillDesktop = styled(Pill)` 149 | background: #fffefe; 150 | display: inline; 151 | margin: 1rem; 152 | padding: 0.2rem 0.3rem; 153 | `; 154 | 155 | const QuickAccess = styled.div` 156 | @media ${device.laptop} { 157 | display: flex; 158 | flex-direction: row; 159 | padding: 1.5rem 1.5rem; 160 | } 161 | `; 162 | 163 | const SvgBookmarkDesktopStyled = styled(SvgDesktopBookmark)` 164 | margin: 0.7rem 0rem; 165 | `; 166 | 167 | // components 168 | export interface ContraceptivesProps { 169 | SvgContraceptive: ReactElement; 170 | effectProps: EffectProps; 171 | efficacyProps: EfficacyProps; 172 | title: string; 173 | overviewProps: OverviewProps; 174 | mechanismProps: MechanismProps; 175 | practicalProps: PracticalProps; 176 | useProps: UseProps; 177 | additionalProps: AdditionalProps; 178 | } 179 | 180 | const Contraceptives = ({ 181 | SvgContraceptive, 182 | title, 183 | effectProps, 184 | efficacyProps, 185 | overviewProps, 186 | useProps, 187 | mechanismProps, 188 | practicalProps, 189 | additionalProps, 190 | }: ContraceptivesProps): ReactElement => { 191 | const [tabIndex, setTabIndex] = useState(0); 192 | 193 | const tabs = [ 194 | 'Overview', 195 | 'Use', 196 | 'Efficacy', 197 | 'Effect', 198 | 'Mechanism', 199 | 'Practical Questions', 200 | 'Additional', 201 | ]; 202 | const tabComponents = [ 203 | , 204 | , 205 | , 206 | , 207 | , 208 | , 209 | , 210 | ]; 211 | const BookmarkIcon = ( 212 | 213 | 214 | 215 | ); 216 | const CompareMethodsIcon = ( 217 | 218 | 219 | 220 | ); 221 | return ( 222 | <> 223 |
224 | 225 |
226 | {tabIndex === 0 && SvgContraceptive} 227 | {title} 228 |
229 | 230 | 231 | 232 | {title} 233 | 234 | 235 | {'Compare Methods'} 236 | 237 | 238 | 239 |
240 | 241 | {tabIndex === 0 ? ( 242 | 243 | {BookmarkIcon} 244 | {CompareMethodsIcon} 245 | 246 | ) : ( 247 | 248 | {BookmarkIcon} 249 | {CompareMethodsIcon} 250 | 251 | )} 252 |
253 | 254 | 255 | {tabComponents[tabIndex]} 256 | 257 | ); 258 | }; 259 | 260 | export default Contraceptives; 261 | -------------------------------------------------------------------------------- /frontend/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { ReactElement, useMemo, useState } from 'react'; 3 | import { 4 | MenuOutlined, 5 | UserOutlined, 6 | QuestionCircleOutlined, 7 | BarChartOutlined, 8 | } from '@ant-design/icons'; 9 | import styled from 'styled-components'; 10 | import slideStyle from './slide.module.scss'; 11 | import SvgBookmarkIcon from '../public/bookmark-nav-bar.svg'; 12 | import SvgMenuIcon from '../public/menu.svg'; 13 | import SvgQuickAccessButton from '../public/quick-access.svg'; 14 | import SvgTakeQuestionnaire from '../public/take-questionnaire.svg'; 15 | 16 | const MenuHeading = styled.h1` 17 | color: #911d7a; 18 | font-family: 'din-2014'; 19 | font-size: 1rem; 20 | font-weight: bold; 21 | 22 | > * { 23 | margin-right: 10px; 24 | } 25 | `; 26 | 27 | const MenuElements = styled.div` 28 | margin-left: 10px; 29 | :hover { 30 | color: #911d7a; 31 | } 32 | & > div { 33 | & > a { 34 | color: #404040; 35 | font-family: 'roboto'; 36 | font-size: 0.8rem; 37 | } 38 | } 39 | 40 | > * { 41 | margin-left: 15px; 42 | } 43 | `; 44 | 45 | const SidebarDiv = styled.div` 46 | position: absolute; 47 | x: 0; 48 | y: 0; 49 | width: 70vw; 50 | height: 100vh; 51 | z-index: 10; 52 | `; 53 | 54 | const MenuSection = styled.div` 55 | border-bottom: 1.5px solid #d6d6d6; 56 | padding: 1rem 0rem; 57 | & > div { 58 | display: flex; 59 | flex-direction: column; 60 | row-gap: 0.5rem; 61 | } 62 | `; 63 | 64 | const TwoColumns = styled.div` 65 | display: flex; 66 | margin-left: 10px; 67 | 68 | > * { 69 | margin-left: 20px; 70 | } 71 | `; 72 | const ColumnItem = styled.div` 73 | width: 50%; 74 | :hover { 75 | color: #800080; 76 | font-weight: bold; 77 | } 78 | & > a { 79 | font-family: 'roboto'; 80 | font-size: 0.8rem; 81 | } 82 | `; 83 | 84 | const Menu = styled.div` 85 | border: 2px solid #d6d6d6; 86 | border-left: none; 87 | padding: 3rem 1rem; 88 | width: 75vw; 89 | height: 100vh; 90 | display: flex; 91 | flex-direction: column; 92 | background: white; 93 | position: absolute; 94 | z-index: 0; 95 | `; 96 | 97 | const Sidebar = (): ReactElement => { 98 | const [open, setOpen] = useState(false); 99 | const [closing, setClosing] = useState(false); 100 | 101 | const animation = open 102 | ? slideStyle.slide 103 | : closing 104 | ? slideStyle.close 105 | : undefined; 106 | console.log(animation); 107 | 108 | const closeMenu = () => { 109 | setOpen(false); 110 | setClosing(true); 111 | return setTimeout(() => { 112 | setClosing(false); 113 | }, 500); 114 | }; 115 | 116 | return ( 117 | 118 | {open || closing ? ( 119 | 120 | 126 | 127 |
128 |
129 | 130 | 131 | Profile 132 | 133 | 134 |
135 | Survey Report 136 |
137 |
138 |
139 |
140 | 141 | 142 | Bookmarks 143 | 144 | 145 |
146 | My method list 147 |
148 |
149 | Saved Posts 150 |
151 |
152 | Saved Topics 153 |
154 |
155 |
156 |
157 | 158 | 159 | Take Questionnaire 160 | 161 |
162 |
163 | 164 | Q&A 165 | 166 |
167 |
168 |
169 | 170 |
171 | 172 | 173 | Quick Access 174 | 175 | 176 | 177 | Sterilization 178 | 179 | 180 | Implant 181 | 182 | 183 | 184 | 185 | Copper IUD 186 | 187 | 188 | Hormonal IUD 189 | 190 | 191 | 192 | 193 | Shot 194 | 195 | 196 | Ring 197 | 198 | 199 | 200 | 201 | Patch 202 | 203 | 204 | Condom 205 | 206 | 207 | 208 | 209 | Spermicide 210 | 211 | 212 | Diaphram 213 | 214 | 215 | 216 | 217 | Pill 218 | 219 | 220 | Cervical Cap 221 | 222 | 223 |
224 |
225 |
226 | Settings and privacy 227 |
228 |
229 | ) : ( 230 | setOpen(true)} 233 | style={{ width: '30px', marginLeft: '30px', marginTop: '30px' }} 234 | ghost 235 | /> 236 | )} 237 |
238 | ); 239 | }; 240 | 241 | const Bookmark = (): ReactElement => { 242 | return ( 243 | <> 244 | 245 | 246 | ); 247 | }; 248 | 249 | export default Sidebar; 250 | -------------------------------------------------------------------------------- /frontend/pages/implant.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | import SvgAcne from '../public/acne.svg'; 4 | import SvgBed from '../public/bed.svg'; 5 | import SvgBreastFeeding from '../public/breastfeeding.svg'; 6 | import SvgBreastTenderness from '../public/breast-tenderness.svg'; 7 | import SvgCalendar from '../public/calendar.svg'; 8 | import SvgDepressed from '../public/depressed.svg'; 9 | import SvgDoctor from '../public/doctor.svg'; 10 | import SvgHeadache from '../public/headache.svg'; 11 | import SvgImplant from '../public/implant.svg'; 12 | import SvgImplantRemoval from '../public/implant-removal.svg'; 13 | import SvgPad from '../public/pad.svg'; 14 | import SvgTime from '../public/time.svg'; 15 | 16 | import ContraceptiveTemplate from '../templates/contraceptives'; 17 | import { EffectProps } from '../templates/contraceptives/tabs/Effect'; 18 | import { EfficacyProps } from '../templates/contraceptives/tabs/Efficacy'; 19 | import { OverviewProps } from '../templates/contraceptives/tabs/Overview'; 20 | import { UseProps } from '../templates/contraceptives/tabs/Use'; 21 | import { MechanismProps } from '../templates/contraceptives/tabs/Mechanism'; 22 | import { PracticalProps } from '../templates/contraceptives/tabs/PracticalQuestions'; 23 | import { AdditionalProps } from '../templates/contraceptives/tabs/AdditionalInfo'; 24 | 25 | const Implant = (): ReactElement => { 26 | const implantDesc = 27 | 'The implant is a tiny, flexible rod (the size of a matchstick) that is inserted under the skin of your upper arm to prevent pregnancy. It is a long-acting hormonal methods.'; 28 | const implantInfo = [ 29 | ['Effective', '99%'], 30 | ['Use Pattern', '3-5 years'], 31 | ['Cost', '$ 0-1.3k'], 32 | ['Accessibility', 'Operation by doctor'], 33 | ]; 34 | const pillTitles = ['Use of hormones', 'Scalpel included']; 35 | const overviewProps: OverviewProps = { 36 | description: implantDesc, 37 | info: implantInfo, 38 | pillTitles: pillTitles, 39 | }; 40 | 41 | // Use Props 42 | const careFreeFor: [number, string] = [3, 'years']; 43 | const howToUseDesc = 44 | 'It is inserted by a doctor or nurse under the skin of your upper arm. Once it’s in, you can’t feel it unless you try to find it with your fingers.'; 45 | const howToUsePills = ['Use of hormones', 'Scalpel included']; 46 | const ifMissedRoutineDesc = 47 | 'Talk with your doctor first and try to avoid having sex or use another contraceptive method until you confirm remedial actions with your doctor.'; 48 | const lastsUpTo: [number, string] = [5, 'years']; 49 | 50 | const useProps: UseProps = { 51 | careFreeFor: careFreeFor, 52 | howToUseDesc: howToUseDesc, 53 | howToUsePills: howToUsePills, 54 | ifMissedRoutineDesc: ifMissedRoutineDesc, 55 | lastsUpTo: lastsUpTo, 56 | }; 57 | 58 | // Efficacy props 59 | const backToFertilityDesc = 60 | 'Once the implant is removed your ability to get pregnant quickly returns.'; 61 | const howToStopDesc = 62 | 'The implant can be removed at any time by a trained doctor or nurse.'; 63 | const pregnancyPreventionRate = 99; 64 | const stopInfos: [ReactElement, string][] = [ 65 | [ 66 | , 67 | 'A trained doctor or nurse will make a tiny cut in your skin to gently pull the implant out.', 68 | ], 69 | [ 70 | , 71 | 'The process only takes a few minutes to remove, and a local anaesthetic will be used.', 72 | ], 73 | ]; 74 | const whenItStartsToWorkInfos: Array = [ 75 | [ 76 | 'If the implant is fitted during ', 77 | 'the first 5 days of your menstrual cycle', 78 | ', you’ll be immediately protected against becoming pregnant;', 79 | ], 80 | [ 81 | 'If it’s fitted on ', 82 | 'any other day of your menstrual cycle', 83 | ', you’ll need to use additional contraception (such as condoms) for the first week.', 84 | ], 85 | ]; 86 | 87 | const efficacyProps: EfficacyProps = { 88 | backToFertilityDesc: backToFertilityDesc, 89 | howToStopDesc: howToStopDesc, 90 | pregnancyPreventionRate: pregnancyPreventionRate, 91 | stopInfos: stopInfos, 92 | whenItStartsToWorkInfos: whenItStartsToWorkInfos, 93 | }; 94 | 95 | // Effect props 96 | const benefitsInfos: [ReactElement, string][] = [ 97 | [, 'It doesn’t interrupting sex.'], 98 | [, 'Safe with breastfeeding'], 99 | [ 100 | , 101 | 'Your fertility will return to normal as soon as the implant is taken out.', 102 | ], 103 | ]; 104 | const sideEffectsInfos: [ReactElement, string][] = [ 105 | [, 'Headache'], 106 | [, 'Breast tenderness'], 107 | [, 'Acne'], 108 | [, 'Spotting\n' + '(in the first 6–12 months)'], 109 | [, 'Lighter to no period after a while'], 110 | [, 'Mood swing /\n' + 'Depression'], 111 | ]; 112 | 113 | const effectProps: EffectProps = { 114 | benefitsInfos: benefitsInfos, 115 | sideEffectsInfos: sideEffectsInfos, 116 | }; 117 | 118 | const mechanism = 119 | 'The implant releases the hormone progestogen into your bloodstream, which prevents the release of an egg each month (ovulation) to prevent pregnancy.'; 120 | const healthRisk = [ 121 | 'Serious problems with Nexplanon are rare, but they include ', 122 | 'arm pain', 123 | ' that lasts for longer than a few days, ', 124 | 'an infection', 125 | ' in the arm that needs medicine, or ', 126 | 'a scar', 127 | ' on your arm where the implant goes.', 128 | ]; 129 | const warning = 130 | '* Tell your doctor or nurse if you have any unexpected symptoms while using Nexplanon.'; 131 | const cantUse = [ 132 | 'Most women can be fitted with the contraceptive implant. It may not be suitable if you:', 133 | "can't use an Estrogen-based method", 134 | 'have arterial disease or a history of a heart disease or stroke', 135 | 'have liver disease', 136 | 'have breast cancer or have had it in the past', 137 | 'have unexplained bleeding in between periods or after sex', 138 | ]; 139 | const mechanismProps: MechanismProps = { 140 | mechanism: mechanism, 141 | healthRisk: healthRisk, 142 | whoCantUse: cantUse, 143 | warning: warning, 144 | }; 145 | const location = [ 146 | 'You can get the contraceptive implant from:', 147 | 'Contraception clinics', 148 | 'Sexual health clinics', 149 | 'GP surgeries', 150 | ]; 151 | const administration = 'Put in by doctor or nurse.'; 152 | const [loCost, hiCost] = [0, 1300]; 153 | const costInfo = 154 | 'Price may vary from geographic regions and health insurers. But the good news is that implants are totally free (or low cost) with most health insurance plans, Medicaid, and some other government programs.'; 155 | const practicalProps: PracticalProps = { 156 | access: location, 157 | administration: administration, 158 | lowPrice: loCost, 159 | highPrice: hiCost, 160 | costInfo: costInfo, 161 | }; 162 | const additionalInfo = [ 163 | [ 164 | 'Needle phobia', 165 | "Needles will be included in the inserting process. If you don't feel comfortable with that, please inform your doctor in advance.", 166 | ], 167 | [ 168 | 'Is it compatible with your religious beliefs or cultural practices?', 169 | 'Some forms of birth control are considered a violation of certain religious rights or cultural traditions. Weigh the risks and benefits of a birth control method against your personal convictions.', 170 | ], 171 | ]; 172 | const additionalProps: AdditionalProps = { 173 | info: additionalInfo, 174 | }; 175 | return ( 176 | } 178 | effectProps={effectProps} 179 | efficacyProps={efficacyProps} 180 | title={'Implant'} 181 | overviewProps={overviewProps} 182 | useProps={useProps} 183 | mechanismProps={mechanismProps} 184 | practicalProps={practicalProps} 185 | additionalProps={additionalProps} 186 | /> 187 | ); 188 | }; 189 | 190 | export default Implant; 191 | -------------------------------------------------------------------------------- /frontend/public/depressed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/pages/signin.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React, { ReactElement, useState } from 'react'; 3 | import TabBar from '../templates/TabBar'; 4 | import SvgEye from '../public/eye.svg'; 5 | import SvgFacebook from '../public/facebook.svg'; 6 | import SvgGoogle from '../public/google.svg'; 7 | import { API } from '../api-client'; 8 | import { useRouter } from 'next/router'; 9 | import axios, { AxiosError } from 'axios'; 10 | import Toast from '../components/Toast'; 11 | import { HttpException } from '@nestjs/common'; 12 | import { Redirect } from '../classes/response-classes'; 13 | 14 | // TODO: need to make this responsive 15 | const Container = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | padding: 5rem 5vw; 19 | `; 20 | 21 | const Continue = styled.button` 22 | background-color: #911d7a; 23 | border-color: transparent; 24 | border-radius: 0.25rem; 25 | border-width: 0; 26 | color: white; 27 | cursor: pointer; 28 | padding: 1rem; 29 | margin: 0.5rem 0; 30 | width: 100%; 31 | `; 32 | 33 | const Eye = styled(SvgEye)` 34 | align-self: center; 35 | cursor: pointer; 36 | margin: 0 0 0 -2rem; 37 | `; 38 | 39 | const FacebookIcon = styled(SvgFacebook)` 40 | cursor: pointer; 41 | margin-right: 2rem; 42 | 43 | :hover { 44 | g[fill='none'] { 45 | stroke: #7e1a6a; 46 | } 47 | path { 48 | fill: #7e1a6a; 49 | } 50 | } 51 | `; 52 | 53 | const ForgotPassword = styled.a` 54 | align-self: flex-end; 55 | color: #aaaaaa; 56 | cursor: pointer; 57 | font-size: 0.75rem; 58 | margin-bottom: 1rem; 59 | `; 60 | 61 | const Form = styled.form` 62 | color: #aaaaaa; 63 | display: flex; 64 | flex-direction: column; 65 | `; 66 | 67 | const GoogleIcon = styled(SvgGoogle)` 68 | cursor: pointer; 69 | 70 | :hover { 71 | g[fill='none'] { 72 | stroke: #911d7a; 73 | } 74 | path { 75 | fill: #911d7a; 76 | } 77 | } 78 | `; 79 | 80 | const Input = styled.input` 81 | background-color: #fafafa; 82 | border-color: transparent; 83 | border-radius: 0.25rem; 84 | border-width: 0; 85 | padding: 1rem; 86 | margin: 0.5rem 0; 87 | width: 100%; 88 | 89 | :focus-visible { 90 | outline: #911d7a auto 1px; 91 | } 92 | `; 93 | 94 | const InputRow = styled.div` 95 | display: flex; 96 | flex-direction: row; 97 | justify-self: flex-end; 98 | padding: 0rem; 99 | margin: 0rem; 100 | 101 | :focus-visible { 102 | outline: #911d7a auto 1px; 103 | } 104 | `; 105 | 106 | const Label = styled.label` 107 | display: flex; 108 | flex-direction: column; 109 | font-size: 0.75rem; 110 | margin-bottom: 0.25rem; 111 | `; 112 | 113 | const NameLabel = styled(Label)` 114 | margin-top: 2rem; 115 | `; 116 | 117 | const OAuth = styled.span` 118 | align-self: center; 119 | color: #aaaaaa; 120 | font-size: 0.75rem; 121 | justify-self: flex-end; 122 | margin: 3rem 0 1rem 0; 123 | `; 124 | 125 | const OAuthIconContainer = styled.div` 126 | display: flex; 127 | flex-direction: row; 128 | justify-content: center; 129 | `; 130 | 131 | const SignInTabBar = styled(TabBar)` 132 | box-shadow: none; 133 | display: flex; 134 | flex-direction: row; 135 | justify-content: flex-start; 136 | margin-bottom: 1.5rem; 137 | padding: 0; 138 | 139 | h2 { 140 | margin-left: 0rem; 141 | margin-right: 0.5rem; 142 | padding: 0.75rem 0rem; 143 | } 144 | `; 145 | 146 | const Submit = styled(Input)` 147 | background-color: #7e1a6a; 148 | color: white; 149 | cursor: pointer; 150 | `; 151 | 152 | const GuestButton = styled.button` 153 | margin-top: 5px; 154 | font-family: Roboto; 155 | font-size: 1rem; 156 | font-style: normal; 157 | text-underline-offset: 1px; 158 | text-align: center; 159 | color: #535353; 160 | text-decoration-line: underline; 161 | background-color: transparent; 162 | border: none; 163 | 164 | :hover { 165 | cursor: pointer; 166 | } 167 | `; 168 | 169 | const signInFields: Array<[string, boolean]> = [ 170 | ['E-MAIL', false], 171 | ['PASSWORD', true], 172 | ]; 173 | const signUpFields: Array<[string, boolean]> = [ 174 | ...signInFields, 175 | ['CONFIRM PASSWORD', true], 176 | ]; 177 | 178 | const SignInForm = (): ReactElement => { 179 | const router = useRouter(); 180 | const [error, setError] = useState(''); 181 | 182 | const signin = async (event: React.SyntheticEvent) => { 183 | event.preventDefault(); 184 | const form = event.currentTarget; 185 | const elements = form.elements as typeof form.elements & { 186 | 'E-MAIL': { value: string }; 187 | PASSWORD: { value: string }; 188 | }; 189 | 190 | try { 191 | const response: Redirect = await API.signIn.post({ 192 | email: elements['E-MAIL'].value, 193 | password: elements.PASSWORD.value, 194 | }); 195 | setError(''); 196 | await axios.get(response.redirect, { withCredentials: true }); 197 | router.push('/'); 198 | } catch (e) { 199 | const err = e as AxiosError; 200 | if (err.response) { 201 | setError(err.response.data.message); 202 | } 203 | } 204 | }; 205 | 206 | return ( 207 |
208 | {signInFields.map(([name, hidable]: [string, boolean]) => { 209 | const [hidden, setHidden] = useState(hidable); 210 | return ( 211 | 218 | ); 219 | })} 220 | FORGOT PASSWORD? 221 | 222 | {error && setError('')} />} 223 | 224 | ); 225 | }; 226 | 227 | const SignUpForm = (): ReactElement => { 228 | const [subtab, setSubtab] = useState(0); 229 | const router = useRouter(); 230 | const [error, setError] = useState(''); 231 | 232 | const signup = async (event: React.SyntheticEvent) => { 233 | event.preventDefault(); 234 | const form = event.currentTarget; 235 | const elements = form.elements as typeof form.elements & { 236 | 'E-MAIL': { value: string }; 237 | PASSWORD: { value: string }; 238 | 'CONFIRM PASSWORD': { value: string }; 239 | NAME: { value: string }; 240 | }; 241 | 242 | if (elements['CONFIRM PASSWORD'].value !== elements.PASSWORD.value) { 243 | setError('Password and confirm password do not match.'); 244 | return; 245 | } 246 | 247 | try { 248 | const response: Redirect = await API.signUp.post({ 249 | email: elements['E-MAIL'].value, 250 | password: elements.PASSWORD.value, 251 | name: elements.NAME.value, 252 | }); 253 | setError(''); 254 | await axios.get(response.redirect, { withCredentials: true }); 255 | 256 | router.push('/welcome'); 257 | } catch (e) { 258 | const err = e as AxiosError; 259 | if (err.response) { 260 | setError(err.response.data.message); 261 | } 262 | } 263 | }; 264 | 265 | return ( 266 |
267 | {signUpFields.map(([name, hidable]: [string, boolean]) => { 268 | const [hidden, setHidden] = useState(hidable); 269 | return ( 270 | 277 | ); 278 | })} 279 | 280 | WHAT WOULD YOU LIKE TO BE CALLED? 281 | 282 | 283 | 284 | 285 | {subtab === 0 ? ( 286 | setSubtab(1)}> 287 | Continue 288 | 289 | ) : ( 290 | 291 | )} 292 | {error && ( 293 | { 296 | setError(''); 297 | setSubtab(0); 298 | }} 299 | /> 300 | )} 301 | 302 | ); 303 | }; 304 | 305 | const SignIn = (): ReactElement => { 306 | const tabNames = ['Sign In', 'Sign Up']; 307 | const [tabIndex, setTabIndex] = useState(0); 308 | return ( 309 | 310 | 315 | {tabIndex === 0 ? ( 316 | <> 317 | 318 | Continue as Guest 319 | OR CONTINUE WITH 320 | 321 | 322 | 323 | 324 | 325 | ) : ( 326 | 327 | )} 328 | 329 | ); 330 | }; 331 | 332 | export default SignIn; 333 | -------------------------------------------------------------------------------- /frontend/public/acne.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/public/doctor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------