From 4a97e23aa5b18a77b86f9535d652340167b5c1ab Mon Sep 17 00:00:00 2001 From: PASQUET Fabien Date: Thu, 12 Oct 2023 12:35:39 +0200 Subject: [PATCH] feat: add react front template --- templates/react-front-template/.editorconfig | 9 + templates/react-front-template/.env | 2 + templates/react-front-template/.env.prod | 2 + templates/react-front-template/.eslintignore | 2 + templates/react-front-template/.eslintrc | 3 + templates/react-front-template/.gitignore | 9 + templates/react-front-template/Makefile | 38 + templates/react-front-template/README.md | 90 + templates/react-front-template/codegen.cjs | 21 + templates/react-front-template/index.html | 17 + templates/react-front-template/jest.setup.ts | 1 + templates/react-front-template/package.json | 71 + .../react-front-template/public/favicon.ico | Bin 0 -> 8310 bytes .../react-front-template/public/react.svg | 1 + .../public/translations/messages.en.json | 24 + .../public/translations/messages.fr.json | 28 + .../react-front-template/public/vite.svg | 1 + .../public/web-app-manifest.json | 10 + .../src/config/i18n/i18n.config.ts | 25 + .../src/config/i18n/index.ts | 1 + .../src/config/router/index.ts | 1 + .../src/config/router/routes.tsx | 55 + .../react-front-template/src/constants.ts | 17 + .../HomePageContainer/HomePageContainer.tsx | 10 + .../src/containers/HomePageContainer/index.ts | 1 + .../LayoutTemplateContainer.tsx | 53 + .../LayoutTemplateContainer/index.ts | 1 + .../NotFoundPageContainer.tsx | 16 + .../containers/NotFoundPageContainer/index.ts | 1 + .../PokemonListPageContainer.tsx | 18 + .../PokemonListPageContainer/index.ts | 1 + .../RootContainer/RootContainer.tsx | 27 + .../src/containers/RootContainer/index.ts | 1 + .../src/containers/index.ts | 5 + .../src/graphql/client.ts | 3 + .../src/graphql/fetcher.ts | 8 + .../src/graphql/fragments/PokemonItem.graphql | 4 + .../react-front-template/src/graphql/index.ts | 1 + .../src/graphql/queries/Pokemons.graphql | 8 + .../src/graphql/types-and-hooks.ts | 309 + .../src/helpers/example.test.ts | 5 + .../src/helpers/getEnv.ts | 3 + .../src/helpers/langHelper.ts | 4 + .../src/hooks/useCurrentPath.ts | 13 + templates/react-front-template/src/main.tsx | 18 + .../src/pages/HomePage/HomePage.scss | 34 + .../src/pages/HomePage/HomePage.tsx | 24 + .../src/pages/HomePage/index.ts | 1 + .../src/pages/NotFoundPage/NotFoundPage.tsx | 16 + .../src/pages/NotFoundPage/index.ts | 1 + .../pages/PokemonListPage/PokemonListPage.tsx | 30 + .../src/pages/PokemonListPage/index.ts | 1 + .../react-front-template/src/pages/index.ts | 3 + .../react-front-template/src/vite-env.d.ts | 1 + templates/react-front-template/tsconfig.json | 27 + .../react-front-template/tsconfig.node.json | 13 + templates/react-front-template/vite.config.ts | 16 + templates/react-front-template/yarn.lock | 7725 +++++++++++++++++ 58 files changed, 8830 insertions(+) create mode 100644 templates/react-front-template/.editorconfig create mode 100644 templates/react-front-template/.env create mode 100644 templates/react-front-template/.env.prod create mode 100644 templates/react-front-template/.eslintignore create mode 100644 templates/react-front-template/.eslintrc create mode 100644 templates/react-front-template/.gitignore create mode 100644 templates/react-front-template/Makefile create mode 100644 templates/react-front-template/README.md create mode 100644 templates/react-front-template/codegen.cjs create mode 100644 templates/react-front-template/index.html create mode 100644 templates/react-front-template/jest.setup.ts create mode 100644 templates/react-front-template/package.json create mode 100644 templates/react-front-template/public/favicon.ico create mode 100644 templates/react-front-template/public/react.svg create mode 100644 templates/react-front-template/public/translations/messages.en.json create mode 100644 templates/react-front-template/public/translations/messages.fr.json create mode 100644 templates/react-front-template/public/vite.svg create mode 100644 templates/react-front-template/public/web-app-manifest.json create mode 100644 templates/react-front-template/src/config/i18n/i18n.config.ts create mode 100644 templates/react-front-template/src/config/i18n/index.ts create mode 100644 templates/react-front-template/src/config/router/index.ts create mode 100644 templates/react-front-template/src/config/router/routes.tsx create mode 100644 templates/react-front-template/src/constants.ts create mode 100644 templates/react-front-template/src/containers/HomePageContainer/HomePageContainer.tsx create mode 100644 templates/react-front-template/src/containers/HomePageContainer/index.ts create mode 100644 templates/react-front-template/src/containers/LayoutTemplateContainer/LayoutTemplateContainer.tsx create mode 100644 templates/react-front-template/src/containers/LayoutTemplateContainer/index.ts create mode 100644 templates/react-front-template/src/containers/NotFoundPageContainer/NotFoundPageContainer.tsx create mode 100644 templates/react-front-template/src/containers/NotFoundPageContainer/index.ts create mode 100644 templates/react-front-template/src/containers/PokemonListPageContainer/PokemonListPageContainer.tsx create mode 100644 templates/react-front-template/src/containers/PokemonListPageContainer/index.ts create mode 100644 templates/react-front-template/src/containers/RootContainer/RootContainer.tsx create mode 100644 templates/react-front-template/src/containers/RootContainer/index.ts create mode 100644 templates/react-front-template/src/containers/index.ts create mode 100644 templates/react-front-template/src/graphql/client.ts create mode 100644 templates/react-front-template/src/graphql/fetcher.ts create mode 100644 templates/react-front-template/src/graphql/fragments/PokemonItem.graphql create mode 100644 templates/react-front-template/src/graphql/index.ts create mode 100644 templates/react-front-template/src/graphql/queries/Pokemons.graphql create mode 100644 templates/react-front-template/src/graphql/types-and-hooks.ts create mode 100644 templates/react-front-template/src/helpers/example.test.ts create mode 100644 templates/react-front-template/src/helpers/getEnv.ts create mode 100644 templates/react-front-template/src/helpers/langHelper.ts create mode 100644 templates/react-front-template/src/hooks/useCurrentPath.ts create mode 100644 templates/react-front-template/src/main.tsx create mode 100644 templates/react-front-template/src/pages/HomePage/HomePage.scss create mode 100644 templates/react-front-template/src/pages/HomePage/HomePage.tsx create mode 100644 templates/react-front-template/src/pages/HomePage/index.ts create mode 100644 templates/react-front-template/src/pages/NotFoundPage/NotFoundPage.tsx create mode 100644 templates/react-front-template/src/pages/NotFoundPage/index.ts create mode 100644 templates/react-front-template/src/pages/PokemonListPage/PokemonListPage.tsx create mode 100644 templates/react-front-template/src/pages/PokemonListPage/index.ts create mode 100644 templates/react-front-template/src/pages/index.ts create mode 100644 templates/react-front-template/src/vite-env.d.ts create mode 100644 templates/react-front-template/tsconfig.json create mode 100644 templates/react-front-template/tsconfig.node.json create mode 100644 templates/react-front-template/vite.config.ts create mode 100644 templates/react-front-template/yarn.lock diff --git a/templates/react-front-template/.editorconfig b/templates/react-front-template/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/templates/react-front-template/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/templates/react-front-template/.env b/templates/react-front-template/.env new file mode 100644 index 0000000..a489b8a --- /dev/null +++ b/templates/react-front-template/.env @@ -0,0 +1,2 @@ +VITE_GRAPHQL_GATEWAY_URL=https://graphql-pokeapi.graphcdn.app/ +VITE_TRANSLATIONS_ENDPOINT_URI=/translations diff --git a/templates/react-front-template/.env.prod b/templates/react-front-template/.env.prod new file mode 100644 index 0000000..a489b8a --- /dev/null +++ b/templates/react-front-template/.env.prod @@ -0,0 +1,2 @@ +VITE_GRAPHQL_GATEWAY_URL=https://graphql-pokeapi.graphcdn.app/ +VITE_TRANSLATIONS_ENDPOINT_URI=/translations diff --git a/templates/react-front-template/.eslintignore b/templates/react-front-template/.eslintignore new file mode 100644 index 0000000..3796845 --- /dev/null +++ b/templates/react-front-template/.eslintignore @@ -0,0 +1,2 @@ +src/graphql/types-and-hooks.ts +src/vite-env.d.ts diff --git a/templates/react-front-template/.eslintrc b/templates/react-front-template/.eslintrc new file mode 100644 index 0000000..bc8cca2 --- /dev/null +++ b/templates/react-front-template/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "@eleven-labs/eslint-config-react" +} diff --git a/templates/react-front-template/.gitignore b/templates/react-front-template/.gitignore new file mode 100644 index 0000000..21084c4 --- /dev/null +++ b/templates/react-front-template/.gitignore @@ -0,0 +1,9 @@ +# Logs +logs +*.log +yarn-debug.log* +yarn-error.log* + +node_modules +dist +build diff --git a/templates/react-front-template/Makefile b/templates/react-front-template/Makefile new file mode 100644 index 0000000..57d99e0 --- /dev/null +++ b/templates/react-front-template/Makefile @@ -0,0 +1,38 @@ +WORKSPACE ?= $(PWD) +REPORTS_DIR ?= build/reports +STAGE ?= dev + +install: ## Install the project + @yarn --production=false --frozen-lockfile + +start-server: install ## Build assets and launch server + @STAGE=${STAGE} yarn dev + +build: ## Build for production + @STAGE=${STAGE} yarn build + +prepare-ci: ## Prepare workspace to run CI targets + @mkdir -p build/reports + +lint: ## Run Lint + @yarn lint + +lint-ci: prepare-ci ## Run Lint and generate report file + - yarn lint:ci + - sed -e 's#$(PWD)#$(WORKSPACE)#g' -i $(REPORTS_DIR)/checkstyle.xml + +unit-tests: ## Run unit tests and generate report file + @yarn test + +unit-tests-ci: prepare-ci ## Run unit tests and generate report file + @yarn test:ci + +graphql-codegen: ## Generate Operation Apollo GraphQL + @yarn graphql:codegen + +.PHONY: install + +.DEFAULT_GOAL := help +help: + @grep -E '(^[0-9a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-25s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' +.PHONY: help diff --git a/templates/react-front-template/README.md b/templates/react-front-template/README.md new file mode 100644 index 0000000..eb4b8e1 --- /dev/null +++ b/templates/react-front-template/README.md @@ -0,0 +1,90 @@ +# React Front Template + +## Context + +This repositories contains templates that we should use to bootstrap new GraphQL Gateway. + +It contains: +- basic React configuration +- CI/CD configuration +- React Router configurations +- I18Next configurations +- GraphQL configurations + +## How to use? + +1. Create a new Github repository using this template. + +2. Replace all the places where there is `react-front-template` with the name of your repository + +3. Commit and push. + +## Installation + +```bash +$ make install +``` + +## Running the app + +```bash +$ make start-server +``` + +## Unit Tests + +```bash +$ make unit-tests +``` + +## GraphQL + +To use GraphQL with react, we use two libraries: + +- [GraphQL Codegen](https://the-guild.dev/graphql/codegen) + + It's role will be to generate the hooks of the queries and mutations that we have created in the `.graphql` files + +- [React Query](https://tanstack.com/query/v4/docs/react/graphql) + + Similar to React Apollo on the use of hooks Query and Mutation, the only difference is that this one has a simplified cache management which will avoid many bugs in the case of refetch and cache policy. + +Example of usage for a query: + +``` +... +import { usePokemonsQuery } from '@/graphql'; + +export const Component = (): React.FC => { + const pokemonsResult = usePokemonsQuery(); + + return ( + {(pokemonsResult.isLoading && pokemons?.length) ? ( + Loading ... + ) : ( +
+ {pokemonsResult.data?.pokemons?.results.map((pokemonItem) => ( +

+ {pokemonItem.name} +

+ ))} +
+ )} + ) +} +``` + +Example of usage to invalidate a query: + +``` +... +import { useQueryClient } from '@tanstack/react-query'; +import { namedOperations } from '@/graphql'; + +export const Component = (): React.FC => { + const queryClient = useQueryClient(); + const invalidateCachePokemonsQuery = (): void => { + queryClient.invalidateQueries({ queryKey: [namedOperations.Query.Pokemons] }); + }; +} +``` diff --git a/templates/react-front-template/codegen.cjs b/templates/react-front-template/codegen.cjs new file mode 100644 index 0000000..c471f8b --- /dev/null +++ b/templates/react-front-template/codegen.cjs @@ -0,0 +1,21 @@ +module.exports = { + overwrite: true, + schema: process.env.VITE_GRAPHQL_GATEWAY_URL, + documents: ['src/**/*.graphql'], + generates: { + 'src/graphql/types-and-hooks.ts': { + plugins: [ + 'typescript', + 'typescript-operations', + 'typescript-react-query', + 'named-operations-object' + ], + config: { + maybeValue: 'T', + fetcher: { + func: './fetcher#fetcher', + }, + } + }, + }, +}; diff --git a/templates/react-front-template/index.html b/templates/react-front-template/index.html new file mode 100644 index 0000000..cc05cc0 --- /dev/null +++ b/templates/react-front-template/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + Eleven Labs - React Front Template + + +
+ + + diff --git a/templates/react-front-template/jest.setup.ts b/templates/react-front-template/jest.setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/templates/react-front-template/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/templates/react-front-template/package.json b/templates/react-front-template/package.json new file mode 100644 index 0000000..5269583 --- /dev/null +++ b/templates/react-front-template/package.json @@ -0,0 +1,71 @@ +{ + "name": "react-front-template", + "version": "0.0.1", + "scripts": { + "graphql:codegen": "graphql-codegen-esm --config codegen.cjs -r dotenv/config", + "dev": "concurrently \"vite --mode=$STAGE\" \"yarn graphql:codegen --watch\"", + "build": "tsc && vite build --mode=$STAGE --outDir dist-$STAGE", + "prestart": "yarn build", + "start": "NODE_ENV=production vite preview", + "lint": "eslint src", + "lint:ci": "yarn lint --format checkstyle --output-file build/reports/checkstyle.xml || true", + "test": "jest", + "test:ci": "jest --ci" + }, + "lint-staged": { + "*.(ts|tsx)": [ + "eslint --fix" + ] + }, + "engines": { + "npm": "please-use-yarn-instead", + "yarn": ">= 1.2.0", + "node": ">= 18.0" + }, + "prettier": "@eleven-labs/prettier-config", + "dependencies": { + "@eleven-labs/design-system": "^0.6.2", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-devtools": "^4.36.1", + "graphql": "^16.8.1", + "graphql-request": "^6.1.0", + "hoofd": "^1.7.1", + "i18next": "^23.5.1", + "i18next-browser-languagedetector": "^7.1.0", + "i18next-http-backend": "^2.2.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^13.2.2", + "react-router-dom": "^6.16.0" + }, + "devDependencies": { + "@eleven-labs/eslint-config-react": "^1.0.1", + "@eleven-labs/prettier-config": "^1.0.0", + "@graphql-codegen/cli": "^5.0.0", + "@graphql-codegen/named-operations-object": "^3.0.0", + "@graphql-codegen/typescript": "^4.0.1", + "@graphql-codegen/typescript-operations": "^4.0.1", + "@graphql-codegen/typescript-react-query": "^5.0.0", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@types/jest": "^29.5.5", + "@types/react": "^18.2.28", + "@types/react-dom": "^18.2.13", + "@types/react-router-dom": "^5.3.3", + "@types/sha.js": "^2.4.2", + "@vitejs/plugin-react-swc": "^3.4.0", + "concurrently": "^8.2.1", + "dotenv": "^16.3.1", + "eslint": "^8.51.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-junit": "^16.0.0", + "lint-staged": "^14.0.1", + "prettier": "^3.0.3", + "sass": "^1.69.3", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2", + "vite": "^4.4.11", + "vite-tsconfig-paths": "^4.2.1" + } +} diff --git a/templates/react-front-template/public/favicon.ico b/templates/react-front-template/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ffa57ea918ab62ac6271187614220cd3314bfb8a GIT binary patch literal 8310 zcmZ{J1yoegyZsD}bax}D)X*?=OM^55!oX0{-64p80y0QR3Mxo9NVjx~NH<7H3kbqH zy!Bt}|9fw(w_MlDoqO&%=brO@``dfp7#%GYf_t?0AP@+F>SM(x;B0w!;9!COSJQmO zzzM@nPE!s7sZ7ATw!#G0P|qhS^3a)n?oe>SZ2kC&CIk||0fB@?LLh&^Phr0x5N{X+ zvSSH>z|$ZQD!0r=T}f~Q3!$!}2)Vucm)la11b)GFdu-wff$*~39Zc&WutM7On6xZdo!~S~u8JV>GPh)nL)8I47PTEeA|0jfp({ zeJQY-$%8k&jOGi0qyE{R*(Xk0;-sR?Rn^r{c4mw*^bcVpmst|@VFVTM+W|;&a`JQ& zUWmfIYZB{MNn;RGX8gw(9Ow_*6}LENE@O@+7NNNW_TD$n(D&h3xws84EQbgN-?`g` z&HW-&A%>%;5d`7&VW7hNx(ibwaZCvY#Q96mQ?z|qHaA)^Tz$0Eu)&d~c^Ezo6;?ys zsSZX5Uif{SPHoJ*LtX^hOY{n6+g_z**deAHVF&(3fZ*zTf%dq=aapJvryJf|OzN_; ztCsfoGzMflqos@vQQ!1ylIci@OJoAn1?vaeAjxVEQ=$MPjIDd}Sy%!w>j3}a8=Obz z^Ej@G(P>vCY|<=05ALyYx6>?3Sz;rU-(=X|qt(sqo`!mcF`*R`&koAqVTG}^54L}k zB{I9}fm?z}{`IpYI3%;Al&`(EQ{+69K4e8ywtn7Kki~Z+eTKdcO@J;b_x%g?!AIi^ zlR;x*-HUlHK-Vj&meoy!`5P?+uSC&auN-eq1%gjf-B*-nl@29Fqy)aAEKYYs)bzyw1ZMG>Y!ZP2&Ee^?51xMDSs z$dJfge7l%_dwwG;-=OLd_9&NMTG|2^i|iT^Mt8)Cc8kptz*;V9&`U!qPd<#{7}jVB zTPeb)R=$1)C1R#`kPFXypdfv7WB4gns7yT}(m2zO#kTX4Y|psC(*qk>@)s8}O7_yw z4kg7>!`I{X-S$GLw6w<_9s);4M~X^HRxs+ZZLbHhoy*J2=;-LDr>A36Q%dUUcv)Fl zr)OtYA{mKD8dX$p28~KM6WctEwV|$xQ)SWxOS?jQdH7G+n6l~JbeI*IxVZTG<|ZmB zNg*HrURha5jgKX-pfJAeWiO;)u3W`!T|ZZ8T8fX4f7iXHCIJ<%S#EQ4W^C-e#>U2q zx&5^se9WIQI?A-)1~76;y#%Wap&7(3Yr0*+aBBo2O)602H6>48Zf;nA5+@l0!@it7 zRke)?C4m>|B$XV}6Kb^;w9jzrT1(OUur`X=20W`^%wWF4R(F&2=N1 zzo{TkGecD{Q!}+RG;CHnU(Wl?IS*w?q&74(uqf8g?VtYnGx6hxvXK#0D&MnS8-ekq zrG%~(+2Sb#j5<0tmK6?v|MRCYDJf}mLIS6d5P9jJ4CK&?+t~Pc2bkDQwH=X4JiYG5 ziLi}gLK5^T7I6AN!uR*bl9D&AtbFMkUE@ZthoIvf@7Fz@J+G-Q_o_!9@bG-9tD~%*dDFE*&1%~mXK3_lfhK=?m!CGy z#?}^Te*lkxnkw*?H^!8gx}nROgC0z zUyzK!Y_S?*vV)chA~Ana*_d$f^2Rs)ZhU>VJ1ywFqhFg(_$mUtw$J`-k6li%vZtqD zL_~y~3HO3@yUtFSC9KjN=n_`-G$D@g@wy;qKlRXi!EkfJ&^hb-k~aHX7tsz z!>gY&Tv&yLlLrRWeD;3IDJXP>v8&#qUlUTFrs6JA6JuU8`Vq_glZefGo0!<4KqS@8 z;5#p;s2CYAYy%SE8&frSi=v_;G{$hF=h(6u8u(z$5|iqv&QAHx&Q1tR%H0fi%iFZ{ z^d=3IQ0gSn(b3F;f=PLKtS+sWwjLfH(Q$D~D0VDt?1v8@TCMfQr44W1Q$_t&>87|Q zRoc0?De~6{?KZ|u@$%*6$=1o>vm4aLU}|v|W?^Y5@?>+k`EoNuNK_Pk+K|hA_`&NQ zc>@Cq5K144i(h~Lt`Fh{2M^D>_YI9ky1>iXSyMP1E;+jyV&X|-Q_1->rC%mC~2%WC4MJ_f=6( zNP10@Welq*-M*&Yw-jd0qv$or9k(~Gu_w`%IwKdf9ikRMX=TBCsz;><6WLVsC6)tZ zQS9VOA1W&olQ^~WrX2eR23Wbd-+*KQp!=n)>~m>p94;A$j+xmII+0}i-N92oFR!L{ zFQ6r)`_EHu%EjPwb3z9A`rmmK)YK2uz^u#O>Qe_ zqtaL0-x8SG7=J@^-u&9%-xx|4H0GxC@bu)T#%Jf`OiEAh|EQYOaL{^X+!i9`xjDr0 z=+VFdP2hSTNuA4rnznXA;IpLg^03(AVYJeW3zKl_e{Aqs5c#5_sRh{qv3HC7^~e1D z&_U>JAR(iqp8|1+7#TMu-quJ~mu)q_>!RdofK-2f|N7RJAaa0QUO{DRvoYvWfUo%w zjZ-_Dex$-p@$Ic=Srui&?^s!O&7QbZc_rP)T3VZ&GMBg~TSVp@SXfwo=L^1<*C(Up zJ5%(o4Q6y!R#tcToRT8ay-B_Mw-?}ZigHN)!aMCzR9lmi2=2o!=+Q&PZ678w*9 zg9LYQaDb9Aw%b-$2A;c&WJ^h)3JIVoXyvRPdM)y=h$YlX{4b;>)=UdgD3oN`zkdgz zTir2aYw9Z`xex9s=O=zHT(-X6Bp01>h3&%tMMvYmKXU;3Pa9lBGQ_=p5eEkc zPbWip{h#URw3BH|cGS0?@wC}mKYz|u-tvd`pzT)LsLV{422nlJeA8M~6uUoHpHOoc zpdj)#UeHpI9v--1(hlTtbaPo zv@}KOHDzLA;>uY7z%yrO=jq4`UPM!ggOLq4NaD9|iM`+q08u`EWKZEX=^RMrQq^h= z5UArG%!GT#Zsmk#S5*nn6Jp2a5mQlpiy)HjlRBSYn_`ymJ7i`Gy{_C*X2N6*Gm8{E zHpUKe!SfJyJX-CsKz;$w%Yfqe7_obDT8A}qg`+=J`u>wXsjW)(9|LPA0?F)?cL1=AEH7{SEEWZr91 z^Ddp8XN3Tg!>IA0ZC!}U3J{_IGT@34>HOAM3DSQb)X#h1v9PdM90VS;oL}q|m!tUy zN4%UEQ=M}MD>)wjepXrs9d=;f{JLgi>9{=fpIY}45l9JsKcrv;vPtgfE_<;%mj(z{2As=sSrfj9pC{X0vCcLL|m8Kc(Mad{N` z`}glJ_FY5OO-*SDU!k{HXnT1Hk#TCV13>=jxg`KPY%7^9Zs8IapDEW3{j+BRDt_O{ z2#0_Gs-{MWkVzWM^DNp1o5&9)t4l6yBZ`uY2h|^xV9TX8cGa5t`pF+ZL_QLLu|X(3 z0TsD5NZfZRSaf@PdwPDkxVU)jC6&eUW`?U_S85W6<~=XxiRG@DZEvh!X91*SWGX>< zEJcC3pTY_X~=S#eZygva7zP_SQ>s>aMJ0d<76bOom zJqFYe7Z*1&Dhg3)jJ93w>#(*z2vQ1Rt)dl42*PEy#sS>4sK!ZCH>ankUkAH?y}=i% zo8%G^mz~XM5ps!?zCKhaG2{YdUyQ!fEiNX8#=BwSnIAGttAGwQxcyqp-``()KQdQj z;!E`sh{}+!sVX4)wJd^;lmHG!^&p^;dLzkcNFmw?t5mTts`=v> zjW~0Vqk-AeMiR! zL`_ReR(abEZPS8u(bm-T^vO4wn?w*zeiU3Nb`nz3yb#w~Q*_Nobr6O{UcW3TptB%b zFaMnaqQMw+D7U({7BPUKqzm|?Y%A9B2L8xzP7~B!DpabF~ei)u7n$D4*$HbYx^?t^kXOND2j)A+(bd9R*0(0`SCi?X=l^{73J5VNmTUoyL&d&FN2nxRX;|>^) z>`zv}*>XX=YG#OBCMSem9wT>lc7jNkHW;-vH4TDw85kITI@QT4>hK^a{c3c(VE^<{ z>>%R6lIAn^16ERES{ni4q3qRpVm3) z3i1Qq38RgBmz-P)>JZ34Z5S7brV&ADtBoY_W*GDGV=?dmbCqBpYN4S2^r-LM4;G&wyJe1ecGbMPx)e}NSm+j&Bl`Lt z-&MgEn0WS1P9MRd9v^$`?CyRIx~yuf_-Z!x;lqc>SFehjm}{+-Mb3;e?kni*O6D`jai1)q|j z*a>i{4E6QhZm#{y@qhjLwG2RQN1{cpd^#yLwV)9(k|UGQZ*wmaV~aRBDEKo+3d_Jh zLaVSDr_J^LByuV$?E5fQRZ@5sVV0IC0%juyP<{qm*1%f5QX>0L?+;ZnZXoT^kf6!uRfK{yYk? zs6wt43R=aofvso;h9$Ex`v$)w){TvgHC0avDJiMu;{h&usQ@qieC-runc3G>xf-yb z0|Ns=j)H*DFfxvy?yKt?U4w~z_1b>Ie0+;9k8!D=Xzp1_&QGTQ7!}!Xv-+B^6-Vtb zSO}Ie+zja7^;vo78Fl}SWkXOZGJRC$HWO4g5|!H;hsiq;#f)DExFgt`Zvzm#Ls7*z z#~e5QTv~p-D10K5l*{8mzAr^Zh-T1WrZrf?wfU$w4PHKO54aCK>sE15(Z$7Df|O)7 zZ$oE=@4Wk+lnslZ2BKFWX@1AopFH5heC<+#ayttx0VehRMhDJOKd# zepmbTGmYL{3kwU@wEjSRswQ!K!E*}@mARv$6ciL}ysg!CqqaS<6d+W9M)_Iq$^G%Vu9|3P3IJ z&mN8LyRVO$0Dz_dfh^m116~{h@lOP;>5wGS{Z$|zANm{#*SV<7m_3l#8-P3Nba^u? zDp^}w=UT{n`wop z2Y$ZYSIJ&3?c2!ktaJ42jpsrr`=9_14-cToSwMWFjeY;#&2)2!4Pf}O znp&s5P<&AbNQT!`e8)eYx*koa^SS@oo5>|V2I><89dI3fwgjF>KaBwri;piM=k|K5 zXr<0|NqRBpWLQ>!*)jZ9`8)A(xjwY8X`?iHY=)-+>7k11PUbR#rE8FrlM~a>!~kOt zpv(YQxsIu6Y`={>>LW|~5O36j*cV+ZIxQtY(b~<`IX7P)4_>b^-RgLsSbk~%RIRAE zSmpaA(!_SPJ4W)Tn~a=-f<={dYHCXIeBLv>FxyA*>}>zSpEBI>k)fesqX;<@v<|Ax zT4!4HfQ!re>d*f5#WxuaNy$u5i^eA=I)Otn0!%PfH8sS*yN7o)#bJUyFE4M*u@;O5 zoW8q|nfRm*D6*wM`dcJToU*4T+Fv#)Jzes={QS!fqL6lQd+G_6YwIP4-AE?xM=4Gc z@09Bwb1j$D+&Bjd2+I$Fubf2a?%%%;y7!zm_az}A>4+kr18cE2|23(%SDBWM?jaYK zLh)2l{_MuSml(Z5bcmFXK_h+{aGk1s_Duo512lbaw)QoV%r%Ud-yZFTNe6n9%0q{! zY$(NCz>(=i%sZ+y1S!_m*4iMEUsTHk?MA?GuXc7EYvHl#dkxXQ8*feH{6b?6?zsaF z)ph5HNid1JE^4T#s4zQj5h}*D+>F2DK5@fsMLz;!J4~56nuRjBk{+0wV?#YPN?DV}KI_pdAPdeIq06W`8zgH%$MDi3z{; zL>1$D7Y3ljfYUpkuQJo%&IYt8QRKyN`z@xlHNM@bAF>^$>xJvy_g=-q7x-`>2=3%k zX67590)Ld4fZAcQkmwV~0hs5@moFtR*OTT$uJ$8ezFct=1MVRb_+FV3zPxY$ zoUss=`;k?|eex(96|xrP5Q3VwYx?!O`%c|zsHxpqbz@^X0)m3+qORGFG7UbdZ>ZSJ zvd3b`I6X$C&JO_$fx7U@uo%?k3f}_@5$D-%U{azxOfnzB^VASa#?Ms}(zf<(Uc4|S z+G|H^-Yar_{(KG4oL^q>+Cp=Ac{EH+OfY{K3?>T*!j1t$LQ#x*D({^L*GobNb+@`Y z$#>EJ71gt##Hgq!+3xr^Vh-CzV*l){OLIe}6azWQu)?cHs79GaFjP^$wzpS6Ap%rC z>HT{(z(znR27yrt>@xuY0XH#vpiC?)jic<9!$}vCbtJ7iF@WM3>LQXE`gguG3S0r; zgyRzu9-mD=tM)&!RZ~*~UE)Sw5Ju+jKkPn=hBEwK^F7E?Hf7H%nMuMS=#QWV3@!aIm>9xdsOr$cdG)4+``$A(;R?hHOs?xw4z08A(ayLCpt(1K`1(750Wk zFp8bDrndH|pEF12$rE`^P0ifr6U>P9TdEO;gz)SVp`sMAbYkc=#E+H<* zxFtXskCGcm8`J%UVcUFQtpP{1_2%z$fKv7@E}sGU1s3y^j~k!Ca_qw<0Twd@qLbs} zzM&zT&PZHG!nV8zUcSD1rlteHH@c(R;FCi5hzOnh^sTG1gA-;P)I|dZIcHVw)PJS{ zNi;PE&c%NHl2%BFbWBW)0;^#a_vF%2xuMp<{(g^*0QmYaB^v~S4R}0&%mRb}tyrX` z86!HDbz@pSeR^sAExr=ON9*66V$0;pdWb9Ah)x4J_i9267-uWl5xR&NMI_b_l^ z>bIx(*1RZ^lb!u7_*4@V)ND%9bHB*UuCHN7uYoY|j?h_JE}QxpT#;-M$h;s(hLBq%)qQ*X&cuOWbJQz&_ zwtI>jFPpy2*YI3sE!}?@Bs$`~)aH2kN@JQ&>OEiDvBt}M#Y-EhSRNrj!>siF1!0Xn z6CWA&y7j__H+4CS{35BgOg>2(jB{TtdUJ?igs2h6I9S6mAL^KS9;D5g*w93KZXC#B z{3D}zcJz_CIn@AX`p80&rNx3ew6h835`y4lWACZp>H)y9to1clSHf4EE1XZ!ZMnWKT!GoLxD~`iD;ZCUx;^5 z(pfN8V$M>G8>d9I!|Pl(BM%@s4xcM)c)R)`8z-o}NlEm6nc60OEuw3Rh?9fL2y z_q~*iy=<(!kZ@}cBsf6?VEke{{QNxp0{Z+gxF8HJB*+DW!C|oWKW!`j;{j(^8wXqe z|NDW;Vr+Bpfb!i4p02iDzE&Pch^n)d9a7WE&cXUQvk0#!uNaRwGZ<2c2PO=LWY*;u zVYc`3a)a~n`TF|uI^4Ae2WZXf>R~q@wGTRkP~Y{aZ}07lfWsACt-YO*E?#iB4ruBL z;V-yt;`?81{(DR~+#cy*XYU2!7Z)Icc)SE{sQ#m$|JTOI!N$w}uE}Hx3l3;P^B+w# z9h{L~{%%Off6t1~&Bacp%24Zn^`of|T0!`Qh3?i-L`d-A^IGtU9zygV{pdLaAR#b$ z`FDBJd*GJgf7~*7q6)Efa6*FD=W}-Pa&oofv$gVZuy%FvTZ6v^#RY_gM3LZ%Ul_p)p5O)ZBgMdR03$$Dm9!Mg