Add announcements page, i.e gazettes and comptes rendus

This commit is contained in:
pikiou 2022-02-07 17:21:22 +01:00
parent bd8a74cd17
commit 998691b542
19 changed files with 347 additions and 18 deletions

View File

@ -1 +1,2 @@
public
announces/*

6
.gitignore vendored
View File

@ -14,10 +14,8 @@ public/*
# Access
access/*
# Gazettes
gazettes/*
# Misc
.DS_Store
*.log
.idea
.idea
announces/*

View File

@ -9,4 +9,4 @@ public/*
*.log
node_modules/*
access/*
gazettes/*
announces/*

View File

@ -0,0 +1,66 @@
import { memo } from "react"
import { Announcement } from "../../services/announcement"
import styles from "./styles.module.scss"
interface Props {
// eslint-disable-next-line react/require-default-props
announcement: Announcement
}
const AnnouncementLink = ({ announcement }: Props): JSX.Element | null => {
const { id, type, title, url } = announcement
const icon = {
gazette: (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="20"
height="20"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 20 20"
className={styles.gazette}
>
<path
d="M16 2h4v15a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V0h16v2zm0 2v13a1 1 0 0 0 1 1a1 1 0 0 0 1-1V4h-2zM2 2v15a1 1 0 0 0 1 1h11.17a2.98 2.98 0 0 1-.17-1V2H2zm2 8h8v2H4v-2zm0 4h8v2H4v-2zM4 4h8v4H4V4z"
fill="currentColor"
/>
</svg>
),
"compte rendu": (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="26"
height="26"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 32 32"
className={styles.report}
>
<path d="M10 18h8v2h-8z" fill="currentColor" />
<path d="M10 13h12v2H10z" fill="currentColor" />
<path d="M10 23h5v2h-5z" fill="currentColor" />
<path
d="M25 5h-3V4a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v1H7a2 2 0 0 0-2 2v21a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zM12 4h8v4h-8zm13 24H7V7h3v3h12V7h3z"
fill="currentColor"
/>
</svg>
),
}[type]
const typeName = {
gazette: "Gazzette - ",
"compte rendu": "Compte rendu - ",
}[type]
return (
<div key={id} className={styles.announcementLink}>
{icon} {typeName}
<a href={url}>{title}</a>
</div>
)
}
export default memo(AnnouncementLink)

View File

@ -0,0 +1,16 @@
@import "../../theme/variables";
@import "../../theme/mixins";
.announcementLink {
margin: 0.9em;
}
.gazette {
margin-left: 0.22em;
margin-right: 0.5em;
vertical-align: middle;
}
.report {
margin-right: 0.4em;
vertical-align: middle;
}

View File

@ -1,3 +1,4 @@
import AnnouncementLink from "./AnnouncementLink"
import LoginForm from "./LoginForm"
import Notifications from "./Notifications"
import VolunteerList from "./VolunteerList"
@ -10,6 +11,7 @@ import WishAdd from "./WishAdd"
import PreRegisterForm from "./PreRegisterForm"
export {
AnnouncementLink,
LoginForm,
Notifications,
VolunteerList,

View File

@ -0,0 +1,69 @@
import { FC, memo } from "react"
import * as _ from "lodash"
import { RouteComponentProps, Link } from "react-router-dom"
import { useSelector, shallowEqual } from "react-redux"
import { Helmet } from "react-helmet"
import { EntityState } from "@reduxjs/toolkit"
import { AppState, AppThunk } from "../../store"
import { AnnouncementLink, LoginForm } from "../../components"
import styles from "./styles.module.scss"
import { fetchAnnouncementListIfNeed } from "../../store/announcementList"
import { Announcement } from "../../services/announcement"
import { selectUserJwtToken } from "../../store/auth"
export type Props = RouteComponentProps
let prevAnnouncements: EntityState<Announcement> | undefined
const AnnouncementsPage: FC<Props> = (): JSX.Element => {
const jwtToken = useSelector(selectUserJwtToken)
const announcementList = useSelector((state: AppState) => {
if (!state.announcementList) {
return undefined
}
const announcements = _.pick(state.announcementList, "entities", "ids")
if (announcements) {
prevAnnouncements = announcements
return announcements
}
return prevAnnouncements
}, shallowEqual)
if (jwtToken === undefined) return <p>Loading...</p>
if (jwtToken && announcementList) {
const list = _.orderBy(announcementList.ids, _.identity, "desc")
const listElements = list.map((id) => {
const announcement = announcementList.entities[id] as Announcement
return announcement && <AnnouncementLink key={id} announcement={announcement} />
})
return (
<div className={styles.announcements}>
<div className={styles.announcementsContent}>{listElements}</div>
</div>
)
}
return (
<div>
<div className={styles.announcement}>
<div className={styles.loginContent}>
<Helmet title="LoginPage" />
<LoginForm />
</div>
</div>
<div className={styles.announcement}>
<div className={styles.navigationLink}>
<Link to="/preRegister"> S&apos;informer sur le bénévolat </Link>
</div>
</div>
</div>
)
}
// Fetch server-side data here
export const loadData = (): AppThunk[] => [fetchAnnouncementListIfNeed()]
export default memo(AnnouncementsPage)

View File

@ -0,0 +1,16 @@
import loadable from "@loadable/component"
import { Loading, ErrorBoundary } from "../../components"
import { Props, loadData } from "./Announcements"
const Announcements = loadable(() => import("./Announcements"), {
fallback: <Loading />,
})
export default (props: Props): JSX.Element => (
<ErrorBoundary>
<Announcements {...props} />
</ErrorBoundary>
)
export { loadData }

View File

@ -0,0 +1,21 @@
@import "../../theme/mixins";
.announcements {
@include page-wrapper-center;
}
.announcementsContent {
@include page-content-wrapper;
text-align: left;
}
.loginContent {
@include page-content-wrapper;
}
.navigationLink {
@include page-content-wrapper;
text-align: center;
}

View File

@ -2,6 +2,7 @@ import { RouteConfig } from "react-router-config"
import App from "../app"
import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
import AsyncPreRegisterPage, { loadData as loadPreRegisterPage } from "../pages/PreRegister"
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
@ -66,6 +67,11 @@ export default [
component: AsyncBoard,
loadData: loadBoardData,
},
{
path: "/annonces",
component: AsyncAnnouncements,
loadData: loadAnnouncementsData,
},
{
component: NotFound,
},

View File

@ -0,0 +1,17 @@
import ExpressAccessors from "./expressAccessors"
import {
Announcement,
AnnouncementWithoutId,
translationAnnouncement,
} from "../../services/announcement"
const expressAccessor = new ExpressAccessors<AnnouncementWithoutId, Announcement>(
"Announcements",
new Announcement(),
translationAnnouncement
)
export const announcementListGet = expressAccessor.listGet()
// export const announcementGet = expressAccessor.get()
// export const announcementAdd = expressAccessor.add()
// export const announcementSet = expressAccessor.set()

View File

@ -10,6 +10,8 @@ const DB_TO_LOAD_PATH = path.resolve(process.cwd(), "access/dbToLoad.json")
const ANONYMIZED_DB_PATH = path.resolve(process.cwd(), "access/dbAnonymized.json")
export class SheetNames {
Announcements = "Annonces"
JavGames = "Jeux JAV"
PreVolunteers = "PreMembres"

View File

@ -17,6 +17,7 @@ import ssr from "./ssr"
import certbotRouter from "../routes/certbot"
import { hasSecret, secure } from "./secure"
import { announcementListGet } from "./gsheets/announcements"
import { javGameListGet } from "./gsheets/javGames"
import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers"
import { teamListGet } from "./gsheets/teams"
@ -76,6 +77,7 @@ app.post("/VolunteerLogin", volunteerLogin)
app.post("/VolunteerForgot", volunteerForgot)
// Secured APIs
app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet)
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
app.get("/TeamListGet", teamListGet)
// UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet)

View File

@ -58,6 +58,32 @@ export default class ServiceAccessors<
}
}
secureListGet(): (jwt: string) => Promise<{
data?: Element[]
error?: Error
}> {
interface ElementListGetResponse {
data?: Element[]
error?: Error
}
return async (jwt: string): Promise<ElementListGetResponse> => {
try {
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}ListGet`,
fullAxiosConfig
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-types
add(): (volunteerWithoutId: ElementNoId) => Promise<{
data?: Element

View File

@ -0,0 +1,23 @@
export class Announcement {
id = 0
created = new Date()
type = ""
title = ""
url = ""
}
export const translationAnnouncement: { [k in keyof Announcement]: string } = {
id: "id",
created: "creation",
type: "type",
title: "titre",
url: "url",
}
export const elementName = "Announcement"
export type AnnouncementWithoutId = Omit<Announcement, "id">

View File

@ -0,0 +1,10 @@
import ServiceAccessors from "./accessors"
import { elementName, Announcement, AnnouncementWithoutId } from "./announcement"
const serviceAccessors = new ServiceAccessors<AnnouncementWithoutId, Announcement>(elementName)
// export const announcementGet = serviceAccessors.get()
// export const announcementAdd = serviceAccessors.add()
// export const announcementSet = serviceAccessors.set()
export const announcementListGet = serviceAccessors.secureListGet()

View File

@ -0,0 +1,51 @@
import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementListFetch } from "./utils"
import { Announcement } from "../services/announcement"
import { AppThunk, AppState } from "."
import { announcementListGet } from "../services/announcementAccessors"
const announcementAdapter = createEntityAdapter<Announcement>()
export const initialState = announcementAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest)
const announcementList = createSlice({
name: "announcementList",
initialState,
reducers: {
getRequesting: (state) => {
state.readyStatus = "request"
},
getSuccess: (state, { payload }: PayloadAction<Announcement[]>) => {
state.readyStatus = "success"
announcementAdapter.setAll(state, payload)
},
getFailure: (state, { payload }: PayloadAction<string>) => {
state.readyStatus = "failure"
state.error = payload
},
},
})
export default announcementList.reducer
export const { getRequesting, getSuccess, getFailure } = announcementList.actions
export const fetchAnnouncementList = elementListFetch(
announcementListGet,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors du chargement des announcements: ${error.message}`)
)
const shouldFetchAnnouncementList = (state: AppState) =>
state.announcementList.readyStatus !== "success"
export const fetchAnnouncementListIfNeed = (): AppThunk => (dispatch, getState) => {
const { jwt } = getState().auth
if (shouldFetchAnnouncementList(getState())) return dispatch(fetchAnnouncementList(jwt))
return null
}

View File

@ -3,6 +3,7 @@ import { connectRouter } from "connected-react-router"
import auth from "./auth"
import javGameList from "./javGameList"
import announcementList from "./announcementList"
import preVolunteerAdd from "./preVolunteerAdd"
import preVolunteerCount from "./preVolunteerCount"
import teamList from "./teamList"
@ -25,6 +26,7 @@ import wishList from "./wishList"
export default (history: History) => ({
auth,
javGameList,
announcementList,
preVolunteerAdd,
preVolunteerCount,
teamList,

View File

@ -91,8 +91,8 @@ export function elementAddFetch<Element>(
}
}
export function elementListFetch<Element>(
elementListService: () => Promise<{
export function elementListFetch<Element, ServiceInput extends Array<any>>(
elementListService: (...idArgs: ServiceInput) => Promise<{
data?: Element[] | undefined
error?: Error | undefined
}>,
@ -101,20 +101,21 @@ export function elementListFetch<Element>(
getFailure: ActionCreatorWithPayload<string, string>,
errorMessage?: (error: Error) => void,
successMessage?: () => void
): () => AppThunk {
return (): AppThunk => async (dispatch) => {
dispatch(getRequesting())
): (...idArgs: ServiceInput) => AppThunk {
return (...idArgs: ServiceInput): AppThunk =>
async (dispatch) => {
dispatch(getRequesting())
const { error, data } = await elementListService()
const { error, data } = await elementListService(...idArgs)
if (error) {
dispatch(getFailure(error.message))
errorMessage?.(error)
} else {
dispatch(getSuccess(data as Element[]))
successMessage?.()
if (error) {
dispatch(getFailure(error.message))
errorMessage?.(error)
} else {
dispatch(getSuccess(data as Element[]))
successMessage?.()
}
}
}
}
export function elementSet<Element>(