mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add announcements page, i.e gazettes and comptes rendus
This commit is contained in:
parent
bd8a74cd17
commit
998691b542
@ -1 +1,2 @@
|
||||
public
|
||||
announces/*
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -14,10 +14,8 @@ public/*
|
||||
# Access
|
||||
access/*
|
||||
|
||||
# Gazettes
|
||||
gazettes/*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.log
|
||||
.idea
|
||||
.idea
|
||||
announces/*
|
||||
|
@ -9,4 +9,4 @@ public/*
|
||||
*.log
|
||||
node_modules/*
|
||||
access/*
|
||||
gazettes/*
|
||||
announces/*
|
||||
|
66
src/components/AnnouncementLink/index.tsx
Normal file
66
src/components/AnnouncementLink/index.tsx
Normal 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)
|
16
src/components/AnnouncementLink/styles.module.scss
Executable file
16
src/components/AnnouncementLink/styles.module.scss
Executable 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;
|
||||
}
|
@ -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,
|
||||
|
69
src/pages/Announcements/Announcements.tsx
Normal file
69
src/pages/Announcements/Announcements.tsx
Normal 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'informer sur le bénévolat </Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch server-side data here
|
||||
export const loadData = (): AppThunk[] => [fetchAnnouncementListIfNeed()]
|
||||
|
||||
export default memo(AnnouncementsPage)
|
16
src/pages/Announcements/index.tsx
Executable file
16
src/pages/Announcements/index.tsx
Executable 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 }
|
21
src/pages/Announcements/styles.module.scss
Executable file
21
src/pages/Announcements/styles.module.scss
Executable 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;
|
||||
}
|
@ -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,
|
||||
},
|
||||
|
17
src/server/gsheets/announcements.ts
Normal file
17
src/server/gsheets/announcements.ts
Normal 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()
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
23
src/services/announcement.ts
Normal file
23
src/services/announcement.ts
Normal 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">
|
10
src/services/announcementAccessors.ts
Normal file
10
src/services/announcementAccessors.ts
Normal 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()
|
51
src/store/announcementList.ts
Normal file
51
src/store/announcementList.ts
Normal 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
|
||||
}
|
@ -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,
|
||||
|
@ -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>(
|
||||
|
Loading…
x
Reference in New Issue
Block a user