Concepteur développeur d'applications
16 mai 2022
Yann Loosli
IDENTITIES
> JWT
> PASSPORT
> NODEJS
> EXPRESS
> MONGOOSE
MONGODB
GROUPS
> NODEJS
> EXPRESS
MYSQL
MESSAGES
> ???
???
MEMORY
> NODEJS
> EXPRESS
MYSQL
MAIL
> MJML
> NODEMAI-
LER
BUBBLES
> Y.JS
EVENT BUS
> NATS
API GATEWAY
> APOLLO GRAPHQL
BACK-END
FRONT-END
WEBSITE
> REACT
> REACT ROUTER
> CHAKRA UI
COOKIES
WEB EXTENSION
> REACT
LOCAL STORAGE
MOBILE APPLICATION
> REACT NATIVE
> EXPO
Background
Action
Option
Script contents
DevTools
+ F12
Web Ext.
Tabs
Tabs
Tabs
Tabs...
Background
Server
Action (popup)
Script Contents
DevTools
Options
LOGIN
LOGOUT
(menu ?)
1 - login
2 - JWT dans localstrage
3 - Met le JWT
dans le store REDUX
STORE REDUX
(main)
4 - WebSocket & GraphQl
Browser API
REDUX WebExt
LOCAL STORAGE
G
o
o
g
l
e
G
o
o
g
l
e
G
o
o
g
l
e
{
"manifest_version": 2,
"short_name": "Pygma.Link Web Extension",
"name": "Pygma.Link Web Extension",
"version": "0",
"description": "to do",
"content_security_policy": "
script-src 'self' localhost:*;
object-src 'self';
connect-src 'self' ws://localhost:1236/ ws://localhost:4000/ http://localhost:4000/;
",
"author": "PygmaTeam",
"web_accessible_resources": [
"**/*.png",
"**/*.svg",
"excalidraw-assets-dev/*.*",
"excalidraw-assets/*.*"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"match_about_blank": true,
"css": [
"parts/scriptContent/styles/style.scss"
],
"js": [
"parts/scriptContent/var.ts",
"parts/scriptContent/entry.tsx"
]
}
],
"browser_action": {
"default_icon": "./assets/logo192.png",
"default_title": "PYGMA.LINK - Menu",
"default_popup": "./parts/action/popup.html"
},
"devtools_page": "./parts/devtools/devtools.html",
"options_ui": {
"page": "./parts/option/option.html"
},
"background": {
"scripts": ["./parts/background/entry.ts"],
"persistent": true
},
"permissions": [
"tabs",
"webNavigation",
"<all_urls>"
]
}
manifest.json ( )
import React, { FC, useEffect, useState, useRef, RefObject, useLayoutEffect } from 'react';
import { motion } from 'framer-motion';
import { browser } from 'webextension-polyfill-ts';
import App from './Draw';
import SnailMenu from './SnailMenu';
import '../styles/style.scss';
import { CollabContextConsumer } from '../compFct/drawCollabWrapper';
const Overlay: FC = () => {
const [online, setOnline] = useState(true);
const [drawBoard, setDrawBoard] = useState(false);
const [snailMenuOpen, setSnailMenuOpen] = useState(false);
const constraintsRef = useRef<RefObject<Element> & HTMLDivElement>(null);
// send connexion data to background
const port = browser.runtime.connect();
useEffect(() => {
port.postMessage({type: 'PAGELOAD'});
const waitLogin = setInterval(() => {
port.postMessage({
type: 'ISAUTH',
tab: '',
url: window.location.href,
group: '',
data: {}
});
}, 1000);
port.onMessage.addListener((message) => {
console.log(message)
if (online === false && message.type === 'ISAUTH' && message.data.state === true) {
setOnline(true);
clearInterval(waitLogin)
}
});
}, [])
return (
<CollabContextConsumer>
{online && (
<motion.div className="Overlay" ref={constraintsRef} style={{ opacity: online ? 1 : 0 }}>
<motion.div
className="Snail"
drag
dragPropagation
dragMomentum={false}
dragConstraints={constraintsRef}
style={{
transformOrigin: '50% 50%',
zIndex: 100,
position: 'absolute',
left: '50px',
top: 'calc(100vh - 100px)',
}}
animate={
!snailMenuOpen
? {
width: '70px',
height: '70px',
transition: {
delay: 0,
duration: 0,
},
}
: {}
}
>
<SnailMenu setOpen={setSnailMenuOpen} open={snailMenuOpen} setDrawBoard={setDrawBoard} drawBoard={drawBoard} />
</motion.div>
{drawBoard && <App />}
</motion.div>
)}
</CollabContextConsumer>
);
};
export default Overlay;
Overlay.tsx
import React, { ReactElement, ReactNode } from 'react';
import { motion } from 'framer-motion';
type SnailProps = {
open: boolean;
children: ReactNode;
};
const snailSections = [
"M111.061 140.676a4.091 4.091 0 01-5.321 2.27 4.091 4.091 0 01-2.271-5.32 4.091 4.091 0 015.321-2.272 4.091 4.091 0 012.272 5.321",
"M119.693 139.429c-5.05 10.642-17.555 6.165-16.518-1.01-.305 4.438 6.379 7.599 8.03 1.057z",
. . .
]
const snailForm = "m 103.17507,138.41902 c -0.84386,3.46277 3.21063,7.09831 6.85103,. . . "
const snailFormParams = [{ stroke: '#cccccc', strokeOpacity: 0.3, }, { strokeOpacity: . . . ]
const Snail = (props: SnailProps): ReactElement => {
const { open, children } = props;
const variants = {
open: (custom: number) => ({
strokeWidth: 0.5,
strokeOpacity: 1,
zIndex: 13 - custom,
opacity: 1,
transition: {
delay: custom * 0.04,
ease: 'easeInOut',
duration: 0.25,
},
scale: [0, 1.2, 1],
filter: ['blur(1px)', 'blur(.5px)', 'blur(0)'],
}),
closed: (custom: number) => ({
strokeWidth: 0,
strokeOpacity: [1, 1, 0],
opacity: [1, 1, 0],
transition: {
delay: custom * 0.02,
ease: 'easeInOut',
duration: 0.1,
},
scale: [1, 1.2, 0],
filter: ['blur(.25px)', 'blur(.5px)', 'blur(2px)'],
}),
};
return (
<>
<svg
width={367.681}
height={410.366}
viewBox="0 0 97.282 108.576"
id="prefix__svg5"
xmlns="http://www.w3.org/2000/svg"
{...props}
style={{
position: 'absolute',
}}
>
<defs>
<linearGradient id="linear" x1="138.17851" y1="182.58768" x2="68.166489" y2="100.48698">
<stop offset="0%" stopColor="#cacaca" />
<stop offset="100%" stopColor="#f0f0f0" />
</linearGradient>
</defs>
<g id="snail" transform="translate(-54.794 -87.071)" fillOpacity={1} fill="url(#linear)" stroke="url(#linear)">
<g>
{snailSections.map((section, index) => (
<motion.path
key={`snailPart_${index}`}
id={`snailPart_${index}`}
d={section}
fill="url(#linear)"
opacity={0.9}
animate={open ? 'open' : 'closed'}
variants={variants}
custom={open ? index : section.length - index}
/>
))}
</g>
</g>
<g transform="translate(-54.794 -87.071)">
{
snailFormParams.map((el, index) => (
<motion.path
id={`path_${index}`}
key={`path_${index}`}
style={{
fill: 'none',
stroke: el.stroke || '#000',
strokeWidth: el.strokeWidth || '0.25px',
strokeLinecap: 'butt',
strokeLinejoin: 'miter',
strokeOpacity: el.strokeOpacity || 1,
filter: el.filter || '',
scale: el.scale || 1,
}}
d={snailForm}
initial={{ pathLength: open ? 0 : 1 }}
animate={{
pathLength: open ? 1 : 0,
transition: {
ease: 'easeInOut',
duration: open ? 0.5 : 1,
},
}}
/>
))
}
</g>
</svg>
{children}
</>
);
};
export default Snail;
Snail.tsx
?
?
?
?
> connexion <
inscription
présentation
> connexion <
inscription
présentation
Besoin d'aide ?
- Assistance
- Téléchargement
- FAQ
Button
Valider
CONNEXION
INSCRIPTION
mail
mail
mdp
mdp
mdp bis
CGU ?
CGV ?
RGPD ?
export const userSchema = new Schema(
{
mail: {type: String, unique: true},
password: String,
status: {
type: String,
enum: ['Pending', 'Active'],
default: 'Pending'
},
confirmationProcess: [confirmationProcess],
contractAgreement: [{
docId: String,
version: String,
validationDate: {type: Number, default: Math.floor(Date.now() / 1000)}
}],
paymentHistory: [{
}],
profils: [{
nickname: {type: String, unique: true},
groups: [{
id: String
}]
}],
type: {
type: String,
default: 'free',
enum: ['free', 'star', 'premium', 'pro', 'pro+'],
},
typeStartDate: {type: Number, default: Math.floor(Date.now() / 1000)},
typeEndDate: {type: Number, default: Math.floor(Date.now() / 1000)},
pages: [{
id: String,
title: String,
creationDate: {type: Number, default: Math.floor(Date.now() / 1000)},
enabled: {type: Boolean, default: false},
accessibility: [String]
}],
loggedOutAt: Number,
createdAt: Number,
updatedAt: Number,
},
{
timestamps: { currentTime: () => Math.floor(Date.now() / 1000) }
}
);
Schéma Mongoose USER
1 const LOGIN = gql`
2 query login($mail: String, $password: String) {
3 login(mail: $mail, password: $password) {
4 _id
5 }
6 }
7 `
La requête GraphQL
1 const login = async (parent, args, context, info) => {
2 const result = await post('login', args)
3 if (!result)
4 return {
5 success: false,
6 message: 'failed to login',
7 }
8
9 context.setCookies.push({
10 name: 'jwt_sign',
11 value: result.data.jwt_sign,
12 options: {
13 domain: 'localhost',
14 httpOnly: true,
15 path: '/',
16 sameSite: true,
17 secure: true,
18 },
19 })
20 context.setHeaders.push({ key: 'jwt_data', value: result.data.jwt_data })
21
22 return {
23 _id: result.data._id,
24 }
25 }
Depuis le service « API »
1 export default (router: Router): void => {
2
3 router.post(
4 '/login',
5 (request, response) => {
6 passport.authenticate("local",{session: false}, (err, user, info) => {
7 if (err || !user) {
8 return response.json({
9 message: info,
10 user : user
11 });
12 }
13 request.login(user, {session: false}, (err) => {
14 if (err) {
15 response.send(err);
16 }
17
18 const payload = {
19 id: user._doc._id,
20 type: user._doc.type,
21 }
22
23 const token = jwt.sign(payload, process.env.JWT_SECRET)
24 const tokenSplit = token.split('.')
25
26
27 nats('LOGIN', JSON.stringify({
28 id: user._doc._id,
29 type: user._doc.type,
30 mail: user._doc.mail,
31 }))
32
33 return response.json({
34 ...user._doc,
35 jwt_sign: tokenSplit[2],
36 jwt_data: tokenSplit[0] + '.' + tokenSplit[1]
37 });
38 })
39 }) (request, response)
40 }
41 )
42 }
Depuis le service « identities »
1 context.setCookies.push({
2 name: 'jwt_sign',
3 value: result.data.jwt_sign,
4 options: {
5 domain: 'localhost',
6 httpOnly: true,
7 path: '/',
8 sameSite: true,
9 secure: true,
10 },
11 })
12 context.setHeaders.push({ key: 'jwt_data', value: result.data.jwt_data })
Envoi des informations au client dans le header
1 if (req.headers.cookie && req.headers.cookie.length > 0) {
2 let cookiesRaw = req.headers.cookie?.split('; ')
3 cookiesRaw = cookiesRaw?.filter(cookie => cookie.trim().slice(0, 3) === 'jwt')
4 if (cookiesRaw.length === 2) {
5 let tokenWip = { jwt_data: '', jwt_sign: '' }
6 cookiesRaw?.forEach(cookie => {
7 const pieces = cookie.split('=')
8 tokenWip = { ...tokenWip, [pieces[0]]: pieces[1] }
9 })
10 if (tokenWip.jwt_data !== (null || undefined)) {
11 const token = `${tokenWip.jwt_data}.${tokenWip.jwt_sign}` || ''
12 userId = jwt.verify(token, jwtSecret)
13 ? JSON.parse(atob(tokenWip.jwt_data.split('.')[1])).id
14 : ''
15 }
16 }
17 }
Réception de la requête du client
1 import React from 'react';
2 import {View} from 'react-native';
3
4 function HomeScreen() {
5 return (
6 < View />
7 );
8 }
9
10 export default HomeScreen;
Etape 1 : placeholder
1 import React from 'react';
2 import {render} from '@testing-library/react-native';
3 import HomeScreen from '../HomeScreen';
4
5 describe('HomeScreen', () => {
6 test('should render correctly', () => {
7 const wrapper = render(GlobalWrapper(<HomeScreen />));
8 wrapper.getByTestId('home-screen');
9 });
10 });
Etape 2 : mise en place du test
1 import React from 'react';
2 import {View} from 'react-native';
3
4 function HomeScreen() {
5 return (
6 < View testID="home-screen" />
7 );
8 }
9
10 export default HomeScreen;
Etape 3 : compléter le code pour répondre au test
1 import React from 'react';
2 import {Flex} from 'native-base';
3 import Logo from '../components/Logo';
4 import LoginForm from '../components/LoginForm';
5
6 function HomeScreen() {
7 return (
8 <Flex
9 testID="home-screen"
10 bg={{
11 linearGradient: {
12 colors: ['pygmaBlue.300', 'dark.900', 'pygmaOrange.500'],
13 start: [0, 0],
14 end: [1, 0],
15 },
16 }}
17 h="100%"
18 alignItems="center"
19 justifyContent="space-around">
20 <Logo />
21 <LoginForm />
22 </Flex>
23 );
24 }
Etape 5 : le code final
1 import React from 'react';
2 import {render} from '@testing-library/react-native';
3 import LoginForm from '../../components/LoginForm';
4 import HomeScreen from '../HomeScreen';
5 import {View} from 'react-native';
6 import GlobalWrapper from '../../fixtures/GlobalWrapper';
7
8 jest.mock('../../components/LoginForm', () => jest.fn().mockReturnValue(null));
9
10 describe('HomeScreen', () => {
11 test('should render correctly', () => {
12 const wrapper = render(GlobalWrapper(<HomeScreen />));
13 wrapper.getByTestId('home-screen');
14 });
15
16 test('should have a logo', () => {
17 const wrapper = render(GlobalWrapper(<HomeScreen />));
18 wrapper.getByTestId('logo');
19 });
20
21 test('should have a login form', () => {
22 (LoginForm as jest.Mock).mockReturnValue(<View testID="mock-login-form" />);
23 const wrapper = render(GlobalWrapper(<HomeScreen />));
24 wrapper.getByTestId('mock-login-form');
25 });
26 });
Etape 4 : le test final
1 variables:
2 DEV_PATH: /var/www/entreloups.com/kivi1
3
4 stages:
5 - dev
6
7 dev:
8 stage: dev
9 only:
10 - /^release\/dev$/
11 tags:
12 - kivi1dev
13 script:
14 - cp $DEV_PATH/.env.local .
15 - cp $DEV_PATH/.env.prod.local .
16 - test -f composer.lock && rm composer.lock
17 - test -d vendor && rm vendor -rf
18 - test -d node_modules && rm node_modules -rf
19 - /usr/bin/php /usr/local/bin/composer install
20 - /usr/bin/yarn
21 - /usr/bin/php bin/console fos:js-routing:dump --format=json –target=public/js/fos_js_routes.json
22 - /usr/bin/yarn build
23 - cd $DEV_PATH
24 - /usr/bin/php bin/console doctrine:cache:clear-metadata
25 - /usr/bin/php bin/console doctrine:cache:clear-query
26 - /usr/bin/php bin/console doctrine:cache:clear-result
27 - /usr/bin/php bin/console doctrine:migrations:migrate -n
28 - /usr/bin/php bin/console cache:clear
Extraits du fichier de déploiement .gitlab-ci.yml :
push
dépôt
local
distant
clone
serveur
preprod
dépôt
gitlab
runner
Gitlab
version: '3'
services:
apache-php:
build: .docker/dev
container_name: kivi1_apache_php
working_dir: /usr/app/
ports:
- "80:80"
- "25:25"
volumes:
- ./node_modules:/usr/app/node_modules
- ./vendor:/usr/app/vendor
- ./antivirus:/usr/app/antivirus
- ./assets:/usr/app/assets
- ./src:/usr/app/src
- ./bin:/usr/app/bin
- ./client:/usr/app/client
- ./config:/usr/app/config
- ./migrations:/usr/app/migrations
- ./public:/usr/app/public
- ./templates:/usr/app/templates
- ./translations:/usr/app/translations
- ./package.json:/usr/app/package.json
- ./composer.json:/usr/app/composer.json
- ./tsconfig.json:/usr/app/tsconfig.json
- ./.babelrc:/usr/app/.babelrc
- ./.env:/usr/app/.env
- ./.env.dev:/usr/app/.env.dev
- ./.env.dev.local:/usr/app/.env.dev.local
- ./webpack.config.js:/usr/app/webpack.config.js
- ./yarn.lock:/usr/app/yarn.lock
depends_on:
- mysql
command:
- apache2-foreground & composer install &&
php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json &&
php bin/console c:cl &&
php bin/console d:m:m &&
yarn &&
yarn watch
mysql:
image: mysql:5.7
container_name: kivi1_mysql
command: "--default-authentication-plugin=mysql_native_password"
ports:
- 3306
volumes:
- .docker/dev/data/db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: nOts0secur3d
MYSQL_DATABASE: kivi1
MYSQL_USER: kivi1Admin
MYSQL_PASSWORD: nOtMuchs0secured31ther
docker-compose-dev.yml :
FROM php:8.1-apache
RUN a2enmod rewrite
[ . . . ]
## Install composer globally
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer -d=/var/www
[ . . . ]
#NPM / node
RUN apt-get install -y curl \
&& curl -sL https://deb.nodesource.com/setup_12.x | bash - \
&& apt-get install -y nodejs \
&& curl -L https://www.npmjs.com/install.sh | sh
ENV PATH $PATH:/node_modules/.bin
RUN npm install --global yarn
[ . . . ]
COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
COPY php.ini /usr/local/etc/php/php.ini
COPY error_reporting.ini /usr/local/etc/php/conf.d/error_reporting.ini
COPY kivi1.dev.conf /etc/apache2/sites-available/000-default.conf
ENTRYPOINT [ "/bin/sh", "-c" ]
extrait de dockerfile
hôte
conteneur
volumes
1 // Flex is a based component of Chrakra UI
2 // This version had some option which are
3 // required for Kivi1 such as sticky position
4 // option and responsive behavior.
5
6 import React, { memo, ReactElement } from "react";
7 // Import original component and rename it as we need to use the Flex name
8 import { Flex as SrcFlex, ResponsiveValue } from "@chakra-ui/react";
9
10 const Flex = (props: { [x: string]: any; width: string; flexDirection: ResponsiveValue<any>; isSticky: boolean; children: ReactElement | string | null; }) => {
11 // Extract some props
12 const { width, flexDirection, isSticky, children, ...otherProps } = props;
13
14 return (
15 <SrcFlex
16 width={{ lg: width ? width : "100%", base: "100%" }}
17 flexDirection={{ md: flexDirection ? flexDirection : "column", base: "column" }}
18 top={isSticky ? 0 : "auto"}
19 zIndex={isSticky ? 1000 : "auto"}
20 pos={isSticky ? "sticky" : "inherit"}
21 {...otherProps}
22 >
23 {children}
24 </SrcFlex>
25 );
26 };
27
28 export default memo(Flex);
Composant Flex responsive
[...]
7 const ConditionnalDisplay = (props) => {
8 const { children, guests, milestones, adaptative, moments, ...otherProps } = props;
9 const { contacts, params } = useContext(RendererContext) ;
10 let date;
11 let guest;
12 let momentList;
13 let guestList;
14 let milestoneList;
15 let adaptativeList;
16
17 // Children components must be display by default
18 const [shouldRender, setShouldRender] = useState(true);
19
20 const [forDesktop, forTablet, forMobile] = useMediaQuery([
21 '(min-width: 1281px)',
22 '(min-width: 481px) and (max-width: 1280px)',
23 '(max-width: 480px)',
24 ]);
25
26 useEffect(() => {
27 // renderer mode ?
28 if(contacts && Object.keys(contacts).length > 0 &&
29 params && Object.keys(params).length > 0) {
30 date = params.event.date;
31 guest = contacts ? 'isUnknown' : 'isAuth';
32 momentList = moments.split(',');
33 momentList.shift();
34 guestList = guests.split(',');
35 guestList.shift();
36 milestoneList = milestones.split(',');
37 milestoneList.shift();
38 adaptativeList = adaptative.split(',');
39 adaptativeList.shift();
40
41 // conditionnal display on milestone attendancy
42 let tfIds = [];
43 if (contacts) {
44 const tfPrinc = contacts.principal.tempsfort.list.map(cur => cur.invitation !== null && cur.invitation.etatReponse === 3 ? cur.id.toString() : '');
45 const tfAcc = contacts.accompagnant
46 ?
47 contacts.accompagnant.reduce(((accumulator, currentValue) => {
48 let tmp = [];
49 if (contacts.accompagnant.length !== 0) {
50 tmp = currentValue.tempsfort.list.map(cur => {
51 if (cur.list !== null && cur.list.invitation && cur.list.invitation.etatReponse === 3) cur.id.toString();
52 });
53 }
54 return [...accumulator, ...tmp];
55 }), [])
56 :
57 '';
58 tfIds = [...tfPrinc, ...tfAcc].filter((a) => a);
59 }
60
61 const tfsValid = () => {
62 let tmp = false;
63 if(tfIds.length > 0) {
64 tfIds.forEach((element) => {
65 if (milestoneList.includes(element)) {
66 tmp = true;
67 }
68 });
69 }
70 return tmp;
71 };
72
73 // conditionnal display on time (before, during, after)
74 const eventState = isFinish(date.end) ? 'after' : isWaiting(date.start) ? 'before' : 'during';
75
76 // conditionnal display on connexion status (guest or not)
77 const guestState = guest;
78
79 if (
80 !(forMobile && adaptativeList.includes('mobile') || forTablet && adaptativeList.includes('tablet') || forDesktop && adaptativeList.includes('desktop') || adaptativeList.length === 0) ||
81 !(moments.includes(eventState) || moments.length === 0) ||
82 !(guestList.includes(guestState) || guestList.length === 0) ||
83 !(tfIds && tfsValid() || (milestoneList.length === 0)) ||
84 !(Object.keys(contacts).length === 0)
85 ){
86 setShouldRender(false);
87 } else { setShouldRender(true);}
88 }
89 }, [contacts, forMobile, forTablet, forDesktop]);
90
91 return shouldRender ? <Box {...otherProps}>{children}</Box> : <></>;
92 };
93
94 export default ConditionnalDisplay;
Composant Affichage conditionnel
1 useEffect(() => {
2 if (slug && type && languages) {
3 (async () => {
4 const [media, contact, documents] = ([
5 await getMedia(slug),
6 await getContact(slug),
7 await getDocuments(slug),
8 ]);
9 const availableModules = await getAvailableModules(slug, type, params.event.abonnement);
10 const lang = params?.event?.langDefaut;
11 if (lang && lang !== 'fr') {
12 await i18n.changeLanguage(lang);
13 }
14 setAppData({
15 availableModules,
16 type,
17 slug,
18 subType,
19 params,
20 media,
21 contact,
22 documents,
23 multilang
24 });
25 !isLoaded && setIsLoaded(true);
26 })();
27 }
28 }, [slug, type, languages]);
Optimisation
AVANT
1 useEffect(() => {
2 // load medias and other unnecesary data at start
3 if (slug && type && languages && appData !== undefined && !appData.media){
4 void (async () => {
5 const [media, contact, documents] = ([
6 await getMedia(slug),
7 await getContact(slug),
8 await getDocuments(slug),
9 ]);
10 setAppData({
11 ...appData,
12 media,
13 contact,
14 documents,
15 });
16 setAreAssetsLoaded(true); // <-- new state for assets
17 })();
18 }
19 // separate loading of necesary data at start
20 if (slug && type && languages && appData === undefined) {
21 void (async () => {
22 const Lang = params?.event?.langDefaut;
23 if (Lang && Lang !== 'fr') {
24 await i18n.changeLanguage(lang);
25 }
26 setAppData({
27 type,
28 slug,
29 subType,
30 params,
31 });
32 if(!isLoaded) setIsLoaded(true);
33 })();
34 }
35 }, [slug, type, languages, appData]);
APRES
1 useEffect(() => {
2 const getData = async () => {
3 const resp = await page(content[Lang].pages['page-0']);
4 setData(resp);
5 };
6 if(components.root.children.length > 0) void getData();
7 }, [components, Lang]);
Renderer minisite
Récupérer les données
1 const Page = (props: { content: string; }) => {
2 return (
3 <JsxParser
4 jsx={props.content}
5 showWarnings={true}
6 renderInWrapper={false}
7 autoCloseVoidElements={true}
8 // @ts-expect-error Seems like component types are not recognized or typed as awaited
9 components={components}
10 />);
11 };
12
13 export default memo(Page);
Interpréter les données
1 if(array_key_exists('borderW', $p) ||
2 array_key_exists('borderTW', $p) ||
3 array_key_exists('borderRW', $p) ||
4 array_key_exists('borderBW', $p) ||
5 array_key_exists('borderLW', $p)
6 ) {
7 $borderColor = array_key_exists('borderColor', $p) ?
8 $this->chakraColorTranslator($p['borderColor'], $content) : '#000';
9 $borderStyle = array_key_exists('borderStyle', $p) ? $p['borderStyle'] : 'solid';
10 $tbSectionAttr .= ' border-color="' . $borderColor . '" ';
11 $tbSectionAttr .= ' border-style="' . $borderStyle . '" ';
12 $borderW = array_key_exists('borderW', $p) ? $p['borderW'] : '';
13 $borderTW = array_key_exists('borderTW', $p) ? $p['borderTW'] : $borderW;
14 $borderRW = array_key_exists('borderRW', $p) ? $p['borderRW'] : $borderW;
15 $borderBW = array_key_exists('borderBW', $p) ? $p['borderBW'] : $borderW;
16 $borderLW = array_key_exists('borderLW', $p) ? $p['borderLW'] : $borderW;
17 $tbSectionAttr .= ' border-top="' . $borderTW . ' ' . $borderStyle . ' ' . $borderColor . '"';
18 $tbSectionAttr .= ' border-right="' . $borderRW . ' ' . $borderStyle . ' ' . $borderColor . '"';
19 $tbSectionAttr .= ' border-bottom="' . $borderBW . ' ' . $borderStyle . ' ' . $borderColor . '"';
20 $tbSectionAttr .= ' border-left="' . $borderLW . ' ' . $borderStyle . ' ' . $borderColor . '"';
21 }
Renderer mail
Interpréter les props
1 switch ($type) {
2 case 'Box':
3 $children = $comp['children'];
4 if($children !== []) {
5 $tbSectionPartial = '<mj-wrapper';
6 $tbSectionPartial .= $tbSectionAttr;
7 $tbSectionPartial .= 'full-width="full-width" background-size="cover"';
8 $tbSectionPartial .= ' >';
9
10 if (!empty($children)) {
11 foreach ($children as $child) {
12 $tbSectionPartial .= $this->renderMjmlElement($child, $content, $ctLang, $contact);
13 }
14 }
15 $tbSectionPartial .= '</mj-wrapper>';
16
17 } else {
18 $tbSectionPartial = '<mj-spacer';
19 $tbSectionPartial .= $tbSectionAttr;
20 $tbSectionPartial .= array_key_exists('props', $comp) && array_key_exists('backgroundColor', $p) ? ' container-background-color="' . $this->chakraColorTranslator($p['backgroundColor'], $content) . '"' : '';
21 $tbSectionPartial .= ' />';
22 }
23 break;
Interpréter les composants
Questions ?
(merci pour l'attention !)
1
1-Titre
2-Introduction
2-Introduction
2-Introduction
3-Préambule
6-Techno utilisées
7-Extension web
7-Extension web
4-Pygma.link-Fonctionnalités
5-Architecture générale
5-Architecture générale
5-Architecture générale
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
11-Flowchart inscription
11-Flowchart inscription
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
7-Extension web
30-Big Picture
30-Big Picture