diff --git a/src/client/components/features/Actualites/ActualiteCard.module.scss b/src/client/components/features/Actualites/ActualiteCard.module.scss
index 2e6ffc4a35..9ce6370ec8 100644
--- a/src/client/components/features/Actualites/ActualiteCard.module.scss
+++ b/src/client/components/features/Actualites/ActualiteCard.module.scss
@@ -5,7 +5,6 @@
}
.card .content {
@include utilities.text-medium;
- // FIXME (GAFI 11-12-2024): Idéalement .content est sur
plutôt que parent du titre
& p {
@include utilities.line-clamp(3, 1.2);
}
@@ -33,16 +32,24 @@
object-fit: cover;
}
- .content {
- padding-block: 1.25rem;
+ & .content {
+ padding-bottom: 1.25rem;
+ padding-top: .5rem;
padding-inline: 1.5rem;
display: flex;
+ gap: .5rem;
flex: 1;
flex-direction: column;
}
- .title {
- margin-bottom: 0.5rem;
+ & time {
+ align-self: flex-end;
+ @include utilities.text-small;
+ }
+ & time:empty {
+ visibility: hidden;
+ // NOTE (GAFI 17-12-2024): equivalent à 1lh dans ce cas précis mais meilleur support
+ height: 1.2em;
}
& a {
diff --git a/src/client/components/features/Actualites/ActualiteCard.stories.tsx b/src/client/components/features/Actualites/ActualiteCard.stories.tsx
index bda6969d40..7a53910e20 100644
--- a/src/client/components/features/Actualites/ActualiteCard.stories.tsx
+++ b/src/client/components/features/Actualites/ActualiteCard.stories.tsx
@@ -1,7 +1,10 @@
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';
-import { anActualite } from '../../../../server/actualites/domain/actualite.fixture';
+import { DependenciesProvider } from '~/client/context/dependenciesContainer.context';
+import { JsDateService } from '~/client/services/date/js/js.date.service';
+import { anActualite } from '~/server/actualites/domain/actualite.fixture';
+
import ActualiteCard from './ActualiteCard';
const meta: Meta = {
@@ -14,6 +17,11 @@ const meta: Meta = {
actualite: anActualite(),
},
component: ActualiteCard,
+ render: (args) => (
+
+
+
+ ),
title: 'Components/Cards/ActualiteCard',
};
@@ -34,3 +42,8 @@ export const ContenuCourt: Story = {
}),
},
};
+export const avecDateMiseAJour: Story = {
+ args: {
+ actualite: anActualite({ dateMiseAJour: new Date('2024-01-01') }),
+ },
+};
diff --git a/src/client/components/features/Actualites/ActualiteCard.tsx b/src/client/components/features/Actualites/ActualiteCard.tsx
index 324e490840..a109d59d8a 100644
--- a/src/client/components/features/Actualites/ActualiteCard.tsx
+++ b/src/client/components/features/Actualites/ActualiteCard.tsx
@@ -2,7 +2,9 @@ import classNames from 'classnames';
import React from 'react';
import { Card } from '~/client/components/ui/Card/Card';
+import Date from '~/client/components/ui/Date';
import { Link } from '~/client/components/ui/Link/Link';
+import { useIsInternalLink } from '~/client/hooks/useIsInternalLink';
import { Actualite } from '~/server/actualites/domain/actualite';
import { getExtraitContenu } from '~/server/cms/infra/repositories/strapi.utils';
@@ -16,30 +18,30 @@ type ActualiteCardProps = Omit, 'layout
};
export default function ActualiteCard({ actualite, headingLevel = 'h2', className, ...rest }: ActualiteCardProps) {
- // FIXME (GAFI 14-11-2024): Passer plutôt par actualite.lien, actualite.article n'est pas utilisé dans le composant
- // ou bien utiliser actualite.article.slug dans le composant
- const isExternalLink = actualite.article == null;
+ const isInternalLink = useIsInternalLink(actualite.link);
const extrait = getExtraitContenu(actualite.contenu);
return (
-
- {actualite.bannière && (
-
- )}
-
- {actualite.titre}
- {extrait}
-
- {isExternalLink ? 'En savoir plus' : "Lire l'article"}
-
-
-
-
+
+
+ {actualite.bannière && (
+
+ )}
+
+ {actualite.dateMiseAJour ? : }
+ {actualite.titre}
+ {extrait}
+
+ {isInternalLink ? 'Lire l\'article' : 'En savoir plus'}
+
+
+
+
+
);
}
diff --git a/src/client/components/ui/Date/index.stories.tsx b/src/client/components/ui/Date/index.stories.tsx
new file mode 100644
index 0000000000..932fcc8adf
--- /dev/null
+++ b/src/client/components/ui/Date/index.stories.tsx
@@ -0,0 +1,30 @@
+import { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+
+import { DependenciesProvider } from '~/client/context/dependenciesContainer.context';
+import { JsDateService } from '~/client/services/date/js/js.date.service';
+
+import DateComponent from '.';
+
+const meta: Meta = {
+ argTypes: {
+ date: {
+ control: 'date',
+ },
+ },
+ args: {
+ date: new Date(),
+ },
+ component: DateComponent,
+ render: ({ date, ...args }) => (
+
+
+
+ ),
+ title: 'Components/Date',
+};
+
+export default meta;
+
+type Story = StoryObj>;
+export const Default: Story = {};
diff --git a/src/client/components/ui/Date/index.test.tsx b/src/client/components/ui/Date/index.test.tsx
new file mode 100644
index 0000000000..8c9b496904
--- /dev/null
+++ b/src/client/components/ui/Date/index.test.tsx
@@ -0,0 +1,35 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { render, screen } from '@testing-library/react';
+
+import { DependenciesProvider } from '~/client/context/dependenciesContainer.context';
+import { JsDateService } from '~/client/services/date/js/js.date.service';
+
+import DateComponent from '.';
+
+describe(' ', () => {
+ it('affiche la date dans un format compréhensible', () => {
+ const date = new Date('2024-01-01');
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('time')).toHaveTextContent('1 janvier 2024');
+ });
+ it('indique la date programmatiquement au format ISO', () => {
+ const date = new Date('2024-01-01');
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('time')).toHaveAttribute('datetime', '2024-01-01T00:00:00.000Z');
+ });
+});
diff --git a/src/client/components/ui/Date/index.tsx b/src/client/components/ui/Date/index.tsx
new file mode 100644
index 0000000000..fc751f3851
--- /dev/null
+++ b/src/client/components/ui/Date/index.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { useDependency } from '~/client/context/dependenciesContainer.context';
+import { DateService } from '~/client/services/date/date.service';
+
+type DateProps = React.ComponentPropsWithoutRef<'time'> & {
+ date: Date,
+}
+
+export default function Date({ date, ...props }: DateProps) {
+ const dateService = useDependency('dateService');
+
+ return (
+ {dateService.formatToHumanReadableDate(date)}
+ );
+};
diff --git a/src/pages/actualites/index.page.test.tsx b/src/pages/actualites/index.page.test.tsx
index 02406237da..289760c8d3 100644
--- a/src/pages/actualites/index.page.test.tsx
+++ b/src/pages/actualites/index.page.test.tsx
@@ -9,7 +9,7 @@ import { mockUseRouter } from '~/client/components/useRouter.mock';
import { mockScrollIntoView, mockSmallScreen } from '~/client/components/window.mock';
import { DependenciesProvider } from '~/client/context/dependenciesContainer.context';
import { aManualAnalyticsService } from '~/client/services/analytics/analytics.service.fixture';
-import ActualitesPage, { getStaticProps } from '~/pages/actualites/index.page';
+import { ActualitesPage, getStaticProps } from '~/pages/actualites/index.page';
import { anActualite, anActualiteList } from '~/server/actualites/domain/actualite.fixture';
import { createFailure, createSuccess } from '~/server/errors/either';
import { ErreurMetier } from '~/server/errors/erreurMetier.types';
diff --git a/src/pages/actualites/index.page.tsx b/src/pages/actualites/index.page.tsx
index e71393829f..89742c63b4 100644
--- a/src/pages/actualites/index.page.tsx
+++ b/src/pages/actualites/index.page.tsx
@@ -10,6 +10,7 @@ import useAnalytics from '~/client/hooks/useAnalytics';
import { Actualite } from '~/server/actualites/domain/actualite';
import { isFailure } from '~/server/errors/either';
import { dependencies } from '~/server/start';
+import { ISODateTime } from '~/shared/ISODateTime';
import analytics from './index.analytics';
import styles from './index.module.scss';
@@ -17,9 +18,9 @@ import styles from './index.module.scss';
interface ActualitesPageProps {
cartesActualites: Array
}
-
const MAX_VISIBLE_ACTUALITES = 3;
-export default function ActualitesPage({ cartesActualites }: ActualitesPageProps) {
+
+export function ActualitesPage({ cartesActualites }: ActualitesPageProps) {
useAnalytics(analytics);
const articleCardList = useMemo(() => {
@@ -54,7 +55,26 @@ export default function ActualitesPage({ cartesActualites }: ActualitesPageProps
);
}
-export async function getStaticProps(): Promise> {
+type SerializedActualite = Omit & {
+ dateMiseAJour?: ISODateTime
+}
+interface SerializedActualitesPageProps {
+ cartesActualites: Array
+}
+function deserialize(actualites: Array): Array {
+ return actualites.map((actualite) => ({
+ ...actualite,
+ dateMiseAJour: actualite.dateMiseAJour ? new Date(actualite.dateMiseAJour) : undefined,
+ }));
+}
+function serialize(cartesActualitesResponse: Array): Array {
+ return JSON.parse(JSON.stringify(cartesActualitesResponse));
+}
+export default function Deserialize(props: SerializedActualitesPageProps) {
+ const deserializedActus = deserialize(props.cartesActualites);
+ return ;
+}
+export async function getStaticProps(): Promise> {
const isEspaceJeuneVisible = process.env.NEXT_PUBLIC_OLD_ESPACE_JEUNE_FEATURE === '0';
if (!isEspaceJeuneVisible) {
return { notFound: true };
@@ -68,7 +88,7 @@ export async function getStaticProps(): Promise
serviceJeuneList: Array
}
-
const MAX_VISIBLE_ACTUALITES_LENGTH = 3;
-export default function EspaceJeunePage({ cartesActualites, serviceJeuneList }: EspaceJeunePageProps) {
+export function EspaceJeunePage({ cartesActualites, serviceJeuneList }: EspaceJeunePageProps) {
useAnalytics(analytics);
const articleCardList: React.ReactNode[] = useMemo(() => {
@@ -72,7 +73,27 @@ export default function EspaceJeunePage({ cartesActualites, serviceJeuneList }:
);
}
-export async function getStaticProps(): Promise> {
+type SerializedActualite = Omit & {
+ dateMiseAJour?: ISODateTime
+}
+type SerializedEspaceJeunePageProps = {
+ cartesActualites: Array
+ serviceJeuneList: Array
+}
+function deserialize(actualites: Array): Array {
+ return actualites.map((actualite) => ({
+ ...actualite,
+ dateMiseAJour: actualite.dateMiseAJour ? new Date(actualite.dateMiseAJour) : undefined,
+ }));
+}
+function serialize(cartesActualitesResponse: InitialType): SerializedType {
+ return JSON.parse(JSON.stringify(cartesActualitesResponse));
+}
+export default function Deserialize(props: SerializedEspaceJeunePageProps) {
+ const deserializedActus = deserialize(props.cartesActualites);
+ return ;
+}
+export async function getStaticProps(): Promise> {
const isEspaceJeuneVisible = process.env.NEXT_PUBLIC_OLD_ESPACE_JEUNE_FEATURE === '1';
if (!isEspaceJeuneVisible) {
return { notFound: true };
@@ -87,8 +108,8 @@ export async function getStaticProps(): Promise
}
-
-export default function Accueil(accueilProps: AccueilPageProps) {
+export function Accueil({ actualites }: AccueilPageProps) {
useAnalytics(analytics);
const isJobEteCardVisible = process.env.NEXT_PUBLIC_JOB_ETE_FEATURE === '1';
@@ -50,7 +50,7 @@ export default function Accueil(accueilProps: AccueilPageProps) {
const isCampagneHandicapVisible = process.env.NEXT_PUBLIC_CAMPAGNE_HANDICAP === '1';
- const actualitesCardListContent: CardContent[] = accueilProps.actualites.map((carte: Actualite): CardContent => {
+ const actualitesCardListContent: CardContent[] = actualites.map((carte: Actualite): CardContent => {
return {
children: {carte.extraitContenu}
,
imageUrl: carte.bannière?.src || '',
@@ -276,20 +276,20 @@ export default function Accueil(accueilProps: AccueilPageProps) {
{isBanniereStagesSecondeVisible
&& (
+ className={classNames(styles.hero, styles.stageSecondeBanner)}>
{isBanniereStagesSecondePourCampagneDu25Mars ? (
<>
- Un stage du 17 au 28 juin 2024
+ Un stage du 17 au 28 juin 2024
- pour permettre aux élèves de seconde générale et technologique de diversifier leur connaissance des
- métiers.
+ pour permettre aux élèves de seconde générale et technologique de diversifier leur connaissance des
+ métiers.
- Proposer un stage ou candidater
+ Proposer un stage ou candidater
>
@@ -297,14 +297,14 @@ export default function Accueil(accueilProps: AccueilPageProps) {
<>
- Accueillez des élèves en stages de seconde générale et technologique.
+ Accueillez des élèves en stages de seconde générale et technologique.
- Inspirez, transmettez, faites découvrir vos métiers.
+ Inspirez, transmettez, faites découvrir vos métiers.
- Déposer votre offre de stage
+ Déposer votre offre de stage
>
@@ -318,14 +318,14 @@ export default function Accueil(accueilProps: AccueilPageProps) {
- WorldSkills Lyon 2024, la Compétition Mondiale des Métiers.
+ WorldSkills Lyon 2024, la Compétition Mondiale des Métiers.
- 1jeune1solution s’engage en faveur de la jeunesse, venez nous rencontrer du 10 au 15 septembre lors de la compétition WorldSkills Lyon 2024.
+ 1jeune1solution s’engage en faveur de la jeunesse, venez nous rencontrer du 10 au 15 septembre lors de la compétition WorldSkills Lyon 2024.
- Plus d’infos
+ Plus d’infos
@@ -372,7 +372,7 @@ export default function Accueil(accueilProps: AccueilPageProps) {
- Actualités
+ Actualités
@@ -381,7 +381,7 @@ export default function Accueil(accueilProps: AccueilPageProps) {
)}
- Voir toutes les actualités
+ Voir toutes les actualités
@@ -469,9 +469,29 @@ export default function Accueil(accueilProps: AccueilPageProps) {
>
);
-};
+}
+
+type SerializedActualite = Omit & {
+ dateMiseAJour?: ISODateTime
+}
+interface SerializedAccueilPageProps {
+ actualites: Array
+}
+function deserialize(actualites: Array): Array {
+ return actualites.map((actualite) => ({
+ ...actualite,
+ dateMiseAJour: actualite.dateMiseAJour ? new Date(actualite.dateMiseAJour) : undefined,
+ }));
+}
+function serialize(cartesActualitesResponse: Array): Array {
+ return JSON.parse(JSON.stringify(cartesActualitesResponse));
+}
+export default function Deserialize(props: SerializedAccueilPageProps) {
+ const deserializedActus = deserialize(props.actualites);
+ return ;
+}
-export async function getStaticProps(): Promise> {
+export async function getStaticProps(): Promise> {
const isEspaceJeuneVisible = process.env.NEXT_PUBLIC_OLD_ESPACE_JEUNE_FEATURE === '0';
if (!isEspaceJeuneVisible) {
return {
@@ -486,7 +506,7 @@ export async function getStaticProps(): Promise {
@@ -74,6 +76,7 @@ describe('strapiActualitesRepository', () => {
src: 'https://image.example.com/',
},
contenu: 'Contenu',
+ dateMiseAJour: new Date('2024-01-01T00:00:00.000Z'),
extraitContenu: 'Contenu',
link: '/articles/titre-article',
titre: 'Titre',
@@ -143,7 +146,8 @@ describe('strapiActualitesRepository', () => {
const result = await strapiActualites.getActualitesEchantillonList();
// Then
- expect(result).toStrictEqual(createSuccess(anActualiteLongList().slice(0,3)));
+ expect(isSuccess(result)).toBe(true);
+ expect((result as Success).result).toHaveLength(3);
});
it('lorsque le mapping est en échec, appelle le service de gestion d’erreur avec l’erreur et le contexte', async () => {
// Given
diff --git a/src/server/articles/infra/strapiArticle.ts b/src/server/articles/infra/strapiArticle.ts
index 65290b76c8..99653cda8a 100644
--- a/src/server/articles/infra/strapiArticle.ts
+++ b/src/server/articles/infra/strapiArticle.ts
@@ -1,9 +1,10 @@
import { Strapi } from '~/server/cms/infra/repositories/strapi.response';
+import { ISODateTime } from '~/shared/ISODateTime';
export interface StrapiArticle {
contenu: string;
banniere: Strapi.SingleRelation;
slug: string;
titre: string;
- updatedAt: string;
+ updatedAt: ISODateTime;
}
diff --git a/src/shared/ISODateTime.d.ts b/src/shared/ISODateTime.d.ts
new file mode 100644
index 0000000000..b9a9522350
--- /dev/null
+++ b/src/shared/ISODateTime.d.ts
@@ -0,0 +1,11 @@
+type Year = string;
+type Month = string;
+type Day = string;
+type Hour = string;
+type Minute = string;
+type Second = string;
+type Millisecond = string;
+type Offset = string;
+type ISODate = `${Year}-${Month}-${Day}`
+type ISOTime = `${Hour}:${Minute}:${Second}.${Millisecond}`
+export type ISODateTime = `${ISODate}T${ISOTime}${Offset}`