pull/2221/head
Jérémy Rialland 2022-06-30 19:32:57 +02:00 committed by Jérémy Rialland
parent fddc5cc22d
commit 15eceb3eee
14 changed files with 1911 additions and 187 deletions

3
site/.gitignore vendored
View File

@ -6,4 +6,5 @@ cypress/screenshots
cypress/downloads
.deps.json
netlify*.toml
source/public/sitemap.*.txt
source/public/sitemap.*.txt
dev-dist

View File

@ -142,9 +142,11 @@
"ts-morph": "^13.0.3",
"ts-node": "^10.8.0",
"typescript": "^4.7.2",
"vite": "^2.9.9",
"vite": "^2.9.13",
"vite-plugin-pwa": "^0.12.1",
"vite-plugin-shim-react-pdf": "^1.0.5",
"vitest": "^0.9.4",
"workbox-window": "^6.5.3",
"xml2js": "^0.4.23",
"yaml": "^1.9.2"
}

View File

@ -22,6 +22,7 @@ import { I18nextProvider } from 'react-i18next'
import { Provider as ReduxProvider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { CompatRouter } from 'react-router-dom-v5-compat'
import { ServiceWorker } from './ServiceWorker'
import * as safeLocalStorage from './storage/safeLocalStorage'
import { store } from './store'
import { inIframe } from './utils'
@ -30,26 +31,6 @@ import { inIframe } from './utils'
import { TrackingContext } from './ATInternetTracking'
import { createTracker } from './ATInternetTracking/Tracker'
if (
!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&
'serviceWorker' in navigator &&
!inIframe()
) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
// eslint-disable-next-line no-console
console.log('SW registered: ', registration)
})
.catch((registrationError) => {
// eslint-disable-next-line no-console
console.log('SW registration failed: ', registrationError)
})
})
}
type SiteName = 'mon-entreprise' | 'infrance' | 'publicodes'
export const SiteNameContext = createContext<SiteName | null>(null)
@ -100,6 +81,10 @@ export default function Provider({
</Container>
}
>
{!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&
'serviceWorker' in navigator &&
!inIframe() && <ServiceWorker />}
<OverlayProvider>
<ReduxProvider store={store}>
<IsEmbeddedProvider>

View File

@ -0,0 +1,103 @@
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { useRegisterSW } from 'virtual:pwa-register/react'
import { Message } from './design-system'
import { HideButton } from './design-system/banner'
import { Button } from './design-system/buttons'
import { Body } from './design-system/typography/paragraphs'
const PromptContainer = styled.div`
position: fixed;
bottom: 0;
right: 0;
z-index: 10000;
min-height: initial !important;
`
const StyledSmallBody = styled(Body)`
margin: 0.5rem 0.75rem 0.5rem 0;
`
const StyledMessage = styled(Message)`
margin: 0 0.5rem 0.5rem 0.5rem !important;
max-width: 450px;
${Message.Wrapper} {
display: flex;
flex-direction: column;
align-items: stretch;
}
`
const StyledHideButton = styled.div`
position: absolute;
top: 0.375rem;
right: 0.375rem;
`
export const ServiceWorker = () => {
const { t } = useTranslation()
const {
offlineReady: [offlineReady, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegistered: (r) => {
// eslint-disable-next-line no-console
console.log('=> SW Registered: ', r)
},
onRegisterError: (error) => {
// eslint-disable-next-line no-console
console.log('SW registration error', error)
},
})
return (
<PromptContainer>
{offlineReady && (
<StyledMessage type="info">
<StyledSmallBody>
<Trans>L'application est prête à fonctionner hors ligne.</Trans>
</StyledSmallBody>
<StyledHideButton>
<HideButton
onClick={() => setOfflineReady(false)}
aria-label={t('Fermer')}
>
&times;
</HideButton>
</StyledHideButton>
</StyledMessage>
)}
{needRefresh && (
<StyledMessage type="info">
<StyledSmallBody>
<Trans>
Nouveau contenu disponible, cliquez sur recharger pour mettre à
jour la page.
</Trans>{' '}
<Button
light
size="XXS"
onClick={() => void updateServiceWorker(true)}
>
{t('Recharger')}
</Button>
</StyledSmallBody>
<StyledHideButton>
<HideButton
onClick={() => setNeedRefresh(false)}
aria-label={t('Fermer')}
>
&times;
</HideButton>
</StyledHideButton>
</StyledMessage>
)}
</PromptContainer>
)
}

View File

@ -39,7 +39,9 @@ export default function Banner({
<FadeIn>
<Container className={className}>
<Emoji emoji={icon} />
<Content>{children}</Content>
<Content as={typeof children === 'string' ? undefined : 'div'}>
{children}
</Content>
</Container>
</FadeIn>
) : null

View File

@ -7,7 +7,7 @@ import { wrapperDebounceEvents } from '@/utils'
import React, { ForwardedRef, forwardRef } from 'react'
import styled, { css } from 'styled-components'
type Size = 'XL' | 'MD' | 'XS'
type Size = 'XL' | 'MD' | 'XS' | 'XXS'
type Color = 'primary' | 'secondary' | 'tertiary'
type ButtonProps = GenericButtonOrLinkProps & {
@ -69,6 +69,9 @@ export const StyledButton = styled.button<StyledButtonProps>`
if ($size === 'XS') {
return '0.5rem 2rem'
}
if ($size === 'XXS') {
return '0.25rem 1rem'
}
}};
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
width: 100%;

View File

@ -35,6 +35,7 @@ export const Label = styled.label`
}
`}
`
interface ButtonProps {
isFocusVisible?: boolean
}

View File

@ -14,6 +14,7 @@ type MessageProps = {
border?: boolean
type?: MessageType
light?: boolean
className?: string
}
export function Message({
@ -22,6 +23,7 @@ export function Message({
border = true,
light = false,
children,
className,
}: MessageProps) {
if (typeof children !== 'object') {
children = <Body>{children}</Body>
@ -29,7 +31,12 @@ export function Message({
return (
<ThemeProvider theme={(theme) => ({ ...theme, darkMode: false })}>
<StyledMessage type={type} border={border} light={light}>
<StyledMessage
className={className}
type={type}
border={border}
light={light}
>
{icon &&
(type === 'success' ? (
<StyledIcon
@ -56,13 +63,7 @@ export function Message({
alt="icône signalant un texte informatif"
/>
))}
<div
css={`
flex: 1;
`}
>
{children}
</div>
<Wrapper>{children}</Wrapper>
</StyledMessage>
</ThemeProvider>
)
@ -111,3 +112,9 @@ const StyledIcon = styled.img`
margin-top: calc(${theme.spacings.lg});
`}
`
const Wrapper = styled.div`
flex: 1;
`
Message.Wrapper = Wrapper

View File

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -1,21 +0,0 @@
{
"name": "Mon entreprise",
"short_name": "Mon entreprise",
"description": "L'assistant officiel du créateur d'entreprise",
"display": "standalone",
"lang": "fr",
"orientation": "portrait-primary",
"theme_color": "#2975d1",
"icons": [
{
"src": "/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -1,8 +0,0 @@
// A simple, no-op service worker that takes immediate control.
self.addEventListener('install', () => {
// Skip over the "waiting" lifecycle state, to ensure that our
// new service worker is activated immediately, even if there's
// another tab open controlled by our older service worker code.
self.skipWaiting()
})

View File

@ -3,30 +3,46 @@
<head>
<meta charset="utf-8" />
<base href="/" />
<meta name="viewport" content="initial-scale=1" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" href="/favicon/favicon.svg?v=1.0" type="image/svg+xml" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon/favicon-16x16.png?v=1.0"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon/favicon-32x32.png"
href="/favicon/favicon-32x32.png?v=1.0"
/>
<link
rel="icon"
rel="alternate icon"
href="/favicon/favicon.ico?v=1.0"
type="image/png"
sizes="16x16"
href="/favicon/favicon-16x16.png"
/>
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="msapplication-TileColor" content="#da532c" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png?v=1.0"
/>
<link
rel="mask-icon"
href="/favicon/safari-pinned-tab.svg?v=1.0"
color="#98deed"
/>
<link rel="shortcut icon" href="/favicon/favicon.ico?v=1.0" />
<meta name="apple-mobile-web-app-title" content="Mon-entreprise" />
<meta name="application-name" content="Mon-entreprise" />
<meta name="msapplication-TileColor" content="#603cba" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<title>{{ title }}</title>
<!--app-helmet-tags:start-->
<meta name="description" content="{{ description }}" />
<meta property="og:type" content="website" />
@ -35,8 +51,6 @@
<meta property="og:image" content="{{ shareImage }}" />
<!--app-helmet-tags:end-->
<link rel="manifest" href="/manifest.webmanifest" />
<style>
html[data-useragent*='MSIE'] #outdated-browser,
html[data-useragent*='Safari'][data-useragent*='Version/8']

View File

@ -8,8 +8,13 @@ import fs from 'fs/promises'
import path from 'path'
import serveStatic from 'serve-static'
import { defineConfig, loadEnv, Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import shimReactPdf from 'vite-plugin-shim-react-pdf'
const HOUR = 60 * 60
const DAY = HOUR * 24
const YEAR = DAY * 365
const env = (mode: string) => loadEnv(mode, process.cwd(), '')
export default defineConfig(({ command, mode }) => ({
@ -57,10 +62,121 @@ export default defineConfig(({ command, mode }) => ({
},
},
}),
VitePWA({
registerType: 'prompt',
workbox: {
cleanupOutdatedCaches: true,
clientsClaim: true,
skipWaiting: true,
sourcemap: true,
runtimeCaching: [
{
urlPattern: (options) => {
if (
!(
options.sameOrigin &&
options.url.pathname.startsWith('/twemoji/')
) &&
!(
options.sameOrigin &&
options.url.pathname.startsWith('/twemoji/')
)
) {
console.log('=>', options.url.pathname)
}
return (
options.sameOrigin && options.url.pathname.startsWith('/fonts/')
)
},
handler: 'CacheFirst',
options: {
cacheName: 'fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 1 * YEAR,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: (options) => {
return (
options.sameOrigin &&
options.url.pathname.startsWith('/twemoji/')
)
},
handler: 'CacheFirst',
options: {
cacheName: 'twemoji-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 1 * YEAR,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: (options) => {
return (
!options.sameOrigin && options.url.hostname === 'polyfill.io'
)
},
handler: 'CacheFirst',
options: {
cacheName: 'external-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 1 * DAY,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
srcDir: 'public/favicon',
includeAssets: [
'favicon.svg',
'favicon.ico',
'robots.txt',
'apple-touch-icon.png',
],
manifest: {
name: 'Mon entreprise',
short_name: 'Mon entreprise',
description: "L'assistant officiel du créateur d'entreprise",
lang: 'fr',
orientation: 'portrait-primary',
icons: [
{
src: '/favicon/android-chrome-192x192.png?v=1.0',
sizes: '192x192',
type: 'image/png',
},
{
src: '/favicon/android-chrome-512x512.png?v=1.0',
sizes: '512x512',
type: 'image/png',
},
],
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
},
}),
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
esbuild: {
logOverride: { 'this-is-undefined-in-esm': 'silent' }, // will be fixed in next version of @vitejs/plugin-react (actualy 1.3.2), issue https://github.com/vitejs/vite/pull/8674
},
server: {
hmr: {
clientPort:
@ -138,7 +254,7 @@ function multipleSPA(options: MultipleSPAOptions): Plugin {
return next()
}
if (url === '/') {
if (url && ['/', '/index.html'].includes(url)) {
res.writeHead(302, { Location: '/' + options.defaultSite }).end()
} else if (
firstLevelDir &&

1746
yarn.lock

File diff suppressed because it is too large Load Diff