├── .eslintrc.json ├── components ├── directory │ ├── ResourcesDisplay.module.css │ ├── sidebar │ │ ├── ClearFieldsButton.module.css │ │ ├── ClearFieldsButton.tsx │ │ ├── ResourceSearchBar.tsx │ │ ├── FilterSidebar.module.css │ │ ├── SidebarCategories.tsx │ │ └── SidebarStatus.tsx │ ├── ResourcesDisplay.tsx │ ├── ResourceCardDisplay.module.css │ └── ResourceCardDisplay.tsx ├── header │ ├── HeaderTypes.ts │ ├── Header.module.css │ ├── HeaderBrand.tsx │ ├── HeaderCollapseMenu.tsx │ ├── HeaderDesktopContent.tsx │ └── Header.tsx ├── Loading.module.css ├── Layout.module.css ├── HelpTooltip.module.css ├── resources │ ├── ResourceDescription.module.css │ ├── ResourceHeader.module.css │ ├── ResourceDescription.tsx │ └── ResourceHeader.tsx ├── Loading.tsx ├── footer │ ├── Footer.tsx │ ├── FooterBrand.tsx │ ├── Footer.module.css │ ├── FooterCredits.tsx │ └── FooterLinks.tsx ├── quiz │ ├── QuizResultsDisplay.module.css │ ├── CategoryCard.tsx │ ├── Quiz.module.css │ ├── QuizResultsDisplay.tsx │ ├── QuizCategoriesForm.tsx │ └── QuizInformation.tsx ├── Layout.tsx ├── HelpTooltip.tsx ├── admin │ └── dashboard │ │ ├── adminDashboardSearch.tsx │ │ ├── adminDashboardHeader.tsx │ │ ├── statusPage.tsx │ │ ├── incomeRangeContainer.tsx │ │ ├── InputField.tsx │ │ ├── tagSelector.tsx │ │ └── incomeRangeRow.tsx ├── bookmark.tsx ├── home │ ├── Home.module.css │ ├── InfoSection.tsx │ ├── TrendingSection.module.css │ └── TrendingSection.tsx ├── dialog.tsx ├── LanguageSelect.tsx └── tag.tsx ├── public ├── locales │ ├── en │ │ ├── common.json │ │ └── quiz.json │ └── es │ │ ├── common.json │ │ └── quiz.json ├── favicon.ico ├── backArrow.png ├── HappyEastie.png ├── eastieWeek2022.png ├── healthyBabyHealthyMama.png ├── happychildliftingbarbell.png ├── triangle.svg ├── plus.svg ├── greyx.svg ├── filter.svg ├── star.svg ├── close.svg ├── changeImageIcon.svg ├── delete.svg ├── pencil.svg ├── listLayout.svg ├── starEmpty.svg ├── checkmark.svg ├── personBlack.svg ├── website.svg ├── gridLayout.svg ├── pencilwhite.svg ├── laptop.svg ├── phonewhite.svg ├── magnifierflat.svg ├── phonefilled.svg ├── bookmark.svg ├── filledbookmark.svg ├── phone.svg ├── email.svg ├── vercel.svg ├── help.svg ├── emailfilled.svg └── share.svg ├── pages ├── admin │ ├── signUp │ │ ├── index.tsx │ │ ├── success.tsx │ │ └── error.tsx │ ├── resetPassword │ │ ├── success.tsx │ │ ├── error.tsx │ │ └── [resetId].tsx │ ├── forgotPassword │ │ ├── success.tsx │ │ └── index.tsx │ ├── addNew │ │ └── index.tsx │ ├── dashboard │ │ ├── invite.tsx │ │ └── index.tsx │ ├── admin.module.css │ └── index.tsx ├── about.module.css ├── directory.module.css ├── quiz │ ├── index.tsx │ ├── [pageId].module.css │ ├── results.tsx │ └── [pageId].tsx ├── api │ ├── resources │ │ ├── categories │ │ │ └── index.ts │ │ ├── languages │ │ │ └── index.ts │ │ ├── accessibility │ │ │ └── index.ts │ │ ├── [resourceId].ts │ │ └── trending.ts │ ├── TODO │ ├── docs │ │ └── resources │ │ │ ├── ShowMatchingResources.md │ │ │ └── ShowResource.md │ ├── events │ │ ├── index.ts.no │ │ └── [eventId].ts.notdealingwiththis │ └── admin │ │ ├── resetPassword.ts │ │ ├── forgotPassword.ts │ │ ├── invite.ts │ │ ├── index.tsx │ │ ├── resources │ │ └── index.tsx │ │ └── authentication │ │ └── index.tsx ├── resources │ ├── [resourceId].module.css │ └── [resourceId].tsx ├── future.tsx ├── test.tsx ├── _document.tsx ├── _app.tsx ├── index.tsx ├── about.tsx └── directory.tsx ├── fonts ├── static │ ├── Raleway-Bold.ttf │ ├── Raleway-Thin.ttf │ ├── Raleway-Black.ttf │ ├── Raleway-Italic.ttf │ ├── Raleway-Light.ttf │ ├── Raleway-Medium.ttf │ ├── Raleway-ExtraBold.ttf │ ├── Raleway-Regular.ttf │ ├── Raleway-SemiBold.ttf │ ├── Raleway-BlackItalic.ttf │ ├── Raleway-BoldItalic.ttf │ ├── Raleway-ExtraLight.ttf │ ├── Raleway-LightItalic.ttf │ ├── Raleway-ThinItalic.ttf │ ├── Raleway-MediumItalic.ttf │ ├── Raleway-SemiBoldItalic.ttf │ ├── Raleway-ExtraBoldItalic.ttf │ └── Raleway-ExtraLightItalic.ttf ├── Raleway-VariableFont_wght.ttf ├── Raleway-Italic-VariableFont_wght.ttf ├── README.txt └── OFL.txt ├── next-i18next.config.js ├── next.config.js ├── util ├── mailService.ts └── utils.ts ├── context └── context.ts ├── db └── resources │ ├── resourceTemplate.json │ ├── resource_3_template.json │ ├── blank.json │ └── sample_resources.json ├── .gitignore ├── tsconfig.json ├── package.json ├── hooks ├── useResourcesDirectory.ts ├── useResources.ts ├── useResource.ts └── useEvents.ts.nope ├── models ├── constants.ts └── types.ts ├── README.md └── styles └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /components/directory/ResourcesDisplay.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | } -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Continue": "Continue" 3 | } -------------------------------------------------------------------------------- /public/locales/es/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Continue": "Continuar" 3 | } -------------------------------------------------------------------------------- /components/header/HeaderTypes.ts: -------------------------------------------------------------------------------- 1 | export type HeaderContent = {title: string, href?: string} -------------------------------------------------------------------------------- /pages/admin/signUp/index.tsx: -------------------------------------------------------------------------------- 1 | import SignupError from "./error"; 2 | 3 | export default SignupError -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/public/favicon.ico -------------------------------------------------------------------------------- /public/backArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/public/backArrow.png -------------------------------------------------------------------------------- /public/HappyEastie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/public/HappyEastie.png -------------------------------------------------------------------------------- /public/eastieWeek2022.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/public/eastieWeek2022.png -------------------------------------------------------------------------------- /fonts/static/Raleway-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Bold.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Thin.ttf -------------------------------------------------------------------------------- /components/directory/sidebar/ClearFieldsButton.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | margin: auto; 3 | margin-bottom: 10px 4 | } -------------------------------------------------------------------------------- /fonts/static/Raleway-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Black.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Italic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Light.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Medium.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-ExtraBold.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-Regular.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-SemiBold.ttf -------------------------------------------------------------------------------- /public/healthyBabyHealthyMama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/public/healthyBabyHealthyMama.png -------------------------------------------------------------------------------- /fonts/Raleway-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/Raleway-VariableFont_wght.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-BlackItalic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-ExtraLight.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-LightItalic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-ThinItalic.ttf -------------------------------------------------------------------------------- /public/happychildliftingbarbell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/public/happychildliftingbarbell.png -------------------------------------------------------------------------------- /fonts/static/Raleway-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-MediumItalic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /fonts/static/Raleway-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/static/Raleway-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /fonts/Raleway-Italic-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/happy-eastie/master/fonts/Raleway-Italic-VariableFont_wght.ttf -------------------------------------------------------------------------------- /components/Loading.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | width: 100%; 4 | align-items: center; 5 | justify-content: "center"; 6 | } -------------------------------------------------------------------------------- /components/Layout.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | } 6 | 7 | .content { 8 | flex-grow: 1; 9 | } -------------------------------------------------------------------------------- /public/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/HelpTooltip.module.css: -------------------------------------------------------------------------------- 1 | .tooltipText { 2 | font-family: 'Raleway'; 3 | font-size: 16px; 4 | font-weight: 600; 5 | letter-spacing: 0.5px; 6 | color: white; 7 | min-width: 12em; 8 | } -------------------------------------------------------------------------------- /next-i18next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | i18n: { 5 | defaultLocale: 'en', 6 | locales: ['en', 'fr', 'es'], 7 | }, 8 | localePath: path.resolve('./public/locales') 9 | }; 10 | -------------------------------------------------------------------------------- /pages/admin/signUp/success.tsx: -------------------------------------------------------------------------------- 1 | import StatusPage from "../../../components/admin/dashboard/statusPage"; 2 | 3 | const SignUpSuccess = () => 4 | 5 | export default SignUpSuccess -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { i18n } = require('./next-i18next.config'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | reactStrictMode: true, 6 | swcMinify: true, 7 | i18n 8 | } 9 | 10 | module.exports = nextConfig 11 | -------------------------------------------------------------------------------- /pages/admin/resetPassword/success.tsx: -------------------------------------------------------------------------------- 1 | import StatusPage from "../../../components/admin/dashboard/statusPage"; 2 | 3 | const ResetPasswordSuccess = () => 4 | 5 | export default ResetPasswordSuccess -------------------------------------------------------------------------------- /public/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/about.module.css: -------------------------------------------------------------------------------- 1 | .container h2, .container p { 2 | color: var(--secondary-text) 3 | } 4 | .container h1 { 5 | color: var(--brand-primary); 6 | margin: auto; 7 | text-align: center; 8 | } 9 | .container ul { 10 | list-style-type: disc; 11 | } -------------------------------------------------------------------------------- /pages/admin/forgotPassword/success.tsx: -------------------------------------------------------------------------------- 1 | import StatusPage from "../../../components/admin/dashboard/statusPage"; 2 | 3 | const ForgotPasswordSuccess = () => 4 | 5 | export default ForgotPasswordSuccess -------------------------------------------------------------------------------- /public/greyx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/admin/resetPassword/error.tsx: -------------------------------------------------------------------------------- 1 | import StatusPage from "../../../components/admin/dashboard/statusPage"; 2 | 3 | const ResetPasswordError = () => 4 | 5 | export default ResetPasswordError -------------------------------------------------------------------------------- /pages/admin/signUp/error.tsx: -------------------------------------------------------------------------------- 1 | import StatusPage from "../../../components/admin/dashboard/statusPage"; 2 | 3 | const SignUpError = () => 4 | 5 | export default SignUpError -------------------------------------------------------------------------------- /public/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pages/directory.module.css: -------------------------------------------------------------------------------- 1 | .back { 2 | color: var(--secondary-text); 3 | border: none; 4 | outline: none; 5 | background-color: transparent; 6 | font-family: "Raleway"; 7 | font-size: 30px; 8 | font-weight: 600; 9 | margin-top: 15px; 10 | margin-left: 50px; 11 | margin-bottom: 15px; 12 | } -------------------------------------------------------------------------------- /components/resources/ResourceDescription.module.css: -------------------------------------------------------------------------------- 1 | .descriptionText * { 2 | font-family: 'Raleway'; 3 | color: black; 4 | } 5 | 6 | @media screen and (max-width: 650px) { 7 | .addressText { 8 | text-align: left; 9 | } 10 | } 11 | 12 | @media screen and (min-width: 650px) { 13 | .addressText { 14 | text-align: right; 15 | } 16 | } -------------------------------------------------------------------------------- /util/mailService.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from 'resend'; 2 | 3 | const resend = new Resend(process.env.RESEND_KEY); 4 | 5 | export async function sendEmail(to: string, subject: string, html: string) { 6 | await resend.emails.send({ 7 | from: 'happyeastie@resend.dev', 8 | to, 9 | subject, 10 | html 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /public/changeImageIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/resources/ResourceHeader.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | width: 52px; 3 | height: 52px; 4 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 5 | } 6 | 7 | .resourceHeader { 8 | font-family: 'Raleway'; 9 | font-weight: 600; 10 | font-size: 38px; 11 | margin-top: 40px; 12 | text-align: center; 13 | line-height: 44px; 14 | color: var(--brand-primary); 15 | margin-bottom: 20px; 16 | } -------------------------------------------------------------------------------- /components/header/Header.module.css: -------------------------------------------------------------------------------- 1 | .navbarContent * { 2 | font-family: 'Raleway'; 3 | font-size: 24px; 4 | font-weight: 600; 5 | color: var(--secondary-text); 6 | text-decoration: none; 7 | } 8 | 9 | .navbarContent a:hover { 10 | text-decoration: none; 11 | } 12 | 13 | .navbar { 14 | opacity: 1; 15 | background-color: white; 16 | margin-top: 10px; 17 | display: flex; 18 | align-items: center; 19 | } -------------------------------------------------------------------------------- /public/listLayout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /context/context.ts: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react' 2 | 3 | // state to be accesible throughout application 4 | // stores encrypted version of user's quiz responses, and a function that enables updating this value 5 | // other pages can use the cached quiz responses to request the appropriate resources 6 | export const AppContext = createContext({ 7 | encryptedQuizResponse: "", 8 | changeEncryptedQuizResponse: (newHash: string) => {} 9 | }) 10 | -------------------------------------------------------------------------------- /public/starEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/quiz/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import React, { useEffect } from 'react' 3 | import { useRouter } from 'next/router' 4 | import Layout from '../../components/Layout' 5 | import Loading from '../../components/Loading' 6 | 7 | const Quiz: NextPage = () => { 8 | const router = useRouter() 9 | 10 | useEffect(() => { 11 | router.push('quiz/1') 12 | }); 13 | 14 | return 15 | } 16 | 17 | export default Quiz -------------------------------------------------------------------------------- /public/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/personBlack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/website.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loading as NextUILoading } from "@nextui-org/react"; 2 | import styles from "./Loading.module.css" 3 | const Loading = ({relative}: {relative?: boolean}) => { 4 | 5 | 6 | return ( 15 | Loading... 16 | ) 17 | } 18 | export default Loading -------------------------------------------------------------------------------- /db/resources/resourceTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "", 4 | "summary": "", 5 | "category": [], 6 | "keywords": [], 7 | "incomeByHouseholdMembers": [], 8 | "documentationRequired": false, 9 | "headerImageUrl": "", 10 | "website": "", 11 | "phone": "", 12 | "email": "", 13 | "address": "", 14 | "location": {"type": "Point", "coordinates": []}, 15 | "availableLanguages": [], 16 | "accessibilityOptions": [], 17 | "eligibilityInfo": "" 18 | } 19 | 20 | -------------------------------------------------------------------------------- /pages/quiz/[pageId].module.css: -------------------------------------------------------------------------------- 1 | #quizTitle { 2 | font-size: 38px; 3 | font-family: "Raleway"; 4 | font-weight: 600; 5 | line-height: 44.61px; 6 | color: var(--brand-primary); 7 | letter-spacing: 1px; 8 | text-align: center; 9 | } 10 | 11 | #quizSubtitle { 12 | font-size: 20px; 13 | font-family: "Raleway"; 14 | font-weight: 400; 15 | line-height: 23px; 16 | color: var(--secondary-text); 17 | letter-spacing: 0.25px; 18 | text-align: center; 19 | } -------------------------------------------------------------------------------- /components/directory/sidebar/ClearFieldsButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@nextui-org/react" 2 | import styles from "./ClearFieldsButton.module.css" 3 | interface ClearFieldsButtonProps{ 4 | setField: (clearedValue: T) => void; 5 | clearedValue: T; 6 | } 7 | export default function ClearFieldsButton({setField, clearedValue} : ClearFieldsButtonProps) { 8 | return () 11 | } -------------------------------------------------------------------------------- /components/footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@nextui-org/react'; 2 | import styles from "./Footer.module.css" 3 | import FooterBrand from './FooterBrand'; 4 | import FooterCredits from './FooterCredits'; 5 | import FooterLinks from './FooterLinks'; 6 | 7 | export default function Footer() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/footer/FooterBrand.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Image } from "@nextui-org/react"; 2 | 3 | export default function FooterBrand() { 4 | return ( 5 | 17 | HappyEastie 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/resources/categories/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import mongoDbInteractor from "../../../../db/mongoDbInteractor"; 3 | import { RESOURCE_COLLECTION } from "../../../../models/constants"; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | const languages = await mongoDbInteractor.getDistinctValues( 10 | RESOURCE_COLLECTION, 11 | "category" 12 | ); 13 | 14 | return res.status(200).json(languages); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /pages/resources/[resourceId].module.css: -------------------------------------------------------------------------------- 1 | .back { 2 | color: white; 3 | position: absolute; 4 | left: 30px; 5 | top: 100px; 6 | z-index: 2; 7 | border: none; 8 | outline: none; 9 | background-color: transparent; 10 | font-family: "Raleway"; 11 | font-size: 16pt; 12 | font-weight: 600; 13 | text-shadow: 0px 2.31185px 2.31185px rgba(0, 0, 0, 0.25); 14 | } 15 | 16 | .back:hover { 17 | filter: brightness(0.85); 18 | cursor: pointer; 19 | } 20 | 21 | .descriptionText * { 22 | color: black; 23 | } -------------------------------------------------------------------------------- /pages/api/resources/languages/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import mongoDbInteractor from "../../../../db/mongoDbInteractor"; 3 | import { RESOURCE_COLLECTION } from "../../../../models/constants"; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | const languages = await mongoDbInteractor.getDistinctValues( 10 | RESOURCE_COLLECTION, 11 | "availableLanguages" 12 | ); 13 | 14 | return res.status(200).json(languages); 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/resources/accessibility/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import mongoDbInteractor from "../../../../db/mongoDbInteractor"; 3 | import { RESOURCE_COLLECTION } from "../../../../models/constants"; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | const accessibility = await mongoDbInteractor.getDistinctValues( 10 | RESOURCE_COLLECTION, 11 | "accessibilityOptions" 12 | ); 13 | 14 | return res.status(200).json(accessibility); 15 | } 16 | -------------------------------------------------------------------------------- /public/gridLayout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/quiz/QuizResultsDisplay.module.css: -------------------------------------------------------------------------------- 1 | 2 | .backButton { 3 | font-family: "Raleway"; 4 | font-size: 30px; 5 | font-weight: 600; 6 | margin-top: 15px; 7 | color: var(--secondary-text); 8 | text-decoration: none; 9 | } 10 | 11 | .backButton:hover { 12 | text-decoration: none; 13 | 14 | } 15 | 16 | .container { 17 | padding: 0 2rem; 18 | } 19 | 20 | .container h1, .container h2 { 21 | color: var(--brand-primary) 22 | } 23 | 24 | .container h2 { 25 | text-align: center; 26 | } 27 | .subtitle { 28 | color: var(--secondary-text) 29 | } -------------------------------------------------------------------------------- /pages/quiz/results.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import React from 'react' 3 | import { useRouter } from "next/router"; 4 | import { QuizResultsDisplay } from '../../components/quiz/QuizResultsDisplay' 5 | import Layout from '../../components/Layout'; 6 | 7 | 8 | const QuizResults: NextPage = () => { 9 | const router = useRouter(); 10 | 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default QuizResults 19 | -------------------------------------------------------------------------------- /pages/api/TODO: -------------------------------------------------------------------------------- 1 | Backend: 2 | 3 | GET api/resources/:resourceID: 4 | - Returns: a specific Resource 5 | 6 | POST /api/resources 7 | - Body param format: 8 | {data: EncryptedQuizResponse} 9 | - Returns: a list of resources user is eligible for 10 | 11 | 12 | Frontend: 13 | 14 | /quiz 15 | Fill out quiz 16 | Encrypt quiz responses and keep in frontend state 17 | Redirect to /resources 18 | 19 | /resources 20 | Gets list of eligible resources for user based on encrypted quiz responses in frontend state 21 | Displays resources 22 | 23 | /resources/:resourceId 24 | Display a single resource 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from "react" 2 | import Footer from "./footer/Footer" 3 | import Header from "./header/Header" 4 | import styles from "./Layout.module.css" 5 | const Layout = ({children, includePadding = true}: {children: ReactNode, includePadding?: boolean}) => { 6 | 7 | const css : CSSProperties = includePadding? {padding: "0 2em"} : {} 8 | 9 | return (
10 |
11 |
{children}
12 |
13 |
) 14 | } 15 | 16 | export default Layout -------------------------------------------------------------------------------- /db/resources/resource_3_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "", 4 | "summary": "", 5 | "category": [ 6 | "" 7 | ], 8 | "keywords": [], 9 | "incomeByHouseholdMembers": [ 10 | { 11 | "maximum": 60000, 12 | "minimum": 0 13 | } 14 | ], 15 | "documentationRequired": false, 16 | "headerImageUrl": "", 17 | "website": "", 18 | "phone": "", 19 | "email": "", 20 | "address": "", 21 | "location": { 22 | "type": "Point", 23 | "coordinates": [] 24 | }, 25 | "availableLanguages": [ 26 | "" 27 | ], 28 | "accessibilityOptions": [ 29 | "" 30 | ], 31 | "eligibilityInfo": "" 32 | } -------------------------------------------------------------------------------- /components/header/HeaderBrand.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Navbar, Image, Spacer } from "@nextui-org/react" 2 | import NextLink from "next/link" 3 | 4 | const HeaderBrand = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | HappyEastie 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export default HeaderBrand 24 | -------------------------------------------------------------------------------- /public/pencilwhite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/laptop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/phonewhite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/HelpTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Image, Text } from "@nextui-org/react" 2 | import styles from "./HelpTooltip.module.css" 3 | 4 | type HelpProps = { 5 | grayscale?: boolean, 6 | diameter: number, 7 | text: string, 8 | }; 9 | 10 | export const HelpTooltip = (props: HelpProps) => { 11 | let css = {} 12 | if (props.grayscale) css = {filter: "grayscale(1)"} 13 | 14 | return ( 15 | {props.text}} color="primary"> 16 | Help 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /pages/future.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layout"; 2 | import {Text, Image, Col, Container} from "@nextui-org/react" 3 | import NextImage from "next/image" 4 | export default function FuturePage() { 5 | return ( 6 | 7 | Coming soon! 8 | 9 | This page will be added in the future. Please enjoy the rest of the website! 10 | 11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /db/resources/blank.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": { 3 | "$oid": "6389b490dd0b40726d0ca8d3" 4 | }, 5 | "accessibility": [], 6 | "category": [], 7 | "citizenship": [], 8 | "description": "", 9 | "email": "", 10 | "employmentStatus": [], 11 | "family": [], 12 | "headerImageUrl": "", 13 | "howToApply": "", 14 | "insurance": [], 15 | "language": [], 16 | "location": {"type": "Point", "coordinates": []}, 17 | "maximumChildAge": 0, 18 | "maximumIncome": 0, 19 | "maximumParentAge": 0, 20 | "minimumIncome": 0, 21 | "minimumParentAge": 0, 22 | "name": "", 23 | "phoneNumber": "", 24 | "pointOfContact": "", 25 | "tags": [], 26 | "url": "", 27 | "waitlist": { "description": ""} 28 | } -------------------------------------------------------------------------------- /pages/test.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Radio, Text } from "@nextui-org/react" 2 | import { useState } from "react" 3 | 4 | const mockItems = ["blake", "hey", "man", "test"] 5 | export default function Test() { 6 | const [broken, setBroken] = useState(undefined) 7 | console.log(broken) 8 | return ( 9 | <> 10 | 11 | {mockItems.map(s => {s})} 12 | 13 | 17 | 18 | 19 | ) 20 | } -------------------------------------------------------------------------------- /pages/admin/addNew/index.tsx: -------------------------------------------------------------------------------- 1 | import { withIronSessionSsr } from "iron-session/next"; 2 | import { NextPage } from "next" 3 | import { NORMAL_IRON_OPTION } from "../../../models/constants"; 4 | 5 | export const getServerSideProps = withIronSessionSsr( 6 | async function getServerSideProps(ctx) { 7 | const user = ctx.req?.session.user; 8 | 9 | if (!user || user.isAdmin !== true) { 10 | return { 11 | notFound: true, 12 | }; 13 | } 14 | 15 | return { 16 | props: { 17 | user: ctx.req.session.user, 18 | }, 19 | }; 20 | }, 21 | NORMAL_IRON_OPTION 22 | ); 23 | 24 | const AddNewResource: NextPage = () => { 25 | return <>This is a page to add new resource 26 | } 27 | 28 | export default AddNewResource -------------------------------------------------------------------------------- /components/footer/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footerGrid { 2 | display: "flex"; 3 | flex-direction: "row"; 4 | justify-content: "center"; 5 | align-items: "center"; 6 | background-color: var(--brand-primary); 7 | width: 100%; 8 | /* padding-bottom: 50px; */ 9 | margin-top: 20px; 10 | min-height: 150px; 11 | } 12 | 13 | .footerLinkColumn { 14 | padding-left: 20px; 15 | padding-right: 20px; 16 | } 17 | 18 | .footerLink { 19 | color: white; 20 | font-family: 'Raleway'; 21 | font-size: 18px; 22 | font-weight: 600; 23 | word-break: break-all; 24 | } 25 | 26 | .footerLinkDescription { 27 | color: white; 28 | font-family: 'Raleway'; 29 | font-size: 18px; 30 | font-weight: 600; 31 | word-break: break-word; 32 | } 33 | -------------------------------------------------------------------------------- /components/header/HeaderCollapseMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "@nextui-org/react" 2 | import { HeaderContent } from "./HeaderTypes" 3 | import NextLink from 'next/link' 4 | 5 | const HeaderCollapseMenu = ({ items }: { items: HeaderContent[] }) => { 6 | return ( 7 | 8 | {items.map((item, index) => ( 9 | 10 | 14 | {item.title} 15 | 16 | 17 | ))} 18 | 19 | ) 20 | } 21 | 22 | export default HeaderCollapseMenu 23 | -------------------------------------------------------------------------------- /public/magnifierflat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/phonefilled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /pages/api/docs/resources/ShowMatchingResources.md: -------------------------------------------------------------------------------- 1 | ## **Show Matching Resources** 2 | 3 | Returns json data about all resources that match the provided criteria. 4 | 5 | - **URL** 6 | 7 | /resources 8 | 9 | - **Method:** 10 | 11 | `POST` 12 | 13 | - **URL Params** 14 | 15 | **Required:** 16 | 17 | None 18 | 19 | - **Data Params** 20 | 21 | ```javascript 22 | { data: string } 23 | ``` 24 | `data` represents the quiz responses encrypted in AES with a secret passphrase. 25 | 26 | - **Success Response:** 27 | 28 | - **Code:** 200 29 | **Content:** 30 | ```javascript 31 | [{ 32 | id: string; 33 | name: string; 34 | description: string; 35 | incomeLevel: number; 36 | employed: boolean; 37 | }] 38 | ``` 39 | 40 | - **Error Response:** 41 | 42 | None 43 | -------------------------------------------------------------------------------- /components/header/HeaderDesktopContent.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "@nextui-org/react" 2 | import { HeaderContent } from "./HeaderTypes" 3 | import NextLink from "next/link" 4 | import styles from "./Header.module.css" 5 | import { useRouter } from "next/router" 6 | 7 | const HeaderDesktopContent = ({ items }: { items: HeaderContent[] }) => { 8 | const router = useRouter() 9 | 10 | return ( 11 | 12 | {items.map(item => 13 | 15 | 16 | {item.title} 17 | 18 | )} 19 | 20 | ) 21 | } 22 | 23 | export default HeaderDesktopContent 24 | -------------------------------------------------------------------------------- /public/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { CssBaseline } from '@nextui-org/react'; 4 | 5 | class MyDocument extends Document { 6 | static async getInitialProps(ctx: any) { 7 | const initialProps = await Document.getInitialProps(ctx); 8 | return { 9 | ...initialProps, 10 | styles: React.Children.toArray([initialProps.styles]) 11 | }; 12 | } 13 | 14 | // pages/_document.js 15 | // TODO: https://nextjs.org/docs/deployment#manual-graceful-shutdowns 16 | 17 | render() { 18 | return ( 19 | 20 | {CssBaseline.flush()} 21 | 22 |
23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default MyDocument; 31 | -------------------------------------------------------------------------------- /public/filledbookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/admin/dashboard/adminDashboardSearch.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Row, Image } from "@nextui-org/react"; 2 | import { ChangeEvent } from "react"; 3 | import { FormElement } from "@nextui-org/react"; 4 | 5 | type SearchProps = { 6 | onChange(e: ChangeEvent): void; 7 | }; 8 | 9 | export const AdminDashboardSearch = (props: SearchProps) => { 10 | return ( 11 | 12 | } 19 | /> 20 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /pages/api/docs/resources/ShowResource.md: -------------------------------------------------------------------------------- 1 | ## **Show Resource** 2 | 3 | Returns json data about a single resource. 4 | 5 | - **URL** 6 | 7 | /resources/:id 8 | 9 | - **Method:** 10 | 11 | `GET` 12 | 13 | - **URL Params** 14 | 15 | **Required:** 16 | 17 | `id=[string]` 18 | 19 | - **Data Params** 20 | 21 | None 22 | 23 | - **Success Response:** 24 | 25 | - **Code:** 200 26 | **Content:** 27 | ```javascript 28 | { 29 | id: string; 30 | name: string; 31 | description: string; 32 | incomeLevel: number; 33 | employed: boolean; 34 | } 35 | ``` 36 | 37 | - **Error Response:** 38 | 39 | - **Code:** 404 NOT FOUND 40 | **Content:** `{ error : "Resource :id not found" }` 41 | 42 | OR 43 | 44 | - **Code:** 400 BAD REQUEST 45 | **Content:** `{ error : "Invalid resourceId: must be of type string" }` 46 | -------------------------------------------------------------------------------- /components/directory/sidebar/ResourceSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { FormElement, Input, Image } from "@nextui-org/react"; 2 | import { ChangeEvent } from "react"; 3 | 4 | interface ResourceSearchBarProps { 5 | placeholder: string; 6 | onChange: (e: ChangeEvent) => void 7 | } 8 | 9 | export const ResourceSearchBar: React.FC = (props: ResourceSearchBarProps) => { 10 | return ( 11 | } 22 | /> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /public/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/quiz/CategoryCard.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Checkbox, Col, Row, Text } from "@nextui-org/react"; 2 | import { Dispatch, SetStateAction, useState } from "react"; 3 | 4 | type CardProps = { 5 | title: string; 6 | setSelected(s: string): void; 7 | }; 8 | 9 | export function CategoryCard(props: CardProps) { 10 | const [selected, setSelected] = useState(false); 11 | 12 | return ( 13 | { 18 | setSelected(!selected); 19 | props.setSelected(props.title); 20 | }} 21 | > 22 | 23 | 24 | {props.title} 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/bookmark.tsx: -------------------------------------------------------------------------------- 1 | import React, { SyntheticEvent, useState } from "react"; 2 | import { Image } from "@nextui-org/react"; 3 | import Dialog from "./dialog"; 4 | 5 | type BookmarkProps = { 6 | enabled: boolean; 7 | }; 8 | 9 | export default function Bookmark(props: BookmarkProps) { 10 | const [state, setState] = useState(props.enabled); 11 | 12 | const onClick = (e: SyntheticEvent) => { 13 | setState(!state); 14 | e.stopPropagation(); 15 | }; 16 | 17 | return ( 18 |
19 | 22 | { setState(false) }} 27 | /> 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/footer/FooterCredits.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Link, Text, Spacer } from "@nextui-org/react"; 2 | import styles from "./Footer.module.css" 3 | 4 | export default function FooterCredits() { 5 | return ( 6 | 19 |
20 | Made by Sandbox 21 |
22 | 23 |
24 | https://www.sandboxnu.com 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /components/home/Home.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 45px; 3 | font-family: 'Raleway'; 4 | font-weight: 600; 5 | line-height: 53px; 6 | color: var(--secondary-text); 7 | letter-spacing: 1px; 8 | } 9 | 10 | .subtitle { 11 | font-size: 30px; 12 | font-family: 'Raleway'; 13 | font-weight: 400; 14 | line-height: 41px; 15 | color: var(--secondary-text); 16 | letter-spacing: 0.5px; 17 | } 18 | 19 | .button { 20 | background-color: var(--brand-primary); 21 | 22 | width: 307px; 23 | height: 66px; 24 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 25 | } 26 | .button a { 27 | font-size: 30px; 28 | font-family: 'Raleway'; 29 | font-weight: 700; 30 | color: white; 31 | } 32 | 33 | .tableContent { 34 | font-family: 'Raleway'; 35 | font-size: 24px; 36 | font-weight: 600; 37 | color: var(--primary-text) !important; 38 | } 39 | 40 | .sectionHeader { 41 | letter-spacing: 0.5px; 42 | } -------------------------------------------------------------------------------- /components/dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Modal, Button, Text } from "@nextui-org/react"; 3 | 4 | type DialogProps = { 5 | title: string, 6 | message: string, 7 | visible: boolean, 8 | onCloseHandler: () => void, 9 | } 10 | export default function Dialog(props: DialogProps) { 11 | 12 | const closeHandler = () => { 13 | props.onCloseHandler(); 14 | }; 15 | 16 | return ( 17 | 21 | 22 | 23 | {props.title} 24 | 25 | 26 | 27 | {props.message} 28 | 29 | 30 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /public/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/directory/ResourcesDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@nextui-org/react"; 2 | import { WithId } from "mongodb"; 3 | import { Resource } from "../../models/types"; 4 | import { ResourceCardDisplay } from "./ResourceCardDisplay"; 5 | import styles from "./ResourcesDisplay.module.css" 6 | 7 | interface ResourcesDisplayProps { 8 | resources: WithId[]; 9 | } 10 | 11 | export const ResourcesDisplay: React.FC = (props: ResourcesDisplayProps) => { 12 | return ( 13 | 14 | {props.resources?.map((resourceResult: WithId) => ( 15 | 16 | 17 |
18 |
19 | ))} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/directory/sidebar/FilterSidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebarBanner { 2 | font-family: 'Raleway'; 3 | font-size: 15px; 4 | font-weight: 600; 5 | color: #FFFFFF; 6 | background-color: #219EA4; 7 | height: 48px; 8 | padding-left: 16px; 9 | padding-top: 12px; 10 | letter-spacing: 1px; 11 | } 12 | 13 | .sidebarSubCategory { 14 | font-family: 'Raleway'; 15 | font-size: 15px; 16 | font-weight: 600; 17 | color: #000000; 18 | margin-left: 36px; 19 | margin-bottom: 10px; 20 | letter-spacing: 0.25px; 21 | } 22 | 23 | .sidebarCheckboxGroup { 24 | margin-left: 60px; 25 | margin-bottom: 30px; 26 | } 27 | 28 | .sidebarCheckboxText { 29 | font-family: 'Raleway'; 30 | font-size: 15px; 31 | font-weight: 400; 32 | color: #000000; 33 | letter-spacing: 0.25px; 34 | } 35 | 36 | .sidebarInputBox { 37 | font-family: 'Raleway'; 38 | font-size: 15px; 39 | font-weight: 400; 40 | color: #000000; 41 | margin-left: 60px; 42 | margin-bottom: 30px; 43 | width: 150px; 44 | } 45 | -------------------------------------------------------------------------------- /components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "@nextui-org/react" 2 | import { HeaderContent } from "./HeaderTypes" 3 | import HeaderDesktopContent from "./HeaderDesktopContent" 4 | import HeaderCollapseMenu from "./HeaderCollapseMenu" 5 | import HeaderBrand from "./HeaderBrand" 6 | import styles from "./Header.module.css" 7 | import LanguageSelect from "../LanguageSelect" 8 | 9 | const items: HeaderContent[] = [ 10 | { title: "Home", href: "/" }, 11 | { title: "Quiz", href: "/quiz" }, 12 | { title: "Community Events", href: "/future" }, 13 | { title: "Resources", href: "/directory" }, 14 | { title: "About", href: "/about" } 15 | ] 16 | 17 | const Header = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Header 31 | -------------------------------------------------------------------------------- /components/admin/dashboard/adminDashboardHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Navbar, Text } from "@nextui-org/react" 2 | import { useRouter } from "next/router"; 3 | 4 | export const AdminDashboardHeader = () => { 5 | const router = useRouter() 6 | const logout = async () => { 7 | const requestSettings = { 8 | method: "POST", 9 | body: JSON.stringify({type: "logout"}), 10 | headers: { "Content-Type": "application/json" }, 11 | }; 12 | const response = await fetch("/api/admin/authentication", requestSettings); 13 | if (response.status !== 200) { 14 | } else { 15 | router.push("/admin") 16 | } 17 | } 18 | return 19 | 20 | Admin Dashboard 21 | 22 | 23 | 24 | 25 | 26 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-eastie", 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 | "@nextui-org/react": "^1.0.0-beta.10", 13 | "crypto-js": "^4.1.1", 14 | "formik": "^2.2.9", 15 | "i18next": "^22.4.9", 16 | "iron-session": "^6.3.1", 17 | "mongodb": "^4.11.0", 18 | "next": "12.3.0", 19 | "next-i18next": "^13.1.5", 20 | "password-validator": "^5.3.0", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "react-i18next": "^12.1.5", 24 | "react-markdown": "^8.0.3", 25 | "resend": "^0.15.1", 26 | "swr": "^1.3.0", 27 | "use-file-picker": "^1.7.0", 28 | "yup": "^0.32.11" 29 | }, 30 | "devDependencies": { 31 | "@types/crypto-js": "^4.1.1", 32 | "@types/node": "18.7.18", 33 | "@types/react": "18.0.20", 34 | "@types/react-dom": "18.0.6", 35 | "eslint": "8.23.1", 36 | "eslint-config-next": "12.3.0", 37 | "typescript": "4.8.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /components/home/InfoSection.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Image, Text, Button, Spacer, Link } from "@nextui-org/react"; 2 | import styles from "./Home.module.css" 3 | import NextLink from "next/link" 4 | import NextImage from "next/image" 5 | export default function InfoSection() { 6 | return ( 7 | 8 | 9 | Default Image 10 | 11 | 12 | Here to help find the resources for you! 13 | 14 | 15 | Tap "Help My Search" to answer questions and get personalized resource results. 16 | 17 | 18 | 23 | 24 | 25 | ); 26 | } -------------------------------------------------------------------------------- /components/directory/ResourceCardDisplay.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: var(--brand-light-blue); 3 | height: 300px; 4 | width: min(515px, 100%); 5 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 6 | border-radius: 0; 7 | } 8 | 9 | .cardFooter{ 10 | min-height: 20%; 11 | margin-left: 20px; 12 | } 13 | .cardFooterText { 14 | font-family: 'Raleway'; 15 | font-size: 15px; 16 | font-weight: 700; 17 | color: #191E3E; 18 | margin-right: 20px; 19 | margin-left: 10px; 20 | } 21 | 22 | .cardHeader { 23 | margin-top: 20px; 24 | } 25 | .cardHeaderText { 26 | font-family: 'Raleway'; 27 | font-size: 23px; 28 | font-weight: 600; 29 | line-height: 27px; 30 | color: #6F6D6D; 31 | margin-left: 20px; 32 | width: 80%; 33 | } 34 | 35 | .cardBody { 36 | padding-top: 0; 37 | } 38 | 39 | .cardSummary { 40 | font-family: 'Raleway'; 41 | font-size: 16px; 42 | font-weight: 600; 43 | line-height: 20px; 44 | color: var(--secondary-text); 45 | margin-left: 20px; 46 | letter-spacing: 0.5px; 47 | line-height: 19px; 48 | padding-right: 12px; 49 | overflow: hidden 50 | } -------------------------------------------------------------------------------- /public/locales/en/quiz.json: -------------------------------------------------------------------------------- 1 | { 2 | "Food": "Food", 3 | "Healthcare": "Healthcare", 4 | "Financial Assistance": "Financial Assistance", 5 | "Education": "Education", 6 | "Childcare": "Childcare", 7 | "Arabic": "Arabic", 8 | "Chinese": "Chinese", 9 | "Spanish": "Spanish", 10 | "Portuguese": "Portuguese", 11 | "Hindi": "Hindi", 12 | "English": "English", 13 | "Vietnamese": "Vietnamese", 14 | "Hearing": "Hearing", 15 | "Vision": "Vision", 16 | "Household Income": "Household Income", 17 | "Household Members": "Household Members", 18 | "Do you have some form of photo ID?": "Do you have some form of photo ID?", 19 | "Yes": "Yes", 20 | "No": "No", 21 | "Prefer not to say": "Prefer not to say", 22 | "Languages": "Languages", 23 | "Accessibility": "Accessibility", 24 | "Continue": "Continue", 25 | "Back": "Back", 26 | "Submit": "Submit", 27 | "Select what you need help with.": "Select what you need help with.", 28 | "All answer fields are optional to ensure that you only share as much information as you like.": "All answer fields are optional to ensure that you only share as much information as you like.", 29 | "Resource Quiz": "Resource Quiz" 30 | } -------------------------------------------------------------------------------- /hooks/useResourcesDirectory.ts: -------------------------------------------------------------------------------- 1 | import useSWRImmutable from 'swr/immutable' 2 | import { ResourceData, ResourcesResponse } from '../pages/api/resources' 3 | 4 | // hook for getting resources from api to display in frontend 5 | 6 | // useSWR will pass loading/error state if data not retrieved 7 | // when resources are ready (not in state isLoading or isError) the data will be propogated 8 | // to components using useEvents and can be accessed in events field 9 | 10 | // data is cached so request is not sent to api every time page is loaded 11 | export const useResourcesDirectory = (searchQuery: string = "") => { 12 | const requestBody = searchQuery ? JSON.stringify({searchParam: searchQuery}) : null 13 | const requestSettings = { method: 'POST', body: requestBody, headers: {'Content-Type': 'application/json'}} 14 | const resourcesFetcher = async () : Promise => { 15 | const response : Response = await fetch('/api/resources', requestSettings) 16 | const resources : ResourcesResponse = await response.json() 17 | return resources.data 18 | } 19 | const {data, error}= useSWRImmutable(`/api/resources`, resourcesFetcher) 20 | return data?.requested 21 | } 22 | -------------------------------------------------------------------------------- /components/home/TrendingSection.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0; 3 | margin: 0; 4 | max-width: 100%; 5 | } 6 | 7 | .eventCardsRow { 8 | display: flex; 9 | flex-wrap: nowrap; 10 | overflow-x: scroll; 11 | width: 100%; 12 | margin: 0; 13 | padding-bottom: 40px; 14 | } 15 | 16 | .eventCard { 17 | width: 400px; 18 | height: 400px; 19 | flex: 0 0 auto; 20 | margin: 0 25px; 21 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 22 | border-radius: 0; 23 | } 24 | 25 | .eventCardImageHeader { 26 | height: "40%"; 27 | } 28 | .eventCardHeaderText { 29 | font-family: 'Raleway'; 30 | font-weight: 600; 31 | font-size: 23px; 32 | line-height: 27px; 33 | color: #000000; 34 | white-space: nowrap; 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | margin-right: 25px; 38 | margin-bottom: 10px; 39 | } 40 | 41 | .eventCardBodyText { 42 | font-family: 'Raleway'; 43 | font-weight: 400; 44 | font-size: 16px; 45 | line-height: 19px; 46 | color: #7E7C7C; 47 | letter-spacing: 0.3px; 48 | overflow: hidden; 49 | display: -webkit-box; 50 | -webkit-box-orient: vertical; 51 | -webkit-line-clamp: 3; 52 | } 53 | -------------------------------------------------------------------------------- /public/locales/es/quiz.json: -------------------------------------------------------------------------------- 1 | { 2 | "Food": "Comida", 3 | "Healthcare": "Atención médica", 4 | "Financial Assistance": "Asistencia financiera", 5 | "Education": "Educación", 6 | "Childcare": "Cuidado infantil", 7 | "Arabic": "Árabe", 8 | "Chinese": "Chino", 9 | "Spanish": "Español", 10 | "Portuguese": "Portugués", 11 | "Hindi": "Hindi", 12 | "English": "Inglés", 13 | "Vietnamese": "Vietnamita", 14 | "Hearing": "Audición", 15 | "Vision": "Visión", 16 | "Household Income": "Ingreso del Hogar", 17 | "Household Members": "Miembros del Hogar", 18 | "Do you have some form of photo ID?": "¿Tiene algún tipo de identificación con foto?", 19 | "Yes": "Sí", 20 | "No": "No", 21 | "Prefer not to say": "Prefiero no decirlo", 22 | "Languages": "Idiomas", 23 | "Accessibility": "Accesibilidad", 24 | "Continue": "Continuar", 25 | "Back": "Atrás", 26 | "Submit": "Enviar", 27 | "Select what you need help with.": "Seleccione lo que necesita ayuda.", 28 | "All answer fields are optional to ensure that you only share as much information as you like.": "Todos los campos de respuesta son opcionales para asegurarse de que solo comparta la información que desee.", 29 | "Resource Quiz": "Cuestionario de Recursos" 30 | } -------------------------------------------------------------------------------- /components/admin/dashboard/statusPage.tsx: -------------------------------------------------------------------------------- 1 | import { Spacer, Button, Text} from "@nextui-org/react"; 2 | import { useRouter } from "next/router"; 3 | import styles from "../../../pages/admin/admin.module.css"; 4 | 5 | 6 | const StatusPage = ({message}: {message: string}) => { 7 | 8 | const router = useRouter(); 9 | 10 | return ( 11 |
12 |
13 | 14 | 15 | {message} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | ) 40 | } 41 | 42 | export default StatusPage; -------------------------------------------------------------------------------- /pages/api/resources/[resourceId].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import type { Resource } from "../../../models/types"; 3 | import mongoDbInteractor from "../../../db/mongoDbInteractor"; 4 | import { WithId } from "mongodb"; 5 | import { RESOURCE_COLLECTION } from "../../../models/constants"; 6 | 7 | export type ResourceResponse = { 8 | data?: WithId; 9 | error?: string; 10 | }; 11 | 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | const id = req.query["resourceId"]; 17 | if (!id || Array.isArray(id)) { 18 | return res 19 | .status(401) 20 | .json({ 21 | error: `Invalid value for required string query param eventId: received ${id}`, 22 | }); 23 | } 24 | const resource = await getResource(id); 25 | return resource 26 | ? res.status(200).json({ data: resource }) 27 | : res.status(404).json({ error: `Resource ${id} not found` }); 28 | } 29 | 30 | async function getResource(id: string): Promise | null> { 31 | return await mongoDbInteractor.getDocument(RESOURCE_COLLECTION, id); 32 | } 33 | 34 | export const config = { 35 | api: { 36 | bodyParser: { 37 | sizeLimit: '4mb', 38 | }, 39 | }, 40 | } -------------------------------------------------------------------------------- /hooks/useResources.ts: -------------------------------------------------------------------------------- 1 | import useSWRImmutable from 'swr/immutable' 2 | import { ResourceData, ResourcesResponse } from '../pages/api/resources' 3 | 4 | // hook for getting resources from api to display in frontend 5 | 6 | // useSWR will pass loading/error state if data not retrieved 7 | // when resources are ready (not in state isLoading or isError) the data will be propogated 8 | // to components using useEvents and can be accessed in events field 9 | 10 | // data is cached so request is not sent to api every time page is loaded 11 | export const useResources = (encryptedQuizResponse: string = "") => { 12 | const requestBody = encryptedQuizResponse ? JSON.stringify({data: encryptedQuizResponse}) : null 13 | const requestSettings = { method: 'POST', body: requestBody, headers: {'Content-Type': 'application/json'}} 14 | const resourcesFetcher = async () : Promise => { 15 | const response : Response = await fetch('/api/resources', requestSettings) 16 | const resources : ResourcesResponse = await response.json() 17 | return resources.data 18 | } 19 | const {data, error} = useSWRImmutable(`/api/resources`, resourcesFetcher) 20 | return { requestedResources: data ? data!.requested : [], additionalResources: data?.additional, isLoading: !error && !data, error } 21 | } 22 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { AppContext } from '../context/context' 4 | import { useState } from 'react' 5 | import { createTheme, NextUIProvider, Text } from "@nextui-org/react" 6 | import { appWithTranslation } from 'next-i18next'; 7 | 8 | const theme = createTheme({ 9 | type: "light", 10 | theme: { 11 | colors: { 12 | primary: '#219EA4', 13 | link: '#219EA4', 14 | brandBlue: '#22A6DD', 15 | brandPurple: '#630DF0', 16 | brandOrange: '#F0880D', 17 | brandGreen: '#55C130', 18 | secondaryText: '#7E7C7C' 19 | }, 20 | space: {}, 21 | fonts: { 22 | primary: 'Raleway' 23 | } 24 | } 25 | }) 26 | 27 | function MyApp({ Component, pageProps }: AppProps) { 28 | // default is a hashed empty object, which the API will respond to with a list of all resources 29 | const [encryptedQuizResponse, changeEncryptedQuizResponse] = useState("") 30 | return ( 31 | 32 |
33 | 34 | 35 | 36 |
37 |
38 | ) 39 | 40 | } 41 | 42 | export default appWithTranslation(MyApp) 43 | -------------------------------------------------------------------------------- /public/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/directory/sidebar/SidebarCategories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, Grid, Checkbox, Spacer } from "@nextui-org/react"; 3 | import styles from "./FilterSidebar.module.css"; 4 | import ClearFieldsButton from './ClearFieldsButton'; 5 | 6 | interface SidebarCategoriesProps { 7 | categories: string[]; 8 | setSelectedCategories(s: string[]): void 9 | selectedCategories: string[]; 10 | } 11 | 12 | export const SidebarCategories: React.FC = (props: SidebarCategoriesProps) => { 13 | return ( 14 | 15 | Categories 16 | 17 | props.setSelectedCategories(e)} 22 | > 23 | {props.categories.map(category => ( 24 | 25 | {category} 26 | 27 | ))} 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /models/constants.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | 3 | // TODO: replace all instances of the collection name (and other constants) with the constants defined here 4 | export const ADMIN_COLLECTION = "admins" 5 | export const RESOURCE_COLLECTION = "resources3" 6 | export const QUIZ_RESPONSE_ENCRYPTION_PASSPHRASE = "Secret Passphrase" 7 | export const RESOURCE_SEARCH_PLACEHOLDER_TEXT = "Search Resources" 8 | export const INVITE_COLLECTION = "invites" 9 | export const FORGOT_PASSWORD_COLLECTION = "forgotPasswordSessions" 10 | export const LOCALHOST = "localhost:3000" 11 | 12 | export const LOGIN_IRON_OPTION = function(req : NextApiRequest, res: NextApiResponse) { 13 | let ttl = 60*60*24 14 | if (req.body["keepSignedIn"]) { 15 | ttl *= 30 16 | } 17 | return { 18 | cookieName: "MY_APP_COOKIE", 19 | password: process.env.COOKIES_PASSWORD!, 20 | ttl, 21 | // secure: true should be used in production (HTTPS) but can't be used in development (HTTP) 22 | cookieOptions: { 23 | secure: process.env.NODE_ENV === "production", 24 | }, 25 | } 26 | } 27 | 28 | export const NORMAL_IRON_OPTION = { 29 | cookieName: "MY_APP_COOKIE", 30 | password: process.env.COOKIES_PASSWORD!, 31 | // secure: true should be used in production (HTTPS) but can't be used in development (HTTP) 32 | cookieOptions: { 33 | secure: process.env.NODE_ENV === "production", 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown, Navbar, Image } from "@nextui-org/react" 2 | import { useRouter } from "next/router"; 3 | import { useState } from "react"; 4 | 5 | const LanguageSelect = () => { 6 | const router = useRouter() 7 | const { pathname, asPath, query } = router 8 | 9 | const changeLanguage = (key: string) => { 10 | console.log(key) 11 | setLanguage(key) 12 | router.push({ pathname, query }, asPath, { locale: key.toLowerCase() }) 13 | } 14 | 15 | const [language, setLanguage] = useState(router.locale?.toUpperCase()); 16 | 17 | return ( 18 | 19 | 20 | Select language 21 |
{/* a spacer because normal padding doesn't seem to work*/} 22 | {language} 23 |
24 | { 29 | changeLanguage(key.toString()) 30 | }}> 31 | EN 32 | FR 33 | ES 34 | 35 |
36 | ) 37 | } 38 | 39 | export default LanguageSelect -------------------------------------------------------------------------------- /pages/api/events/index.ts.no: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import type { Event, EventInfo } from "../../../models/types"; 3 | import FirebaseInteractor from "../../../db/firebaseInteractor"; 4 | import { WhereQuery } from "../../../db/firebaseInteractor"; 5 | import { eventConverter } from "../../../db/converters"; 6 | import { EventResponse } from "./[eventId]"; 7 | 8 | export type EventsResponse = { 9 | data?: Event[]; 10 | error?: string; 11 | }; 12 | 13 | export default async function handler( 14 | req: NextApiRequest, 15 | res: NextApiResponse 16 | ) { 17 | if (req.method === "GET") { 18 | const eventListData = await getEvents([]); 19 | res.status(200).json({ data: eventListData }); 20 | } else if (req.method === "POST") { 21 | // TODO: validate body sent in post request 22 | const requestBody: EventInfo = req.body; 23 | const newEvent = await createEvent(requestBody); 24 | res.status(201).json({ data: newEvent }); 25 | } else { 26 | res.status(405).json({ error: "unsupported" }); 27 | } 28 | } 29 | 30 | async function getEvents(queryParams: WhereQuery[]): Promise { 31 | return await FirebaseInteractor.getCollectionData( 32 | "events", 33 | eventConverter, 34 | queryParams 35 | ); 36 | } 37 | 38 | async function createEvent(event: EventInfo): Promise { 39 | return await FirebaseInteractor.createDocument( 40 | "events", 41 | event, 42 | eventConverter 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/admin/dashboard/incomeRangeContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react" 2 | import {IncomeRange} from "../../../models/types" 3 | import IncomeRangeRow from "./incomeRangeRow" 4 | import {Button, Text} from "@nextui-org/react" 5 | 6 | interface IncomeRangeContainerProps { 7 | ranges?: IncomeRange[] 8 | editing: boolean 9 | onChange: (mutator: (ranges?: IncomeRange[]) => IncomeRange[] | undefined) => void 10 | } 11 | export default function IncomeRangeContainer({ranges, editing, onChange} : IncomeRangeContainerProps) { 12 | 13 | const addSize = () => { 14 | onChange((ranges) => { 15 | if(ranges === undefined) ranges = [] 16 | 17 | //taken from https://stackoverflow.com/a/12502559 18 | let id = Math.random().toString(36).slice(2); 19 | while(ranges.map(r => r.id).includes(id)) { 20 | id = Math.random().toString(36).slice(2) 21 | } 22 | 23 | ranges.push({minimum: 0, maximum: 0, id}) 24 | 25 | return ranges 26 | }) 27 | } 28 | 29 | const addButton = 32 | 33 | return <> 34 | {ranges?.map((range,i) => ) ?? No income ranges specified.} 35 | {editing && addButton} 36 | 37 | 38 | } -------------------------------------------------------------------------------- /pages/api/admin/resetPassword.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import { Admin, PasswordReset, ResponseMessage } from "../../../models/types"; 3 | import { isPasswordResetValid } from "../../../util/utils"; 4 | import mongoDbInteractor from "../../../db/mongoDbInteractor"; 5 | import { ADMIN_COLLECTION, FORGOT_PASSWORD_COLLECTION } from "../../../models/constants"; 6 | 7 | const handler : NextApiHandler = async (req, res) => { 8 | if(req.method === "POST" && typeof req.body['resetId'] === "string" && typeof req.body['hashedPassword'] === "string") { 9 | 10 | const resetId = req.body['resetId'] 11 | const hashedPassword = req.body["hashedPassword"] 12 | 13 | if(!await isPasswordResetValid(resetId)) { 14 | res.status(404).json({message: 'Password reset link has expired. Please try again.'}) 15 | return 16 | } 17 | 18 | const passwordReset = await mongoDbInteractor.getDocumentByDirectId(FORGOT_PASSWORD_COLLECTION, resetId) 19 | 20 | 21 | await Promise.all([ 22 | mongoDbInteractor.updateDocument(ADMIN_COLLECTION, {email: passwordReset.email}, {$set: {hashedPassword}}), 23 | mongoDbInteractor.deleteDocument(FORGOT_PASSWORD_COLLECTION, {_id: resetId})]) 24 | 25 | res.status(201).json({message: `Password successfully updated for ${passwordReset.email}.`}); 26 | 27 | } else { 28 | res.status(404).json({message: "Invalid request method."}) 29 | } 30 | } 31 | 32 | export default handler -------------------------------------------------------------------------------- /pages/api/admin/forgotPassword.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next" 2 | import { createSingleUseLink, isValidEmail } from "../../../util/utils"; 3 | import mongoDbInteractor from "../../../db/mongoDbInteractor"; 4 | import { ADMIN_COLLECTION, FORGOT_PASSWORD_COLLECTION, LOCALHOST } from "../../../models/constants"; 5 | import { Admin, ResponseMessage } from "../../../models/types"; 6 | import { sendEmail } from "../../../util/mailService"; 7 | 8 | const handler : NextApiHandler = async (req, res) => { 9 | const email: string = req.body["email"] ?? ""; 10 | 11 | if(!isValidEmail(email)) { 12 | res.status(401).json({message: "E-mail is invalid."}) 13 | return 14 | } 15 | 16 | const accounts = await mongoDbInteractor.getDocuments(ADMIN_COLLECTION, {email}) 17 | 18 | if(accounts.length <= 0) { 19 | res.status(401).json({message: "Sorry, we couldn't find an account associated with this email address."}) 20 | return 21 | } 22 | 23 | const link = await createSingleUseLink(FORGOT_PASSWORD_COLLECTION, req.headers.host ?? LOCALHOST, "/admin/resetPassword/",{email}) 24 | 25 | await sendEmail(email, "Request to reset password",`

There was a request to reset the password to your HappyEastie admin account. If this was not you, please disregard this email. You can reset your password at the following link:

26 |
27 | ${link}`) 28 | 29 | res.status(201).json({message: `E-mail sent to ${email}. Please check your spam or junk folder if you did not receive anything.`}) 30 | } 31 | 32 | export default handler -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import InfoSection from '../components/home/InfoSection' 3 | import TrendingSection, { EastieEvent } from '../components/home/TrendingSection' 4 | import Layout from '../components/Layout' 5 | import mongoDbInteractor from '../db/mongoDbInteractor' 6 | import { Resource } from '../models/types' 7 | import { RESOURCE_COLLECTION } from '../models/constants' 8 | 9 | const getServerSideProps : GetServerSideProps = async () => { 10 | try{ 11 | const trendingResources = (await mongoDbInteractor.getDocuments(RESOURCE_COLLECTION, {"trendingInfo.isTrending": true})).sort((a,b) => a.trendingInfo!.trendingDate.getTime() >= b.trendingInfo!.trendingDate.getTime()? -1 : 1) //these should have trending info because we filtered on them 12 | 13 | const events : EastieEvent[] = trendingResources.map(r => ({ 14 | id: r._id.toString(), 15 | name: r.name, 16 | summary: r.summary, 17 | imageFilename: r.headerImageUrl ?? "", 18 | tags: r.category 19 | })) 20 | 21 | return { 22 | props: { 23 | events 24 | } 25 | } 26 | } catch(e) { 27 | console.error('Could not get trending resources.') 28 | 29 | return { 30 | props: { 31 | 32 | } 33 | } 34 | } 35 | } 36 | 37 | const HomePage = ({events}: {events?: EastieEvent[]}) => { 38 | return ( 39 | 40 | 41 | ) 42 | } 43 | 44 | export {getServerSideProps} 45 | export default HomePage -------------------------------------------------------------------------------- /components/tag.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, Image, Row } from "@nextui-org/react"; 3 | 4 | type TagProps = { 5 | text: string; 6 | editing?: boolean; 7 | colorful?: boolean; 8 | onXClick?: (s: string) => void; 9 | }; 10 | 11 | export default function Tag({ 12 | text, 13 | editing = false, 14 | colorful = true, 15 | onXClick, 16 | }: TagProps) { 17 | const tagColor = (tagName: string) => { 18 | const tagColors = ["#C7F1FF", "#E1CFFF", "#DFFCD2", "#FDE5FF", "#FFE9CF"]; 19 | const index = tagName.length % tagColors.length; 20 | return tagColors[index]; 21 | }; 22 | 23 | const borderColor = (tagName: string) => { 24 | const tagColors = ["#005A8F", "#6200FF", "#32631D", "#9800A3", "#854700"]; 25 | const index = tagName.length % tagColors.length; 26 | return tagColors[index]; 27 | }; 28 | 29 | return ( 30 | 46 | {text} 47 | {/* Show an X if the tag is editable */} 48 | {editing && ( 49 | { 53 | onXClick?.(text); 54 | }} 55 | /> 56 | )} 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pages/resources/[resourceId].tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useResource } from "../../hooks/useResource"; 3 | import { useRouter } from "next/router"; 4 | import { ResourceHeader } from "../../components/resources/ResourceHeader"; 5 | import ReactMarkdown from "react-markdown"; 6 | import { Image, Spacer, Text } from "@nextui-org/react"; 7 | import styles from "./[resourceId].module.css"; 8 | import { ResourceDescription } from "../../components/resources/ResourceDescription"; 9 | import Loading from "../../components/Loading"; 10 | import Layout from "../../components/Layout"; 11 | 12 | const ResourcePageContent: NextPage = () => { 13 | const router = useRouter(); 14 | const { resourceId } = router.query; 15 | const { resource, isLoading, error } = useResource(resourceId); 16 | 17 | if (error) return
{error.message}
; 18 | if (isLoading) return ; 19 | if (!resource) 20 | return
Internal server error: invalid resource loaded
; 21 | 22 | 23 | return ( 24 | <> 25 | 28 | Resource header image 30 | 31 |
32 | 33 | 34 | 35 |
36 | 37 | ); 38 | }; 39 | 40 | const ResourcePage = () => { 41 | return 42 | 43 | 44 | } 45 | export default ResourcePage; 46 | -------------------------------------------------------------------------------- /public/emailfilled.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --brand-primary: #219EA4; 3 | --primary-text: black; 4 | --secondary-text: #7E7C7C; 5 | --brand-blue: #22A6DD; 6 | --brand-purple: #630DF0; 7 | --brand-orange: #F0880D; 8 | --brand-green: #55C130; 9 | --brand-light-blue: #F4F8FA; 10 | } 11 | 12 | html, 13 | body { 14 | padding: 0; 15 | background-color: white; 16 | margin: 0; 17 | font-family: 'Raleway'; 18 | } 19 | 20 | @font-face { 21 | font-family: 'Raleway'; 22 | src: url(../fonts/Raleway-VariableFont_wght.ttf); 23 | } 24 | 25 | * { 26 | box-sizing: border-box; 27 | } 28 | 29 | h1 { 30 | font-family: 'Raleway'; 31 | font-size: 35px; 32 | font-weight: 600; 33 | color: black; 34 | margin-top: 0; 35 | margin-bottom: 0; 36 | letter-spacing: normal; 37 | } 38 | 39 | h3 { 40 | font-size: 25px; 41 | font-family: 'Raleway'; 42 | font-weight: 600; 43 | color: var(--primary-text); 44 | letter-spacing: normal; 45 | } 46 | 47 | table { 48 | max-width: 800px; 49 | width: 90%; 50 | border-collapse: collapse; 51 | text-align: center; 52 | } 53 | 54 | td { 55 | border: 0.75px solid var(--secondary-text); 56 | padding: 40px 0px; 57 | } 58 | 59 | a { 60 | color: var(--brand-primary); 61 | } 62 | 63 | a:hover { 64 | text-decoration: underline; 65 | } 66 | 67 | p { 68 | font-size: 16px; 69 | line-height: 19px; 70 | font-family: 'Raleway'; 71 | font-weight: 400; 72 | color: var(--secondary-text); 73 | letter-spacing: normal; 74 | } 75 | 76 | li { 77 | font-size: 20px; 78 | font-family: 'Raleway'; 79 | font-weight: 400; 80 | color: var(--secondary-text); 81 | list-style-type: disc; 82 | } 83 | 84 | ul > li > ul > li { 85 | list-style: square; 86 | } 87 | 88 | html { 89 | overflow-x: hidden; 90 | } -------------------------------------------------------------------------------- /util/utils.ts: -------------------------------------------------------------------------------- 1 | import { FORGOT_PASSWORD_COLLECTION, INVITE_COLLECTION } from "../models/constants"; 2 | import { Invite, PasswordReset } from "../models/types"; 3 | import mongoDbInteractor from "../db/mongoDbInteractor"; 4 | import crypto from "crypto" 5 | 6 | export async function isInviteValid(inviteId: string) { 7 | return isSingleUseSessionValid(INVITE_COLLECTION, inviteId) 8 | } 9 | 10 | export async function isPasswordResetValid(resetId: string) { 11 | return isSingleUseSessionValid(FORGOT_PASSWORD_COLLECTION, resetId) 12 | } 13 | async function isSingleUseSessionValid(collection: string, id: string){ 14 | const sessions = await mongoDbInteractor.getDocuments(collection,{_id: id} as any) //seems to be a typescript bug, T is just here to enforce safety 15 | //see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/46375 16 | 17 | return sessions.length > 0 && sessions[0].expiration.getTime() >= Date.now() 18 | } 19 | 20 | //apparently email address validation is a pain. this regex taken from here: 21 | //https://stackoverflow.com/a/1373724 22 | export const emailRegex = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/ 23 | 24 | export function isValidEmail(email: string) { 25 | return emailRegex.test(email) 26 | } 27 | 28 | export async function createSingleUseLink(collectionName: string,host: string, path: string, otherProps: {[key: string]: unknown} = {}) { 29 | const _id = crypto.randomUUID() 30 | //link expires after 1 hour 31 | const expiration = new Date(Date.now() + 1*60*60*1000) 32 | const link = `http://${host}${path}${_id}` 33 | 34 | const session = { 35 | _id, expiration, ...otherProps 36 | } 37 | await mongoDbInteractor.createDocument(session,collectionName) 38 | 39 | return link 40 | } -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layout" 2 | import { Container, Spacer, Text } from "@nextui-org/react" 3 | import styles from './about.module.css' 4 | const AboutPage = () => { 5 | return ( 6 | 7 | 8 | About Us 9 | 10 | HappyEastie is a tool that helps match you with helpful resources that you qualify for. Our mission is to make this search as easy as possible, creating a community hub for information. 11 | 12 | You can: 13 |
    14 |
  • 15 | Follow the path of answering questions to get resources that you’re eligible for 16 |
  • 17 |
  • 18 | 19 | Discover new and relevant events in the community 20 | 21 |
  • 22 |
  • 23 | Manually search our database for what you’re looking for 24 |
  • 25 |
26 | On The Way: 27 |
    28 |
  • 29 | Multiple Language Support 30 |
  • 31 |
  • 32 | 33 | Accessibility/ADA Info for Resource Sites 34 | 35 |
  • 36 |
  • 37 | Linguistic Access Info for Resource Sites 38 |
  • 39 |
  • 40 | Resource Site Hours 41 |
  • 42 |
  • 43 | Suggestions for Traveling to a Resource Site 44 |
  • 45 |
46 |
47 |
) 48 | } 49 | export default AboutPage -------------------------------------------------------------------------------- /components/footer/FooterLinks.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Link, Spacer } from "@nextui-org/react"; 2 | import styles from "./Footer.module.css" 3 | import NextLink from "next/link" 4 | 5 | type LinkObj = { 6 | title: string, 7 | href: string 8 | } 9 | 10 | const items: LinkObj[] = [ 11 | { title: "Quiz", href: "/quiz" }, 12 | { title: "Community Events", href: "/future" }, 13 | { title: "Resources", href: "/directory" }, 14 | { title: "About Us", href: "/about" } 15 | ] 16 | 17 | export default function FooterLinks() { 18 | return ( 19 | 32 | {mapLinksToColumn(items).map((i: LinkPair) => ( 33 |
34 | 35 | {i.firstLink.title} 36 | 37 | 38 | {i.secondLink && 39 | 40 | {i.secondLink.title} 41 | 42 | } 43 |
44 | ))} 45 |
46 | ) 47 | } 48 | 49 | type LinkPair = { 50 | firstLink: LinkObj, 51 | secondLink: LinkObj | undefined 52 | } 53 | 54 | function mapLinksToColumn(arr: LinkObj[]): LinkPair[] { 55 | let result: LinkPair[] = [] 56 | for (let i = 0; i < arr.length; i += 2) { 57 | if (i + 1 < arr.length) { 58 | result.push({ "firstLink": arr[i], "secondLink": arr[i + 1] }) 59 | } else { 60 | result.push({ "firstLink": arr[i], "secondLink": undefined }) 61 | } 62 | } 63 | 64 | return result 65 | } 66 | -------------------------------------------------------------------------------- /pages/api/events/[eventId].ts.notdealingwiththis: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import type { Event, EventInfo } from "../../../models/types"; 3 | import FirebaseInteractor from "../../../db/firebaseInteractor"; 4 | import { eventConverter } from "../../../db/converters"; 5 | 6 | export type EventResponse = { 7 | data?: Event; 8 | error?: string; 9 | }; 10 | 11 | export type DeleteEventResponse = { 12 | message?: string; 13 | error?: string; 14 | }; 15 | 16 | export default async function handler( 17 | req: NextApiRequest, 18 | res: NextApiResponse 19 | ) { 20 | const id = req.query["eventId"]; 21 | if (!id || Array.isArray(id)) { 22 | return res 23 | .status(401) 24 | .json({ 25 | error: `Invalid value for required query param eventId: received ${id}`, 26 | }); 27 | } 28 | if (req.method === "GET") { 29 | const event = await getEvent(id); 30 | return event 31 | ? res.status(200).json({ data: event }) 32 | : res.status(404).json({ error: `Resource ${id} not found` }); 33 | } else if (req.method === "PUT") { 34 | // TODO: error handling for invalid event passed here? 35 | const event: EventInfo = req.body; 36 | const updatedEvent = await modifyEvent(event, id); 37 | res.status(200).json({ data: updatedEvent }); 38 | } else if (req.method === "DELETE") { 39 | await deleteEvent(id); 40 | res.status(200).json({ message: `Resource id ${id} deleted successfully` }); 41 | } else { 42 | res.status(405).json({ error: "unsupported" }); 43 | } 44 | } 45 | 46 | async function getEvent(id: string): Promise { 47 | return await FirebaseInteractor.getDocumentById("events", id, eventConverter); 48 | } 49 | 50 | async function modifyEvent(newEvent: EventInfo, id: string): Promise { 51 | return { 52 | id, 53 | ...(await FirebaseInteractor.updateDocument( 54 | "events", 55 | newEvent, 56 | id, 57 | eventConverter 58 | )), 59 | }; 60 | } 61 | 62 | async function deleteEvent(id: string): Promise { 63 | FirebaseInteractor.deleteDocument("events", id); 64 | } 65 | -------------------------------------------------------------------------------- /public/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/admin/dashboard/InputField.tsx: -------------------------------------------------------------------------------- 1 | import { FormElement, Input, Text, Textarea } from "@nextui-org/react"; 2 | import ReactMarkdown from "react-markdown"; 3 | 4 | type InputFieldProps = { 5 | name: string; 6 | editing: boolean; 7 | placeholder: string; 8 | fullWidth?: boolean; 9 | size?: "xs" | "sm" | "md" | "lg" | "xl"; 10 | multiLine?: boolean; 11 | value?: string; 12 | onChange?: (e: React.ChangeEvent) => void; 13 | }; 14 | const InputField = ({ 15 | name, 16 | placeholder, 17 | fullWidth, 18 | size = "xs", 19 | editing, 20 | value, 21 | multiLine, 22 | onChange 23 | }: InputFieldProps) => 24 | multiLine ? ( 25 |