diff --git a/.eslintignore b/.eslintignore
index a48cf0d..157c89f 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
public
+announces/*
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index db28de7..73a4726 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,10 +14,8 @@ public/*
# Access
access/*
-# Gazettes
-gazettes/*
-
# Misc
.DS_Store
*.log
-.idea
\ No newline at end of file
+.idea
+announces/*
diff --git a/.prettierignore b/.prettierignore
index 0c69b25..d5651d6 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -9,4 +9,4 @@ public/*
*.log
node_modules/*
access/*
-gazettes/*
+announces/*
diff --git a/src/components/AnnouncementLink/index.tsx b/src/components/AnnouncementLink/index.tsx
new file mode 100644
index 0000000..d2846e5
--- /dev/null
+++ b/src/components/AnnouncementLink/index.tsx
@@ -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: (
+
+ ),
+
+ "compte rendu": (
+
+ ),
+ }[type]
+
+ const typeName = {
+ gazette: "Gazzette - ",
+ "compte rendu": "Compte rendu - ",
+ }[type]
+
+ return (
+
+ )
+}
+
+export default memo(AnnouncementLink)
diff --git a/src/components/AnnouncementLink/styles.module.scss b/src/components/AnnouncementLink/styles.module.scss
new file mode 100755
index 0000000..39840ac
--- /dev/null
+++ b/src/components/AnnouncementLink/styles.module.scss
@@ -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;
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index e38540b..1ec55a0 100755
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -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,
diff --git a/src/pages/Announcements/Announcements.tsx b/src/pages/Announcements/Announcements.tsx
new file mode 100644
index 0000000..f3db4cb
--- /dev/null
+++ b/src/pages/Announcements/Announcements.tsx
@@ -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 | undefined
+
+const AnnouncementsPage: FC = (): 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 Loading...
+
+ if (jwtToken && announcementList) {
+ const list = _.orderBy(announcementList.ids, _.identity, "desc")
+ const listElements = list.map((id) => {
+ const announcement = announcementList.entities[id] as Announcement
+ return announcement &&
+ })
+
+ return (
+
+ )
+ }
+ return (
+
+
+
+
+ S'informer sur le bénévolat
+
+
+
+ )
+}
+
+// Fetch server-side data here
+export const loadData = (): AppThunk[] => [fetchAnnouncementListIfNeed()]
+
+export default memo(AnnouncementsPage)
diff --git a/src/pages/Announcements/index.tsx b/src/pages/Announcements/index.tsx
new file mode 100755
index 0000000..e31f516
--- /dev/null
+++ b/src/pages/Announcements/index.tsx
@@ -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: ,
+})
+
+export default (props: Props): JSX.Element => (
+
+
+
+)
+
+export { loadData }
diff --git a/src/pages/Announcements/styles.module.scss b/src/pages/Announcements/styles.module.scss
new file mode 100755
index 0000000..6a061a9
--- /dev/null
+++ b/src/pages/Announcements/styles.module.scss
@@ -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;
+}
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 9ec7afb..b1b6d5d 100755
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -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,
},
diff --git a/src/server/gsheets/announcements.ts b/src/server/gsheets/announcements.ts
new file mode 100644
index 0000000..65d4fd3
--- /dev/null
+++ b/src/server/gsheets/announcements.ts
@@ -0,0 +1,17 @@
+import ExpressAccessors from "./expressAccessors"
+import {
+ Announcement,
+ AnnouncementWithoutId,
+ translationAnnouncement,
+} from "../../services/announcement"
+
+const expressAccessor = new ExpressAccessors(
+ "Announcements",
+ new Announcement(),
+ translationAnnouncement
+)
+
+export const announcementListGet = expressAccessor.listGet()
+// export const announcementGet = expressAccessor.get()
+// export const announcementAdd = expressAccessor.add()
+// export const announcementSet = expressAccessor.set()
diff --git a/src/server/gsheets/localDb.ts b/src/server/gsheets/localDb.ts
index a2e43ff..ff703c1 100644
--- a/src/server/gsheets/localDb.ts
+++ b/src/server/gsheets/localDb.ts
@@ -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"
diff --git a/src/server/index.ts b/src/server/index.ts
index 0b58b3c..085f7c2 100755
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -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)
diff --git a/src/services/accessors.ts b/src/services/accessors.ts
index d84b151..d256ae7 100644
--- a/src/services/accessors.ts
+++ b/src/services/accessors.ts
@@ -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 => {
+ 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
diff --git a/src/services/announcement.ts b/src/services/announcement.ts
new file mode 100644
index 0000000..f3ebb54
--- /dev/null
+++ b/src/services/announcement.ts
@@ -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
diff --git a/src/services/announcementAccessors.ts b/src/services/announcementAccessors.ts
new file mode 100644
index 0000000..460cf42
--- /dev/null
+++ b/src/services/announcementAccessors.ts
@@ -0,0 +1,10 @@
+import ServiceAccessors from "./accessors"
+import { elementName, Announcement, AnnouncementWithoutId } from "./announcement"
+
+const serviceAccessors = new ServiceAccessors(elementName)
+
+// export const announcementGet = serviceAccessors.get()
+// export const announcementAdd = serviceAccessors.add()
+// export const announcementSet = serviceAccessors.set()
+
+export const announcementListGet = serviceAccessors.secureListGet()
diff --git a/src/store/announcementList.ts b/src/store/announcementList.ts
new file mode 100644
index 0000000..01abca8
--- /dev/null
+++ b/src/store/announcementList.ts
@@ -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()
+
+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) => {
+ state.readyStatus = "success"
+ announcementAdapter.setAll(state, payload)
+ },
+ getFailure: (state, { payload }: PayloadAction) => {
+ 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
+}
diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts
index beb3aa8..b30ac58 100644
--- a/src/store/rootReducer.ts
+++ b/src/store/rootReducer.ts
@@ -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,
diff --git a/src/store/utils.ts b/src/store/utils.ts
index ed19b4f..fa28b9c 100644
--- a/src/store/utils.ts
+++ b/src/store/utils.ts
@@ -91,8 +91,8 @@ export function elementAddFetch(
}
}
-export function elementListFetch(
- elementListService: () => Promise<{
+export function elementListFetch>(
+ elementListService: (...idArgs: ServiceInput) => Promise<{
data?: Element[] | undefined
error?: Error | undefined
}>,
@@ -101,20 +101,21 @@ export function elementListFetch(
getFailure: ActionCreatorWithPayload,
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(