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 _id5 }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 : user11 });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])).id14 : ''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 - kivi1dev13 script:14 - cp $DEV_PATH/.env.local .15 - cp $DEV_PATH/.env.prod.local .16 - test -f composer.lock && rm composer.lock17 - test -d vendor && rm vendor -rf18 - test -d node_modules && rm node_modules -rf19 - /usr/bin/php /usr/local/bin/composer install20 - /usr/bin/yarn21 - /usr/bin/php bin/console fos:js-routing:dump --format=json –target=public/js/fos_js_routes.json22 - /usr/bin/yarn build23 - cd $DEV_PATH24 - /usr/bin/php bin/console doctrine:cache:clear-metadata25 - /usr/bin/php bin/console doctrine:cache:clear-query26 - /usr/bin/php bin/console doctrine:cache:clear-result27 - /usr/bin/php bin/console doctrine:migrations:migrate -n28 - /usr/bin/php bin/console cache:clear Extraits du fichier de déploiement .gitlab-ci.yml : push dépôt local distant clone serveurpreprod dépôt gitlabrunner 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-apacheRUN a2enmod rewrite[ . . . ]## Install composer globallyRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer -d=/var/www[ . . . ]#NPM / nodeRUN 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 | shENV PATH $PATH:/node_modules/.binRUN npm install --global yarn[ . . . ]COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.iniCOPY php.ini /usr/local/etc/php/php.iniCOPY error_reporting.ini /usr/local/etc/php/conf.d/error_reporting.iniCOPY 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 props12 const { width, flexDirection, isSticky, children, ...otherProps } = props;13 14 return (15 <SrcFlex16 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 default18 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 attendancy42 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.accompagnant46 ?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 multilang24 });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 assets17 })();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. 1-Titre
  2. 2-Introduction
  3. 2-Introduction
  4. 2-Introduction
  5. 3-Préambule
  6. 6-Techno utilisées
  7. 7-Extension web
  8. 7-Extension web
  9. 4-Pygma.link-Fonctionnalités
  10. 5-Architecture générale
  11. 5-Architecture générale
  12. 5-Architecture générale
  13. 7-Extension web
  14. 7-Extension web
  15. 7-Extension web
  16. 7-Extension web
  17. 7-Extension web
  18. 7-Extension web
  19. 7-Extension web
  20. 7-Extension web
  21. 7-Extension web
  22. 7-Extension web
  23. 7-Extension web
  24. 11-Flowchart inscription
  25. 11-Flowchart inscription
  26. 7-Extension web
  27. 7-Extension web
  28. 7-Extension web
  29. 7-Extension web
  30. 7-Extension web
  31. 7-Extension web
  32. 7-Extension web
  33. 7-Extension web
  34. 7-Extension web
  35. 7-Extension web
  36. 7-Extension web
  37. 7-Extension web
  38. 7-Extension web
  39. 7-Extension web
  40. 7-Extension web
  41. 7-Extension web
  42. 7-Extension web
  43. 7-Extension web
  44. 7-Extension web
  45. 7-Extension web
  46. 7-Extension web
  47. 7-Extension web
  48. 7-Extension web
  49. 7-Extension web
  50. 7-Extension web
  51. 7-Extension web
  52. 7-Extension web
  53. 7-Extension web
  54. 7-Extension web
  55. 7-Extension web
  56. 7-Extension web
  57. 7-Extension web
  58. 7-Extension web
  59. 7-Extension web
  60. 7-Extension web
  61. 30-Big Picture
  62. 30-Big Picture