Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@

## This PR includes

- [ ] Translation
- [ ] Permission
- [ ] Permission Checks
- [ ] Translations
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,61 @@
# ERCS ClIENT
# 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=<api-endpoint>
APP_GRAPHQL_CODEGEN_ENDPOINT=<graphql-api-endpoint>
APP_TITLE=<app-title>
APP_ENVIRONMENT="app-env"
APP_MAPBOX_TOKEN="token"
APP_GO_API=<go-api-url>
APP_GO_URL=<go-website-url>
APP_GO_RISK_API_ENDPOINT=<go-risk-api>
```

---

## 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).

---
40 changes: 40 additions & 0 deletions app/Root/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`);
Expand All @@ -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'),
Expand Down
10 changes: 4 additions & 6 deletions app/Root/hooks/useRouteMatching.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) {
Expand All @@ -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;
}

Expand Down
15 changes: 11 additions & 4 deletions app/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,20 @@ const gqlClient = new Client({

function Root() {
const [user, setUser] = useState<MeQuery['me'] | undefined>();
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,
Expand Down Expand Up @@ -88,7 +96,6 @@ function Root() {
</AlertContext.Provider>
</UserContext.Provider>
</RequestContext.Provider>

</UrqlProvider>
);
}
Expand Down
104 changes: 104 additions & 0 deletions app/components/AdditionalLinkList/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container
pending={fetching}
footerActions={(
<Pager
activePage={page}
itemsCount={data?.publicLinks?.totalCount ?? 0}
maxItemsPerPage={limit}
onActivePageChange={setPage}
/>
)}
>
<ListView
layout="block"
spacing="2xl"
>
{items.map((item) => (
<Container
key={item.id}
spacingOffset={-2}
headingLevel={4}
heading={item.title}
headerDescription={item.description}
>
<Link
href={item.url}
external
styleVariant="action"
colorVariant="primary"
>
{item.url}
</Link>
</Container>
))}
</ListView>
</Container>
);
}

export default AdditionalLinkList;
6 changes: 3 additions & 3 deletions app/components/DisasterTypeSelectInput/index.tsx
Original file line number Diff line number Diff line change
@@ -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<DisasterTypes['results']>[number];

Expand Down Expand Up @@ -45,7 +45,7 @@ function DisasterTypeSelectInput<const NAME>(props: Props<NAME>) {
...otherProps
} = props;

const { disasterTypes } = use(GoContext);
const { disasterTypes } = useGoContext();

const options = optionsFilter
? disasterTypes?.results.filter(optionsFilter)
Expand Down
46 changes: 46 additions & 0 deletions app/components/DocumentCard/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ListView
layout="block"
withCenteredContents={!manual.date}
withPadding
>
<Image
withContainedFit
src={manual.src}
size="lg"
className={styles.documentCover}
/>
<Heading
level={5}
>
{manual.title}
</Heading>
{isDefined(manual.date) && (
<Description withLightText>
{manual.date}
</Description>
)}
</ListView>
);
}

export default DocumentCard;
6 changes: 6 additions & 0 deletions app/components/DocumentCard/styles.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading