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
Someone at HappyEastie has invited you to become an admin. Visit the link below or copy and paste it into your browser in order to create an account. The link expires after 1 hour.
37 |
38 | ${link}`);
39 | } catch (e) {
40 | console.error(e)
41 | res.status(500).json({message: `An error occured: ${JSON.stringify(e)}`})
42 | return
43 | }
44 |
45 | res.status(201).json({message: `E-mail invite sent to ${email}. Ensure they check their spam or junk folder in case they do not see it, and ensure their e-mail address was typed correctly.`});
46 |
47 | } else {
48 | res.status(404).json({message: "Invalid request method."})
49 | }
50 | }, NORMAL_IRON_OPTION);
--------------------------------------------------------------------------------
/fonts/README.txt:
--------------------------------------------------------------------------------
1 | Raleway Variable Font
2 | =====================
3 |
4 | This download contains Raleway as both variable fonts and static fonts.
5 |
6 | Raleway is a variable font with this axis:
7 | wght
8 |
9 | This means all the styles are contained in these files:
10 | Raleway-VariableFont_wght.ttf
11 | Raleway-Italic-VariableFont_wght.ttf
12 |
13 | If your app fully supports variable fonts, you can now pick intermediate styles
14 | that aren’t available as static fonts. Not all apps support variable fonts, and
15 | in those cases you can use the static font files for Raleway:
16 | static/Raleway-Thin.ttf
17 | static/Raleway-ExtraLight.ttf
18 | static/Raleway-Light.ttf
19 | static/Raleway-Regular.ttf
20 | static/Raleway-Medium.ttf
21 | static/Raleway-SemiBold.ttf
22 | static/Raleway-Bold.ttf
23 | static/Raleway-ExtraBold.ttf
24 | static/Raleway-Black.ttf
25 | static/Raleway-ThinItalic.ttf
26 | static/Raleway-ExtraLightItalic.ttf
27 | static/Raleway-LightItalic.ttf
28 | static/Raleway-Italic.ttf
29 | static/Raleway-MediumItalic.ttf
30 | static/Raleway-SemiBoldItalic.ttf
31 | static/Raleway-BoldItalic.ttf
32 | static/Raleway-ExtraBoldItalic.ttf
33 | static/Raleway-BlackItalic.ttf
34 |
35 | Get started
36 | -----------
37 |
38 | 1. Install the font files you want to use
39 |
40 | 2. Use your app's font picker to view the font family and all the
41 | available styles
42 |
43 | Learn more about variable fonts
44 | -------------------------------
45 |
46 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
47 | https://variablefonts.typenetwork.com
48 | https://medium.com/variable-fonts
49 |
50 | In desktop apps
51 |
52 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc
53 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
54 |
55 | Online
56 |
57 | https://developers.google.com/fonts/docs/getting_started
58 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
59 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
60 |
61 | Installing fonts
62 |
63 | MacOS: https://support.apple.com/en-us/HT201749
64 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
65 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
66 |
67 | Android Apps
68 |
69 | https://developers.google.com/fonts/docs/android
70 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
71 |
72 | License
73 | -------
74 | Please read the full license text (OFL.txt) to understand the permissions,
75 | restrictions and requirements for usage, redistribution, and modification.
76 |
77 | You can use them in your products & projects – print or digital,
78 | commercial or otherwise.
79 |
80 | This isn't legal advice, please consider consulting a lawyer and see the full
81 | license for all details.
82 |
--------------------------------------------------------------------------------
/components/resources/ResourceHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Resource } from "../../models/types";
2 | import { Button, Grid, Image, Text } from "@nextui-org/react";
3 | import Tag from "../tag";
4 | import styles from "./ResourceHeader.module.css";
5 | import { useRouter } from "next/router";
6 |
7 | interface ResourceHeaderProps {
8 | resource: Resource;
9 | }
10 |
11 | export const ResourceHeader: React.FC = (
12 | props: ResourceHeaderProps
13 | ) => {
14 | const router = useRouter();
15 |
16 | const phoneNumberClicked = () => {
17 | router.push(`tel:${props.resource.phone}`);
18 | };
19 |
20 | const emailClicked = () => {
21 | router.push(`mailto:${props.resource.email}`);
22 | };
23 |
24 | const shareClicked = () => {
25 | const url = props.resource.website;
26 | if (url) {
27 | router.push(url);
28 | }
29 | };
30 |
31 | return (
32 | <>
33 | {props.resource.name}
34 |
35 |
36 | {props.resource.category?.map((c, i) => (
37 |
38 |
39 |
40 | ))}
41 |
42 |
43 |
44 |
45 |
55 | }
56 | rounded={true}
57 | className={styles.button}
58 | onClick={phoneNumberClicked}
59 | >
60 |
61 |
62 |
63 |
73 | }
74 | rounded={true}
75 | className={styles.button}
76 | onClick={emailClicked}
77 | >
78 |
79 |
80 |
81 |
91 | }
92 | rounded={true}
93 | className={styles.button}
94 | onClick={shareClicked}
95 | >
96 |
97 |
98 | >
99 | );
100 | };
101 |
--------------------------------------------------------------------------------
/hooks/useResource.ts:
--------------------------------------------------------------------------------
1 | import { WithId } from 'mongodb'
2 | import { useContext } from 'react'
3 | import useSWRImmutable from 'swr/immutable'
4 | import { AppContext } from '../context/context'
5 | import { Resource } from '../models/types'
6 | import { ResourceData, ResourcesResponse } from '../pages/api/resources'
7 | import { ResourceResponse } from '../pages/api/resources/[resourceId]'
8 |
9 | // hook for getting a single resource from api to display in frontend
10 |
11 | // useSWR will pass loading/error state if data not retrieved
12 | // this hook is designed to first look in the cached resources to see if the resource has already been fetched
13 | // if it is not, then it will fetch directly from the api
14 |
15 | // data is cached so request is not sent to api every time page is loaded
16 | export const useResource = (id: string | string[] | undefined) => {
17 | const quizState = useContext(AppContext)
18 | const requestBody = quizState.encryptedQuizResponse ? JSON.stringify({data: quizState.encryptedQuizResponse}) : null
19 | const requestSettings = { method: 'POST', body: requestBody, headers: {'Content-Type': 'application/json'}}
20 | const resourcesFetcher = async () : Promise => {
21 | const response : Response = await fetch('/api/resources', requestSettings)
22 | const resources : ResourcesResponse = await response.json()
23 | return resources.data
24 | }
25 | const {data: resourcesData}= useSWRImmutable('/api/resources', resourcesFetcher)
26 |
27 | const resourceFetcher = async () : Promise> => {
28 | if (!id || Array.isArray(id)) {
29 | throw Error(`Invalid resource id type: received ${id} instead of string`)
30 | }
31 | const resource : WithId | undefined = resourcesData?.requested.concat(resourcesData.additional).find(r => r._id.toString() === id)
32 | if (resource) {
33 | return resource
34 | } else {
35 | const response : Response = await fetch(`/api/resources/${id}`)
36 | const responseJson : ResourceResponse = await response.json()
37 | if (responseJson.data) {
38 | return responseJson.data
39 | } else {
40 | throw new Error(responseJson.error)
41 | }
42 | }
43 | }
44 | const resourceKeyFunction = () : string => {
45 | if (resourcesData != undefined) {
46 | return `/api/resources/${id}`
47 | } else {
48 | throw new Error("Don't proceed")
49 | }
50 | }
51 | // if the resources are not ready, the useSWR key function will throw an error
52 | // and the resource fetcher will not be called
53 | const {data, error} = useSWRImmutable, Error>(resourceKeyFunction, resourceFetcher, {shouldRetryOnError: false})
54 | return { resource: data, isLoading: !error && !data, error }
55 | }
56 |
--------------------------------------------------------------------------------
/pages/admin/dashboard/invite.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Container, Divider, FormElement, Input, Row, Spacer, Text } from "@nextui-org/react";
2 | import { AdminDashboardHeader } from "../../../components/admin/dashboard/adminDashboardHeader";
3 | import { withIronSessionSsr } from "iron-session/next";
4 | import { NORMAL_IRON_OPTION } from "../../../models/constants";
5 | import { FormEvent, useState } from "react";
6 |
7 | export const getServerSideProps = withIronSessionSsr(ctx => {
8 | const user = ctx.req.session.user;
9 |
10 | if (!user || user.isAdmin !== true) {
11 | return {
12 | redirect: {
13 | destination: '/admin',
14 | permanent: false,
15 | }
16 | };
17 | } else {
18 | return {
19 | props: {}
20 | }
21 | }
22 | }, NORMAL_IRON_OPTION)
23 | export default function InvitePage() {
24 | const [email, setEmail] = useState("")
25 | const [isSending, setIsSending] = useState(false)
26 | const [responseMessage, setResponseMessage] = useState("")
27 | const [isError, setIsError] = useState(false)
28 | return <>
29 |
30 |
31 |
32 | Send Admin Invite
33 |
34 |
35 |
36 |
39 | Recipient Email
40 |
41 | {
42 | setEmail(e.currentTarget.value)
43 | }}/>
44 |
71 |
72 | {responseMessage && {responseMessage}}
73 |
74 |
75 | >
76 | }
--------------------------------------------------------------------------------
/components/quiz/QuizResultsDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { Progress, Row } from "@nextui-org/react";
2 | import { useEffect, useState } from "react";
3 | import styles from "./QuizResultsDisplay.module.css";
4 | import { ResourcesDisplay } from "../directory/ResourcesDisplay";
5 | import { Grid, Spacer, Text, Image } from "@nextui-org/react";
6 | import { HelpTooltip } from "../HelpTooltip";
7 | import { ResourcesResponse } from "../../pages/api/resources";
8 | import { Resource } from "../../models/types";
9 | import { WithId } from "mongodb";
10 |
11 | interface QuizResultsDisplayProps {
12 | encryptedQuizResponse: string;
13 | }
14 |
15 | export const QuizResultsDisplay: React.FC = (props: QuizResultsDisplayProps) => {
16 | const [requestedResources, setRequestedResources] = useState[]>([]);
17 | const [additionalResources, setAdditionalResources] = useState[]>([]);
18 |
19 | useEffect(() => {
20 | const fetchFilteredResources = async () => {
21 | const requestBody = JSON.stringify({ data: props.encryptedQuizResponse });
22 | const requestSettings = {
23 | method: "POST",
24 | body: requestBody,
25 | headers: { "Content-Type": "application/json" },
26 | };
27 | const response: Response = await fetch("/api/resources", requestSettings);
28 | const resources: ResourcesResponse = await response.json();
29 | setRequestedResources(resources.data.requested);
30 | setAdditionalResources(resources.data.additional);
31 | };
32 |
33 | fetchFilteredResources().catch(console.error);
34 | }, []);
35 |
36 | return (
37 |
43 |
44 |
45 |
46 |
47 | Your Matches
48 |
49 |
50 |
51 |
52 |
53 | In order of best fit for you.
54 |
55 |
56 |
57 |
58 |
59 |
60 | {additionalResources.length > 0 && (
61 | <>
62 | Additional Resources
63 |
64 | Services you also qualify for.
65 |
66 |
67 |
68 |
69 | >
70 | )}
71 |
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/components/admin/dashboard/tagSelector.tsx:
--------------------------------------------------------------------------------
1 | import { FormElement, Grid, Input } from "@nextui-org/react";
2 | import Tag from "../../tag";
3 | import { useEffect, useState } from "react";
4 |
5 | type TagSelectorProps = {
6 | name: string;
7 | tags: string[];
8 | editing: boolean;
9 | colorful?: boolean;
10 | onChange?: (e: React.ChangeEvent) => void;
11 | fetchDatalist?: () => Promise;
12 | };
13 |
14 | export const TagSelector = ({
15 | name,
16 | tags,
17 | editing,
18 | colorful = true,
19 | onChange,
20 | fetchDatalist,
21 | }: TagSelectorProps) => {
22 | const [inputValue, setInputValue] = useState("");
23 | const [dropdownTags, setDropdownTags] = useState([]);
24 | useEffect(() => {
25 | fetchDatalist?.().then((data) => setDropdownTags(data));
26 | }, [fetchDatalist]);
27 | const handleXClick = (tagName: string) => {
28 | // Remove the tag from the list of tags
29 | onChange?.({
30 | target: {
31 | name,
32 | value: tags.filter((tag) => tag !== tagName),
33 | },
34 | } as unknown as React.ChangeEvent);
35 | };
36 |
37 | const addTag = (newTagName: string) => {
38 | // Don't add the tag if it's already in the list
39 | if (tags.includes(newTagName)) return;
40 |
41 | // Add the tag to the list of tags
42 | onChange?.({
43 | target: {
44 | name,
45 | value: [...tags, newTagName],
46 | },
47 | } as unknown as React.ChangeEvent);
48 | };
49 |
50 | return (
51 |
156 | );
157 | };
158 |
159 | export default LogIn;
160 |
--------------------------------------------------------------------------------
/fonts/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name "Raleway".
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/pages/admin/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Container,
4 | FormElement,
5 | Grid,
6 | Link,
7 | Row,
8 | Spacer,
9 | Image
10 | } from "@nextui-org/react";
11 | import { WithId } from "mongodb";
12 | import { ChangeEvent, useEffect, useState } from "react";
13 | import { AdminDashboardHeader } from "../../../components/admin/dashboard/adminDashboardHeader";
14 | import { AdminDashboardSearch } from "../../../components/admin/dashboard/adminDashboardSearch";
15 | import { ResourceRow } from "../../../components/admin/dashboard/resourceRow";
16 | import { Resource } from "../../../models/types";
17 | import { withIronSessionSsr } from "iron-session/next";
18 | import { NORMAL_IRON_OPTION } from "../../../models/constants";
19 |
20 | type AdminDashboardProps = {
21 | resources: WithId[];
22 | };
23 |
24 |
25 | export const getServerSideProps = withIronSessionSsr(
26 | async function getServerSideProps(ctx) {
27 | const user = ctx.req.session.user;
28 |
29 | if (!user || user.isAdmin !== true) {
30 | return {
31 | redirect: {
32 | destination: '/admin',
33 | permanent: false,
34 | }
35 | };
36 | }
37 |
38 | // since serverSideProps is run on the server's side while cookie belongs to the browser, need to set and send the cookie in the request's header
39 | const headers : HeadersInit = [["Cookie", ctx.req.headers.cookie!], ["Content-Type", "application/json"]]
40 | const res = await fetch(`http://${ctx.req?.headers.host}/api/admin/resources`, {headers});
41 |
42 | if (res.status !== 200) {
43 | return {
44 | props: {
45 | resources: []
46 | }
47 | }
48 | }
49 |
50 | const resources: WithId[] = await res.json();
51 | resources.sort((r1, r2) => r1.name.localeCompare(r2.name));
52 | return {
53 | props: {
54 | user: ctx.req.session.user,
55 | resources,
56 | },
57 | };
58 | },
59 | NORMAL_IRON_OPTION
60 | );
61 |
62 | function AdminDashboard({ resources }: AdminDashboardProps) {
63 | const [resourcesDisplayed, setResourcesDisplayed] =
64 | useState[]>(resources);
65 | const [searchQuery, setSearchQuery] = useState("");
66 | // list layout == true, grid layout == false
67 | const [listLayout, setListLayout] = useState(true);
68 |
69 | useEffect(() => {
70 | if (searchQuery === "") return setResourcesDisplayed(resources);
71 | const searchQueryAppliedResources = resources.filter(
72 | (r) =>
73 | r.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
74 | r.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
75 | r.summary.toLowerCase().includes(searchQuery.toLowerCase())
76 | );
77 | setResourcesDisplayed(searchQueryAppliedResources);
78 | }, [searchQuery, resources]);
79 |
80 | return (
81 |