diff --git a/site/package.json b/site/package.json index af2abfa3b..15e648e44 100644 --- a/site/package.json +++ b/site/package.json @@ -12,7 +12,10 @@ "engines": { "node": ">=12.16.1" }, - "browserslist": ["> 1% in FR", "not ie < 11"], + "browserslist": [ + "> 1% in FR", + "not ie < 11" + ], "scripts": { "prepare": "node scripts/prepare.js", "build": "yarn run build:prod && yarn run build:legacy", @@ -40,6 +43,7 @@ }, "dependencies": { "@babel/runtime": "^7.3.4", + "@icons/material": "^0.4.1", "@internationalized/number": "^3.0.3", "@mui/material": "^5.0.4", "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest", @@ -188,6 +192,7 @@ "webpack-hot-middleware": "^2.24.2", "workbox-webpack-plugin": "^6.0.2", "worker-loader": "^2.0.0", + "xml2js": "^0.4.23", "yaml-loader": "^0.5.0" }, "optionalDependencies": { diff --git a/site/scripts/fetch-job-offers.js b/site/scripts/fetch-job-offers.js new file mode 100644 index 000000000..7ec6cac5b --- /dev/null +++ b/site/scripts/fetch-job-offers.js @@ -0,0 +1,38 @@ +// We publish our job offers on https://beta.gouv.fr/recrutement/. To augment +// their reach, we also publish a banner on our website automatically by using +// the beta.gouv.fr API. + +require('isomorphic-fetch') +const xml2js = require('xml2js') +const util = require('util') +const { createDataDir, writeInDataDir } = require('./utils.js') + +const parseXML = util.promisify(xml2js.parseString) + +main() + +async function main() { + createDataDir() + const jobOffers = await fetchJobOffers() + writeInDataDir('job-offers.json', jobOffers) +} + +async function fetchJobOffers() { + const response = await fetch('https://beta.gouv.fr/jobs.xml') + const content = await response.text() + // The XML API isn't the most ergonomic, we ought to have a JSON API. + // cf. https://github.com/betagouv/beta.gouv.fr/issues/6343 + const jobOffers = (await parseXML(content)).feed.entry + .map((entry) => ({ + title: entry.title[0]['_'].trim(), + link: entry.link[0].$.href, + content: entry.content[0]['_'].trim(), + })) + .filter(({ title }) => title.includes('Offre de Mon-entreprise')) + .map(({ title, ...rest }) => ({ + ...rest, + title: title.replace(' - Offre de Mon-entreprise', ''), + })) + + return jobOffers +} diff --git a/site/scripts/prepare.js b/site/scripts/prepare.js index ebf6bf168..2a60ee6bd 100644 --- a/site/scripts/prepare.js +++ b/site/scripts/prepare.js @@ -1,2 +1,3 @@ require('./fetch-releases.js') require('./fetch-stats.js') +require('./fetch-job-offers.js') diff --git a/site/source/components/layout/NewsBanner.tsx b/site/source/components/layout/NewsBanner.tsx index 29f63a0ae..87d873afd 100644 --- a/site/source/components/layout/NewsBanner.tsx +++ b/site/source/components/layout/NewsBanner.tsx @@ -2,12 +2,11 @@ import { useLocalStorage, writeStorage } from '@rehooks/local-storage' import { Appear } from 'Components/ui/animate' import Emoji from 'Components/utils/Emoji' import { SitePathsContext } from 'Components/utils/SitePathsContext' -import { Button } from 'DesignSystem/buttons' -import { GenericButtonOrLinkProps, Link } from 'DesignSystem/typography/link' +import lastRelease from 'Data/last-release.json' +import { Banner, HideButton, InnerBanner } from 'DesignSystem/banner' +import { Link } from 'DesignSystem/typography/link' import { useContext, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import lastRelease from '../../data/last-release.json' const localStorageKey = 'last-viewed-release' @@ -17,44 +16,6 @@ export const hideNewsBanner = () => { export const determinant = (word: string) => /^[aeiouy]/i.exec(word) ? 'd’' : 'de ' -const Container = styled.div` - display: flex; - width: 100%; - margin: auto; - align-items: center; - justify-content: center; - font-family: ${({ theme }) => theme.fonts.main}; -` - -const InnerContainer = styled.div` - display: flex; - margin: auto; - align-items: center; - justify-content: center; - padding: 0.5rem 1rem; - background-color: ${({ theme }) => theme.colors.bases.primary[100]}; - border: 2px solid; - border-color: ${({ theme }) => theme.colors.bases.primary[500]}; - border-radius: 0.375rem; -` - -const HideButton = styled(Button)` - display: flex; - align-items: center; - justify-content: center; - height: 1.5rem; - width: 1.5rem; - padding: 0; - background: ${({ theme }) => theme.colors.extended.grey[100]}; - color: ${({ theme }) => theme.colors.bases.primary[600]}; - font-weight: bold; - margin-left: 1rem; - - &:hover { - background: ${({ theme }) => theme.colors.bases.primary[300]}; - } -` - export default function NewsBanner() { const [lastViewedRelease] = useLocalStorage(localStorageKey) const sitePaths = useContext(SitePathsContext) @@ -77,8 +38,8 @@ export default function NewsBanner() { } return ( - - + + Découvrez les nouveautés{' '} {determinant(lastRelease.name)} @@ -89,8 +50,8 @@ export default function NewsBanner() { × - - + + ) } diff --git a/site/source/design-system/banner/index.ts b/site/source/design-system/banner/index.ts new file mode 100644 index 000000000..d1dddf7e9 --- /dev/null +++ b/site/source/design-system/banner/index.ts @@ -0,0 +1,41 @@ +import { Button } from 'DesignSystem/buttons' +import { GenericButtonOrLinkProps } from 'DesignSystem/typography/link' +import styled from 'styled-components' + +export const Banner = styled.div` + display: flex; + width: 100%; + margin: auto; + align-items: center; + justify-content: center; + font-family: ${({ theme }) => theme.fonts.main}; +` + +export const InnerBanner = styled.div` + display: flex; + margin: auto; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + background-color: ${({ theme }) => theme.colors.bases.primary[100]}; + border: 2px solid; + border-color: ${({ theme }) => theme.colors.bases.primary[500]}; + border-radius: 0.375rem; +` + +export const HideButton = styled(Button)` + display: flex; + align-items: center; + justify-content: center; + height: 1.5rem; + width: 1.5rem; + padding: 0; + background: ${({ theme }) => theme.colors.extended.grey[100]}; + color: ${({ theme }) => theme.colors.bases.primary[600]}; + font-weight: bold; + margin-left: 1rem; + + &:hover { + background: ${({ theme }) => theme.colors.bases.primary[300]}; + } +` diff --git a/site/source/pages/Stats/DemandesUtilisateurs.tsx b/site/source/pages/Stats/DemandesUtilisateurs.tsx index f5b95b510..2d14b0c0a 100644 --- a/site/source/pages/Stats/DemandesUtilisateurs.tsx +++ b/site/source/pages/Stats/DemandesUtilisateurs.tsx @@ -4,7 +4,7 @@ import { Li, Ul } from 'DesignSystem/typography/list' import { Body } from 'DesignSystem/typography/paragraphs' import { useState } from 'react' import styled from 'styled-components' -import stats from '../../data/stats.json' +import stats from 'Data/stats.json' export default function DemandeUtilisateurs() { return ( diff --git a/site/source/pages/Stats/Stats.tsx b/site/source/pages/Stats/Stats.tsx index 444b6ca53..98e057a09 100644 --- a/site/source/pages/Stats/Stats.tsx +++ b/site/source/pages/Stats/Stats.tsx @@ -13,7 +13,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { Trans } from 'react-i18next' import { useHistory, useLocation } from 'react-router-dom' import { toAtString } from '../../ATInternetTracking' -import statsJson from '../../data/stats.json' +import statsJson from 'Data/stats.json' import { debounce } from '../../utils' import { SimulateurCard } from '../Simulateurs/Home' import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata' diff --git a/site/source/pages/integration/index.tsx b/site/source/pages/integration/index.tsx index e333f940d..edd569d5d 100644 --- a/site/source/pages/integration/index.tsx +++ b/site/source/pages/integration/index.tsx @@ -9,10 +9,19 @@ import { TrackChapter } from '../../ATInternetTracking' import Iframe from './Iframe' import Library from './Library' import Options from './Options' +import jobOffers from 'Data/job-offers.json' +import { Banner, InnerBanner } from 'DesignSystem/banner' + +type JobOffer = { + title: string + link: string + content: string +} export default function Integration() { const sitePaths = useContext(SitePathsContext) const { pathname } = useLocation() + const openJobOffer = (jobOffers as Array)[0] return ( <> @@ -26,27 +35,19 @@ export default function Integration() { ← Outils pour les développeurs )} - {/* TODO: Nous pourrions automatiser la publication de cette bannière - de recrutement lorsqu'une annonce est postée sur beta.gouv.fr - https://github.com/betagouv/beta.gouv.fr/issues/6343 */} - {/*
- 📯{' '} - - - Mon-entreprise recrute ! - - {' '} - Freelance Typescript / React pour 6 mois minimum -
*/} + {openJobOffer && ( + + + + {' '} + + Mon-entreprise recrute ! + {' '} + {openJobOffer.title} + + + + )} diff --git a/site/tsconfig.json b/site/tsconfig.json index 3fc679807..113999ef5 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -18,7 +18,8 @@ "Reducers/*": ["reducers/*"], "Selectors/*": ["selectors/*"], "Types/*": ["types/*"], - "DesignSystem/*": ["design-system/*"] + "DesignSystem/*": ["design-system/*"], + "Data/*": ["data/*"] }, "typeRoots": ["./types/", "./node_modules/@types"], "noEmit": true, diff --git a/site/webpack.common.js b/site/webpack.common.js index bb914fc3e..edfe69197 100644 --- a/site/webpack.common.js +++ b/site/webpack.common.js @@ -113,6 +113,7 @@ module.exports.default = { Types: path.resolve('source/types/'), Images: path.resolve('source/static/images/'), DesignSystem: path.resolve('source/design-system'), + Data: path.resolve('source/data'), }, extensions: ['.js', '.ts', '.tsx'], }, diff --git a/yarn.lock b/yarn.lock index 920a1abda..32cdca69c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1243,6 +1243,11 @@ resolved "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== +"@icons/material@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.4.1.tgz#20ba0dff8d59a2b8749c7ad765faad52ae45b868" + integrity sha512-r4CuKUZv9GeAYvWc6WEVF0Xiw/IS4S50zna/M0/ISJOKe3RbpbHN3yBjX7ZnaPGqH/rm5SnDBv8FNOHLpM7OpQ== + "@internationalized/date@3.0.0-alpha.1": version "3.0.0-alpha.1" resolved "https://registry.npmjs.org/@internationalized/date/-/date-3.0.0-alpha.1.tgz" @@ -12557,7 +12562,7 @@ safe-regex@^1.1.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@~1.2.1: +sax@>=0.6.0, sax@~1.2.1: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -14909,6 +14914,19 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.1.1, xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz"