diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index daf35a3..951d203 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,5 +21,5 @@ ## This PR includes -- [ ] Translation -- [ ] Permission \ No newline at end of file +- [ ] Permission Checks +- [ ] Translations \ No newline at end of file diff --git a/README.md b/README.md index 21a5b24..54c61c9 100644 --- a/README.md +++ b/README.md @@ -1 +1,61 @@ -# ERCS ClIENT \ No newline at end of file +# ERCS EOC Platform + +A React application [ERCS EOC](https://eoc-ercs.redcrosseth.org/) website. + +--- + +## Development + +### Prerequisites + +Before you start, create a `.env.local` file: + +```bash +touch .env.local +``` + +Set the following environment variables: + +```env +PORT=3056 +APP_GRAPHQL_ENDPOINT= +APP_GRAPHQL_CODEGEN_ENDPOINT= +APP_TITLE= +APP_ENVIRONMENT="app-env" +APP_MAPBOX_TOKEN="token" +APP_GO_API= +APP_GO_URL= +APP_GO_RISK_API_ENDPOINT= +``` + +--- + +## Local Development (Using Docker, with Backend) + +This project uses Git submodules. After cloning, initialize and update them: + +```bash +git submodule update --init --recursive +``` + +We use a `docker-compose.yml` file located at `./backend/docker-compose.yml`. + +To run it, add the following to your `.env.local`: + +```env +# Include the backend services +COMPOSE_FILE=./backend/docker-compose.yaml:docker-compose.yml +# Use the same .env file for both backend and web-app +BACKEND_ENV_FILE=../.env +``` + +Then start the full stack: + +```bash +docker compose up +``` + +> **Note:** `../` refers to the ERCS EOC Platform folder, relative to `./backend/docker-compose.yml` +> (the main Docker Compose file). + +--- \ No newline at end of file diff --git a/app/Root/config/routes.ts b/app/Root/config/routes.ts index db2cb53..3b22f61 100644 --- a/app/Root/config/routes.ts +++ b/app/Root/config/routes.ts @@ -130,6 +130,41 @@ const login: RouteConfig = { visibility: 'is-not-authenticated', }; +const additionalLinks: RouteConfig = { + index: true, + path: '/additional-links', + load: () => import('#views/AdditionalLinks'), + visibility: 'is-anything', +}; + +const manuals: RouteConfig = { + index: true, + path: '/manuals', + load: () => import('#views/Documents/Manuals'), + visibility: 'is-anything', +}; + +const policies: RouteConfig = { + index: true, + path: '/policies', + load: () => import('#views/Documents/Policies'), + visibility: 'is-anything', +}; + +const guidelines: RouteConfig = { + index: true, + path: '/guidelines', + load: () => import('#views/Documents/Guidelines'), + visibility: 'is-anything', +}; + +const onlineInteractive: RouteConfig = { + index: true, + path: '/online-interactive', + load: () => import('#views/Documents/OnlineInteractive'), + visibility: 'is-anything', +}; + function child(route: RouteConfig, path: string): RouteConfig { const found = route.children?.find((c) => c.path === path); if (!found) throw new Error(`Child route "${path}" not found in "${route.path}"`); @@ -153,6 +188,11 @@ const routes = { login, reportDetail, capacityAndResourcesDetails, + additionalLinks, + manuals, + guidelines, + onlineInteractive, + policies, // child routes emergencyAlert: child(preparedness, 'emergency-alert'), disasterResponse: child(preparedness, 'disaster-response'), diff --git a/app/Root/hooks/useRouteMatching.tsx b/app/Root/hooks/useRouteMatching.tsx index a2b2a5a..b7d763b 100644 --- a/app/Root/hooks/useRouteMatching.tsx +++ b/app/Root/hooks/useRouteMatching.tsx @@ -1,7 +1,6 @@ -import { use } from 'react'; import { generatePath } from 'react-router'; -import UserContext from '#contexts/UserContext'; +import useAuth from '#hooks/useAuth'; import type { RouteKeys } from '#root/config/routes'; import routes from '#root/config/routes'; @@ -10,8 +9,7 @@ export interface Attrs { } function useRouteMatching(routeKey: RouteKeys, attrs?: Attrs) { - const { authenticated } = use(UserContext); - + const { isAuthenticated } = useAuth(); const to = routes[routeKey]; if (!to) { @@ -23,11 +21,11 @@ function useRouteMatching(routeKey: RouteKeys, attrs?: Attrs) { path, } = to; - if (visibility === 'is-not-authenticated' && authenticated) { + if (visibility === 'is-not-authenticated' && isAuthenticated) { return undefined; } - if (visibility === 'is-authenticated' && !authenticated) { + if (visibility === 'is-authenticated' && !isAuthenticated) { return undefined; } diff --git a/app/Root/index.tsx b/app/Root/index.tsx index ee570ab..9ec620f 100644 --- a/app/Root/index.tsx +++ b/app/Root/index.tsx @@ -53,12 +53,20 @@ const gqlClient = new Client({ function Root() { const [user, setUser] = useState(); - const authenticated = !!user; + const [isAuthLoading, setIsAuthLoading] = useState(true); + + const removeUserAuth = (() => { + setUser(undefined); + setIsAuthLoading(false); + }); + const userContext: UserContextInterface = useMemo(() => ({ - authenticated, user, setUser, - }), [authenticated, user]); + removeUserAuth, + isAuthLoading, + setIsAuthLoading, + }), [user, isAuthLoading]); const requestContextValue = useMemo(() => ({ transformUrl: processGoUrls, @@ -88,7 +96,6 @@ function Root() { - ); } diff --git a/app/components/AdditionalLinkList/index.tsx b/app/components/AdditionalLinkList/index.tsx new file mode 100644 index 0000000..2e3f821 --- /dev/null +++ b/app/components/AdditionalLinkList/index.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { + Container, + ListView, + Pager, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import Link from '#components/Link'; +import { + type LinkTypeEnum, + usePublicLinksQuery, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const PUBLIC_LINKS_QUERY = gql` + query PublicLinks( + $limit: Int = 10 + $offset: Int = 0 + $linkType: LinkTypeEnum + ) { + publicLinks( + pagination: { limit: $limit, offset: $offset } + filters: { linkType: $linkType } + ) { + totalCount + results { + description + id + title + url + linkTypeDisplay + linkType + } + } + } +`; + +interface LinkListProps { + linkType: LinkTypeEnum; +} + +function AdditionalLinkList({ linkType }: LinkListProps) { + const { + limit, + page, + setPage, + offset, + } = useFilterState({ + filter: {}, + pageSize: 10, + }); + + const [{ data, fetching }] = usePublicLinksQuery({ + variables: { + linkType, + limit, + offset, + }, + }); + + const items = data?.publicLinks?.results ?? []; + + return ( + + )} + > + + {items.map((item) => ( + + + {item.url} + + + ))} + + + ); +} + +export default AdditionalLinkList; diff --git a/app/components/DisasterTypeSelectInput/index.tsx b/app/components/DisasterTypeSelectInput/index.tsx index ec57e82..a8214b1 100644 --- a/app/components/DisasterTypeSelectInput/index.tsx +++ b/app/components/DisasterTypeSelectInput/index.tsx @@ -1,10 +1,10 @@ -import { use } from 'react'; import { SelectInput, type SelectInputProps, } from '@ifrc-go/ui'; -import GoContext, { type DisasterTypes } from '#contexts/GoContext'; +import { type DisasterTypes } from '#contexts/GoContext'; +import useGoContext from '#hooks/useGoContext'; export type DisasterTypeItem = NonNullable[number]; @@ -45,7 +45,7 @@ function DisasterTypeSelectInput(props: Props) { ...otherProps } = props; - const { disasterTypes } = use(GoContext); + const { disasterTypes } = useGoContext(); const options = optionsFilter ? disasterTypes?.results.filter(optionsFilter) diff --git a/app/components/DocumentCard/index.tsx b/app/components/DocumentCard/index.tsx new file mode 100644 index 0000000..43e65d2 --- /dev/null +++ b/app/components/DocumentCard/index.tsx @@ -0,0 +1,46 @@ +import { + Description, + Heading, + Image, + ListView, +} from '@ifrc-go/ui'; +import { isDefined } from '@togglecorp/fujs'; + +import styles from './styles.module.css'; + +interface DocumentCardProps { + manual: { + title: string; + src: string; + date?: string; + }; +} + +function DocumentCard({ manual }: DocumentCardProps) { + return ( + + + + {manual.title} + + {isDefined(manual.date) && ( + + {manual.date} + + )} + + ); +} + +export default DocumentCard; diff --git a/app/components/DocumentCard/styles.module.css b/app/components/DocumentCard/styles.module.css new file mode 100644 index 0000000..17495b3 --- /dev/null +++ b/app/components/DocumentCard/styles.module.css @@ -0,0 +1,6 @@ +.document-cover { + border: var(--go-ui-width-separator-sm) solid var(--go-ui-color-gray-30); + width: 12.5rem; + height: 12.5rem; + overflow: hidden; +} diff --git a/app/components/DocumentList/index.tsx b/app/components/DocumentList/index.tsx new file mode 100644 index 0000000..5e990c7 --- /dev/null +++ b/app/components/DocumentList/index.tsx @@ -0,0 +1,96 @@ +import { + Container, + ListView, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import DocumentCard from '#components/DocumentCard'; +import Link from '#components/Link'; +import Page from '#components/Page'; +import { + ReportTypeEnum, + useDocumentListQuery, +} from '#generated/types/graphql'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const DOCUMENT_LIST_QUERY = gql` + query DocumentList( + $reportType: ReportTypeEnum + $limit: Int = 10 + $offset: Int = 0 + ) { + reports( + filters: { reportType: $reportType } + pagination: { limit: $limit, offset: $offset } + ) { + totalCount + results { + id + title + description + visibility + file { + url + } + coverImage { + url + } + } + } + } +`; + +interface Props { + reportType: ReportTypeEnum + heading: string; + description: string; +} + +function DocumentList(props: Props) { + const { + reportType, + heading, + description, + } = props; + + const [{ data, fetching }] = useDocumentListQuery({ + variables: { + reportType, + }, + }); + + return ( + + + + {data?.reports.results.map((document) => ( + + + + ))} + + + + ); +} + +export default DocumentList; diff --git a/app/components/Footer/index.tsx b/app/components/Footer/index.tsx index 6085e0a..d87f0ae 100644 --- a/app/components/Footer/index.tsx +++ b/app/components/Footer/index.tsx @@ -92,7 +92,7 @@ function GlobalFooter(props: Props) { - Cookie Policy + Manuals - Terms and Conditions + Policy + + + Guidelines @@ -122,9 +125,8 @@ function GlobalFooter(props: Props) { withSpacingOpticalCorrection > Dataset @@ -134,12 +136,15 @@ function GlobalFooter(props: Props) { Project Mapping Online Interactive + + Additional Links + { navigate('/login'); }; + + const handleLogout = async () => { + await triggerLogout({}); + removeUserAuth(); + alert.show('Logout successful!', { variant: 'success' }); + }; return (