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 ( + + ); +}; 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}`