From 37ffc2cc62f1d8c17cf947fc9b7fc8930d98f9bc Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Mon, 26 Feb 2024 18:43:32 +0600 Subject: [PATCH 01/91] fix: set cookie's `SameSite` and `Secure` (#5186) * fix: set cookie's `SameSite` and `Secure` * fix: remove caddy file --- .../AuthenticationFlow/EmailLoginForm.tsx | 8 ++----- .../AuthenticationFlow/GitHubButton.tsx | 8 ++----- .../AuthenticationFlow/GoogleButton.tsx | 8 ++----- .../AuthenticationFlow/LinkedInButton.tsx | 8 ++----- .../AuthenticationFlow/ResetPasswordForm.tsx | 10 +++----- .../TriggerVerifyAccount.tsx | 8 ++----- src/components/Navigation/navigation.ts | 7 ++---- .../UpdateProfile/UploadProfilePicture.tsx | 4 ++-- src/lib/http.ts | 24 +++++++++---------- src/lib/jwt.ts | 17 +++++++++++++ 10 files changed, 46 insertions(+), 56 deletions(-) diff --git a/src/components/AuthenticationFlow/EmailLoginForm.tsx b/src/components/AuthenticationFlow/EmailLoginForm.tsx index 07304f00e..060ddc13e 100644 --- a/src/components/AuthenticationFlow/EmailLoginForm.tsx +++ b/src/components/AuthenticationFlow/EmailLoginForm.tsx @@ -2,7 +2,7 @@ import Cookies from 'js-cookie'; import type { FormEvent } from 'react'; import { useState } from 'react'; import { httpPost } from '../../lib/http'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; type EmailLoginFormProps = { isDisabled?: boolean; @@ -34,11 +34,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) { // Log the user in and reload the page if (response?.token) { - Cookies.set(TOKEN_COOKIE_NAME, response.token, { - path: '/', - expires: 30, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + setAuthToken(response.token); window.location.reload(); return; diff --git a/src/components/AuthenticationFlow/GitHubButton.tsx b/src/components/AuthenticationFlow/GitHubButton.tsx index 3ceaa7892..3ebc3a628 100644 --- a/src/components/AuthenticationFlow/GitHubButton.tsx +++ b/src/components/AuthenticationFlow/GitHubButton.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; import Cookies from 'js-cookie'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; import { httpGet } from '../../lib/http'; import { Spinner } from '../ReactIcons/Spinner.tsx'; @@ -70,11 +70,7 @@ export function GitHubButton(props: GitHubButtonProps) { localStorage.removeItem(GITHUB_REDIRECT_AT); localStorage.removeItem(GITHUB_LAST_PAGE); - Cookies.set(TOKEN_COOKIE_NAME, response.token, { - path: '/', - expires: 30, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + setAuthToken(response.token); window.location.href = redirectUrl; }) .catch((err) => { diff --git a/src/components/AuthenticationFlow/GoogleButton.tsx b/src/components/AuthenticationFlow/GoogleButton.tsx index 4ccc917ac..3d92b1bde 100644 --- a/src/components/AuthenticationFlow/GoogleButton.tsx +++ b/src/components/AuthenticationFlow/GoogleButton.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import Cookies from 'js-cookie'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; import { httpGet } from '../../lib/http'; import { Spinner } from '../ReactIcons/Spinner.tsx'; import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx'; @@ -69,11 +69,7 @@ export function GoogleButton(props: GoogleButtonProps) { localStorage.removeItem(GOOGLE_REDIRECT_AT); localStorage.removeItem(GOOGLE_LAST_PAGE); - Cookies.set(TOKEN_COOKIE_NAME, response.token, { - path: '/', - expires: 30, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + setAuthToken(response.token); window.location.href = redirectUrl; }) .catch((err) => { diff --git a/src/components/AuthenticationFlow/LinkedInButton.tsx b/src/components/AuthenticationFlow/LinkedInButton.tsx index e48481a86..851b2a9d8 100644 --- a/src/components/AuthenticationFlow/LinkedInButton.tsx +++ b/src/components/AuthenticationFlow/LinkedInButton.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import Cookies from 'js-cookie'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; import { httpGet } from '../../lib/http'; import { Spinner } from '../ReactIcons/Spinner.tsx'; import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx'; @@ -69,11 +69,7 @@ export function LinkedInButton(props: LinkedInButtonProps) { localStorage.removeItem(LINKEDIN_REDIRECT_AT); localStorage.removeItem(LINKEDIN_LAST_PAGE); - Cookies.set(TOKEN_COOKIE_NAME, response.token, { - path: '/', - expires: 30, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + setAuthToken(response.token); window.location.href = redirectUrl; }) .catch((err) => { diff --git a/src/components/AuthenticationFlow/ResetPasswordForm.tsx b/src/components/AuthenticationFlow/ResetPasswordForm.tsx index 46dd404ce..eaf378388 100644 --- a/src/components/AuthenticationFlow/ResetPasswordForm.tsx +++ b/src/components/AuthenticationFlow/ResetPasswordForm.tsx @@ -1,7 +1,7 @@ import { type FormEvent, useEffect, useState } from 'react'; import { httpPost } from '../../lib/http'; import Cookies from 'js-cookie'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; export function ResetPasswordForm() { const [code, setCode] = useState(''); @@ -37,7 +37,7 @@ export function ResetPasswordForm() { newPassword: password, confirmPassword: passwordConfirm, code, - } + }, ); if (error?.message) { @@ -53,11 +53,7 @@ export function ResetPasswordForm() { } const token = response.token; - Cookies.set(TOKEN_COOKIE_NAME, token, { - path: '/', - expires: 30, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + setAuthToken(response.token); window.location.href = '/'; }; diff --git a/src/components/AuthenticationFlow/TriggerVerifyAccount.tsx b/src/components/AuthenticationFlow/TriggerVerifyAccount.tsx index e46342a11..c9442ab52 100644 --- a/src/components/AuthenticationFlow/TriggerVerifyAccount.tsx +++ b/src/components/AuthenticationFlow/TriggerVerifyAccount.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import Cookies from 'js-cookie'; import { httpPost } from '../../lib/http'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; import { Spinner } from '../ReactIcons/Spinner'; import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2'; @@ -26,11 +26,7 @@ export function TriggerVerifyAccount() { return; } - Cookies.set(TOKEN_COOKIE_NAME, response.token, { - path: '/', - expires: 30, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + setAuthToken(response.token); window.location.href = '/'; }) .catch((err) => { diff --git a/src/components/Navigation/navigation.ts b/src/components/Navigation/navigation.ts index 5a725f495..04de8a5db 100644 --- a/src/components/Navigation/navigation.ts +++ b/src/components/Navigation/navigation.ts @@ -1,11 +1,8 @@ import Cookies from 'js-cookie'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, removeAuthToken } from '../../lib/jwt'; export function logout() { - Cookies.remove(TOKEN_COOKIE_NAME, { - path: '/', - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); + removeAuthToken(); // Reloading will automatically redirect the user if required window.location.reload(); diff --git a/src/components/UpdateProfile/UploadProfilePicture.tsx b/src/components/UpdateProfile/UploadProfilePicture.tsx index aa61aa3e7..b8a91e34e 100644 --- a/src/components/UpdateProfile/UploadProfilePicture.tsx +++ b/src/components/UpdateProfile/UploadProfilePicture.tsx @@ -1,6 +1,6 @@ import Cookies from 'js-cookie'; import { type ChangeEvent, type FormEvent, useEffect, useRef, useState } from 'react'; -import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; +import { TOKEN_COOKIE_NAME, removeAuthToken } from '../../lib/jwt'; interface PreviewFile extends File { preview: string; @@ -128,7 +128,7 @@ export default function UploadProfilePicture(props: UploadProfilePictureProps) { // Logout user if token is invalid if (data.status === 401) { - Cookies.remove(TOKEN_COOKIE_NAME); + removeAuthToken(); window.location.reload(); } }; diff --git a/src/lib/http.ts b/src/lib/http.ts index f0d7139c2..7f1692b27 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -1,6 +1,6 @@ import Cookies from 'js-cookie'; import fp from '@fingerprintjs/fingerprintjs'; -import { TOKEN_COOKIE_NAME } from './jwt'; +import { TOKEN_COOKIE_NAME, removeAuthToken } from './jwt'; type HttpOptionsType = RequestInit | { headers: Record }; @@ -30,10 +30,10 @@ type ApiReturn = { */ export async function httpCall< ResponseType = AppResponse, - ErrorType = AppError + ErrorType = AppError, >( url: string, - options?: HttpOptionsType + options?: HttpOptionsType, ): Promise> { try { const fingerprintPromise = await fp.load({ monitoring: false }); @@ -65,7 +65,7 @@ export async function httpCall< // Logout user if token is invalid if (data.status === 401) { - Cookies.remove(TOKEN_COOKIE_NAME); + removeAuthToken(); window.location.reload(); return { response: undefined, error: data as ErrorType }; } @@ -92,11 +92,11 @@ export async function httpCall< export async function httpPost< ResponseType = AppResponse, - ErrorType = AppError + ErrorType = AppError, >( url: string, body: Record, - options?: HttpOptionsType + options?: HttpOptionsType, ): Promise> { return httpCall(url, { ...options, @@ -108,7 +108,7 @@ export async function httpPost< export async function httpGet( url: string, queryParams?: Record, - options?: HttpOptionsType + options?: HttpOptionsType, ): Promise> { const searchParams = new URLSearchParams(queryParams).toString(); const queryUrl = searchParams ? `${url}?${searchParams}` : url; @@ -122,11 +122,11 @@ export async function httpGet( export async function httpPatch< ResponseType = AppResponse, - ErrorType = AppError + ErrorType = AppError, >( url: string, body: Record, - options?: HttpOptionsType + options?: HttpOptionsType, ): Promise> { return httpCall(url, { ...options, @@ -138,7 +138,7 @@ export async function httpPatch< export async function httpPut( url: string, body: Record, - options?: HttpOptionsType + options?: HttpOptionsType, ): Promise> { return httpCall(url, { ...options, @@ -149,10 +149,10 @@ export async function httpPut( export async function httpDelete< ResponseType = AppResponse, - ErrorType = AppError + ErrorType = AppError, >( url: string, - options?: HttpOptionsType + options?: HttpOptionsType, ): Promise> { return httpCall(url, { ...options, diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts index 430774550..911c46312 100644 --- a/src/lib/jwt.ts +++ b/src/lib/jwt.ts @@ -31,3 +31,20 @@ export function getUser() { return decodeToken(token); } + +export function setAuthToken(token: string) { + Cookies.set(TOKEN_COOKIE_NAME, token, { + path: '/', + expires: 30, + sameSite: 'lax', + secure: true, + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} + +export function removeAuthToken() { + Cookies.remove(TOKEN_COOKIE_NAME, { + path: '/', + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} From d958a29862d6ea13c41fa18113b85b8cce43e12a Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 27 Feb 2024 00:55:38 +0000 Subject: [PATCH 02/91] Add author page --- pnpm-lock.yaml | 125 +++++++----------- src/components/GuideHeader.astro | 42 +++--- src/components/GuideListItem.astro | 24 ++-- src/data/authors/fernando.md | 11 ++ src/data/authors/kamran.md | 13 ++ src/data/guides/asymptotic-notation.md | 5 +- ...er-blocking-javascript-with-async-defer.md | 5 +- src/data/guides/backend-languages.md | 38 ++---- src/data/guides/basic-authentication.md | 5 +- src/data/guides/basics-of-authentication.md | 5 +- src/data/guides/big-o-notation.md | 5 +- src/data/guides/character-encodings.md | 5 +- src/data/guides/ci-cd.md | 5 +- ...istency-patterns-in-distributed-systems.md | 5 +- src/data/guides/design-patterns-for-humans.md | 5 +- src/data/guides/dhcp-in-one-picture.md | 5 +- src/data/guides/dns-in-one-picture.md | 5 +- .../guides/free-resources-to-learn-llms.md | 5 +- src/data/guides/history-of-javascript.md | 5 +- src/data/guides/how-to-setup-a-jump-server.md | 5 +- src/data/guides/http-basic-authentication.md | 5 +- src/data/guides/http-caching.md | 5 +- src/data/guides/introduction-to-llms.md | 5 +- src/data/guides/journey-to-http2.md | 5 +- src/data/guides/jwt-authentication.md | 5 +- src/data/guides/levels-of-seniority.md | 5 +- src/data/guides/oauth.md | 5 +- src/data/guides/random-numbers.md | 5 +- src/data/guides/scaling-databases.md | 5 +- src/data/guides/session-authentication.md | 5 +- .../guides/session-based-authentication.md | 5 +- .../setup-and-auto-renew-ssl-certificates.md | 5 +- .../guides/single-command-database-setup.md | 5 +- src/data/guides/ssl-tls-https-ssh.md | 5 +- src/data/guides/sso.md | 5 +- src/data/guides/token-authentication.md | 5 +- src/data/guides/unfamiliar-codebase.md | 5 +- src/data/guides/what-are-web-vitals.md | 5 +- src/data/guides/what-is-internet.md | 5 +- src/data/guides/what-is-sli-slo-sla.md | 5 +- src/env.d.ts | 1 - src/icons/github.svg | 2 +- src/icons/globe.svg | 1 + src/icons/linkedin-2.svg | 10 ++ src/lib/author.ts | 73 ++++++++++ src/lib/guide.ts | 32 +++-- src/lib/roadmap.ts | 14 +- src/lib/video.ts | 15 +-- src/pages/authors/[authorId].astro | 110 +++++++++++++++ 49 files changed, 381 insertions(+), 300 deletions(-) create mode 100644 src/data/authors/fernando.md create mode 100644 src/data/authors/kamran.md create mode 100644 src/icons/globe.svg create mode 100644 src/icons/linkedin-2.svg create mode 100644 src/lib/author.ts create mode 100644 src/pages/authors/[authorId].astro diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5dd0b464..b7831ce0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: dependencies: '@astrojs/react': specifier: ^3.0.10 - version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)(vite@5.0.12) + version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3) '@astrojs/sitemap': specifier: ^3.0.5 version: 3.0.5 @@ -21,8 +21,8 @@ dependencies: specifier: ^0.7.1 version: 0.7.1(nanostores@0.9.5)(react@18.2.0) '@types/react': - specifier: ^18.2.55 - version: 18.2.55 + specifier: ^18.2.56 + version: 18.2.58 '@types/react-dom': specifier: ^18.2.19 version: 18.2.19 @@ -45,8 +45,8 @@ dependencies: specifier: ^3.0.5 version: 3.0.5 lucide-react: - specifier: ^0.331.0 - version: 0.331.0(react@18.2.0) + specifier: ^0.334.0 + version: 0.334.0(react@18.2.0) nanoid: specifier: ^5.0.5 version: 5.0.5 @@ -73,7 +73,7 @@ dependencies: version: 18.2.0(react@18.2.0) reactflow: specifier: ^11.10.4 - version: 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + version: 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) rehype-external-links: specifier: ^3.0.0 version: 3.0.0 @@ -90,8 +90,8 @@ dependencies: specifier: ^3.4.1 version: 3.4.1 zustand: - specifier: ^4.5.0 - version: 4.5.0(@types/react@18.2.55)(react@18.2.0) + specifier: ^4.5.1 + version: 4.5.1(@types/react@18.2.58)(react@18.2.0) devDependencies: '@playwright/test': @@ -185,7 +185,7 @@ packages: prismjs: 1.29.0 dev: false - /@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)(vite@5.0.12): + /@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3): resolution: {integrity: sha512-uGRIwKMAn7tva2vxXMyoVIGxWFr0rjZ8ZWIlkTG/vIpnAjD2nM8Cz6B8j7yzj176jvl6gZ6xTbTVPm09aeK0Yw==} engines: {node: '>=18.14.1'} peerDependencies: @@ -194,9 +194,9 @@ packages: react: ^17.0.2 || ^18.0.0 react-dom: ^17.0.2 || ^18.0.0 dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.58 '@types/react-dom': 18.2.19 - '@vitejs/plugin-react': 4.2.1(vite@5.0.12) + '@vitejs/plugin-react': 4.2.1(vite@5.1.3) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ultrahtml: 1.5.2 @@ -1102,39 +1102,39 @@ packages: config-chain: 1.1.13 dev: false - /@reactflow/background@11.3.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/background@11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.0(@types/react@18.2.55)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/controls@11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.0(@types/react@18.2.55)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/core@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==} peerDependencies: react: '>=17' @@ -1150,19 +1150,19 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.0(@types/react@18.2.55)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/minimap@11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 classcat: 5.0.4 @@ -1170,41 +1170,41 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.0(@types/react@18.2.55)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-resizer@2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.0(@types/react@18.2.55)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-toolbar@1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.0(@types/react@18.2.55)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer @@ -1700,11 +1700,11 @@ packages: /@types/react-dom@18.2.19: resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.58 dev: false - /@types/react@18.2.55: - resolution: {integrity: sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==} + /@types/react@18.2.58: + resolution: {integrity: sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==} dependencies: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 @@ -1733,7 +1733,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false - /@vitejs/plugin-react@4.2.1(vite@5.0.12): + /@vitejs/plugin-react@4.2.1(vite@5.1.3): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -1744,7 +1744,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.0.12 + vite: 5.1.3 transitivePeerDependencies: - supports-color dev: false @@ -3996,8 +3996,8 @@ packages: engines: {node: '>=12'} dev: false - /lucide-react@0.331.0(react@18.2.0): - resolution: {integrity: sha512-CHFJ0ve9vaZ7bB2VRAl27SlX1ELh6pfNC0jS96qGpPEEzLkLDGq4pDBFU8RhOoRMqsjXqTzLm9U6bZ1OcIHq7Q==} + /lucide-react@0.334.0(react@18.2.0): + resolution: {integrity: sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 dependencies: @@ -5543,18 +5543,18 @@ packages: loose-envify: 1.4.0 dev: false - /reactflow@11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /reactflow@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/controls': 11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/minimap': 11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-resizer': 2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -6745,41 +6745,6 @@ packages: vfile-message: 4.0.2 dev: false - /vite@5.0.12: - resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.19.11 - postcss: 8.4.33 - rollup: 4.9.6 - optionalDependencies: - fsevents: 2.3.3 - dev: false - /vite@5.1.3: resolution: {integrity: sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==} engines: {node: ^18.0.0 || >=20.0.0} @@ -6964,8 +6929,8 @@ packages: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false - /zustand@4.5.0(@types/react@18.2.55)(react@18.2.0): - resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==} + /zustand@4.5.1(@types/react@18.2.58)(react@18.2.0): + resolution: {integrity: sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' @@ -6979,7 +6944,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.58 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false diff --git a/src/components/GuideHeader.astro b/src/components/GuideHeader.astro index f11662573..089381755 100644 --- a/src/components/GuideHeader.astro +++ b/src/components/GuideHeader.astro @@ -6,28 +6,32 @@ export interface Props { } const { guide } = Astro.props; -const { frontmatter } = guide; -const { author } = frontmatter; +const { frontmatter, author } = guide; --- -
+
-

+

{frontmatter.title}

-
diff --git a/src/components/GuideListItem.astro b/src/components/GuideListItem.astro index 45658649b..118c66dfb 100644 --- a/src/components/GuideListItem.astro +++ b/src/components/GuideListItem.astro @@ -1,5 +1,5 @@ --- -import type { GuideFileType } from "../lib/guide"; +import type { GuideFileType } from '../lib/guide'; export interface Props { guide: GuideFileType; @@ -11,30 +11,34 @@ const { frontmatter, id } = guide; - + {frontmatter.title} { frontmatter.isNew && ( - + New - ) } - diff --git a/src/data/authors/fernando.md b/src/data/authors/fernando.md new file mode 100644 index 000000000..8e936f656 --- /dev/null +++ b/src/data/authors/fernando.md @@ -0,0 +1,11 @@ +--- +name: 'Fernando Doglio' +imageUrl: '/authors/fernando.jpeg' +social: + twitter: 'https://twitter.com/deleteman123' + linkedin: 'https://www.linkedin.com/in/fernandodoglio' +--- + +With two decades of experience in Software Development, Fernando Doglio excels in diverse languages like Ruby, Perl, PHP, Python, and JavaScript. He's led teams in crafting scalable architectures for both in-house and cloud infrastructures. + +An author of 8 technical books and over 250 articles, Fernando's current role as a Dev Advocate allows him to blend his passion for coding with content creation, enhancing developer experiences with products through engaging outreach. \ No newline at end of file diff --git a/src/data/authors/kamran.md b/src/data/authors/kamran.md new file mode 100644 index 000000000..8e5f7c9ee --- /dev/null +++ b/src/data/authors/kamran.md @@ -0,0 +1,13 @@ +--- +name: 'Kamran Ahmed' +imageUrl: '/authors/kamran.jpeg' +social: + linkedin: 'https://www.linkedin.com/in/kamrify' + twitter: 'https://twitter.com/kamrify' + github: 'https://github.com/kamranahmedse' + website: 'https://kamranahmed.info' +--- + +Kamran is the founder of **roadmap.sh**. He has a decade long experience working mostly with startups and scale-ups. Over the years, he has worked with a variety of technologies in a variety of domains and have worn several different hats. He is working full time on roadmap.sh at the moment. + +He is also a Google Developer Expert and a GitHub Star. He is a huge proponent of open-source, and has authored several popular open-source projects. He is [the second most starred developer](https://twitter.com/kamrify/status/1750345095587754382) on GitHub globally. \ No newline at end of file diff --git a/src/data/guides/asymptotic-notation.md b/src/data/guides/asymptotic-notation.md index 4690e94fa..b7db091bb 100644 --- a/src/data/guides/asymptotic-notation.md +++ b/src/data/guides/asymptotic-notation.md @@ -1,10 +1,7 @@ --- title: 'Asymptotic Notation' description: 'Learn the basics of measuring the time and space complexity of algorithms' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Asymptotic Notation - roadmap.sh' description: 'Learn the basics of measuring the time and space complexity of algorithms' diff --git a/src/data/guides/avoid-render-blocking-javascript-with-async-defer.md b/src/data/guides/avoid-render-blocking-javascript-with-async-defer.md index 7718502f8..23852c97d 100644 --- a/src/data/guides/avoid-render-blocking-javascript-with-async-defer.md +++ b/src/data/guides/avoid-render-blocking-javascript-with-async-defer.md @@ -1,10 +1,7 @@ --- title: 'Async and Defer Script Loading' description: 'Learn how to avoid render blocking JavaScript using async and defer scripts.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Async and Defer Script Loading - roadmap.sh' description: 'Learn how to avoid render blocking JavaScript using async and defer scripts.' diff --git a/src/data/guides/backend-languages.md b/src/data/guides/backend-languages.md index a273a2c5e..3c57108e0 100644 --- a/src/data/guides/backend-languages.md +++ b/src/data/guides/backend-languages.md @@ -1,10 +1,7 @@ --- title: 'The 5 Best Backend Development Languages to Master (2024)' description: 'Discover the best backend development languages to master in 2024.' -author: - name: 'Fernando Doglio' - url: 'https://twitter.com/deleteman123' - imageUrl: '/authors/fernando.jpeg' +authorId: fernando excludedBySlug: '/backend/languages' seo: title: 'The 5 Best Backend Development Languages to Master (2024)' @@ -116,9 +113,9 @@ What makes Python extra appealing, especially for beginners, is the fact that re One of Python's standout features is its beginner-friendly syntax, making it an ideal language for those new to programming. The emphasis on readability and the absence of complex syntax (for the most part), eases the learning curve, enabling new developers to quickly grasp fundamental concepts. -Python's community plays a critical role in its accessibility. Abundant learning resources, tutorials, and documentation are readily available, empowering beginners to progress from basic programming principles to advanced backend development seamlessly. Online platforms like Codecademy, Coursera, realpython.com, and even Google offer comprehensive courses tailored to all skill levels. +Python's community plays a critical role in its accessibility. Abundant learning resources, tutorials, and documentation are readily available, empowering beginners to progress from basic programming principles to advanced backend development seamlessly. Online platforms like Codecademy, Coursera, realpython.com, and even Google offer comprehensive courses tailored to all skill levels. -#### Practical Applications and Popular Frameworks +#### Practical Applications and Popular Frameworks Python's versatility is evident in its applicability across a spectrum of industries, from web development and data science to artificial intelligence and automation. In the context of backend development, Python shines brightly with its two standout frameworks: [Django](https://www.djangoproject.com/) and [Flask](https://github.com/pallets/flask). @@ -154,7 +151,7 @@ Java has a massive presence and for good reason (according to [JetBrain’s surv #### Is it worth learning Java? -Now, learning Java, (a strongly typed, object oriented programming language (OOP), is a journey worth taking, but it's not a walk in the park. It's a bit like climbing a mountain – you start at the bottom with the basics, and as you ascend, you get into the nitty-gritty of things like object-oriented programming. The process will force you to learn a lot, which is a great thing, by the end you’ll have a lot of understanding of mechanics and concepts around OOP that can be extrapolated into other languages. However, that can also be overwhelming to some developers who just want to learn by building mini-projects. In those situations, the learning curve of Java might be too long (not steep, but long because there is a lot more to cover than with alternatives such as Python or JavaScript). +Now, learning Java, (a strongly typed, object oriented programming language (OOP), is a journey worth taking, but it's not a walk in the park. It's a bit like climbing a mountain – you start at the bottom with the basics, and as you ascend, you get into the nitty-gritty of things like object-oriented programming. The process will force you to learn a lot, which is a great thing, by the end you’ll have a lot of understanding of mechanics and concepts around OOP that can be extrapolated into other languages. However, that can also be overwhelming to some developers who just want to learn by building mini-projects. In those situations, the learning curve of Java might be too long (not steep, but long because there is a lot more to cover than with alternatives such as Python or JavaScript). That said, the community is big and there are tons of resources, from online courses to forums, helping you navigate the Java landscape. And good reason, considering Java has been around for quite a while. @@ -197,7 +194,7 @@ If you were to rank languages based on the amount of content out there to learn #### Key Frameworks and Development Tools -Now, let's talk about frameworks. In the case of JavaScript, this topic is so varied that recommending a single option for someone just getting started is really hard. +Now, let's talk about frameworks. In the case of JavaScript, this topic is so varied that recommending a single option for someone just getting started is really hard. For example, if you want to go frontend agnostic, or in other words, you don’t care about the technology being used to develop the client side of your app, then a good starting option would be [Express.js](https://expressjs.com/). This framework used to be the industry standard. And while that’s no longer the case, it’s still a perfect first choice if you’re looking for something with the required functionality to make your life a lot easier. @@ -205,7 +202,7 @@ Now, if on the other hand, you’re looking to build the frontend and the backen #### Does it make sense to pick up JavaScript as a backend language? -The answer to this question is always going to be “yes”, whether you’re coming from the frontend and you already have JS experience or if you’re picking it up from scratch. In fact, according to [StackOverflow’s 2023 survey, JavaScript is the most used language by professionals](https://survey.stackoverflow.co/2023/#most-popular-technologies-language-prof) (with 65.85% of the votes). +The answer to this question is always going to be “yes”, whether you’re coming from the frontend and you already have JS experience or if you’re picking it up from scratch. In fact, according to [StackOverflow’s 2023 survey, JavaScript is the most used language by professionals](https://survey.stackoverflow.co/2023/#most-popular-technologies-language-prof) (with 65.85% of the votes). ![JavaScript Interest](/guides/backend-languages/javascript-interest.png) @@ -234,7 +231,7 @@ While there might not be a downside to picking JS, there is no perfect language ### PHP -Now, if you’re looking for something very well established in the web development industry, just like Java but with a shorter learning curve, then you’re probably looking for PHP. +Now, if you’re looking for something very well established in the web development industry, just like Java but with a shorter learning curve, then you’re probably looking for PHP. > As a note about PHP’s relevancy, while many developers might claim that PHP is a dying tech, according to [W3Techs, over 75% of websites with a backend use PHP](https://w3techs.com/technologies/details/pl-php). @@ -242,7 +239,7 @@ It's the glue that holds a ton of websites together, and its longevity in the we #### Ease of Mastery and Vast Library Support -If you're diving into PHP, you wouldn’t be so wrong (no matter what others might tell you). It's got a gentle learning curve, which means you can start building things pretty quickly. Getting everything set up and working will probably take you 10 minutes, and you’ll be writing your first “hello world” 5 minutes after that. +If you're diving into PHP, you wouldn’t be so wrong (no matter what others might tell you). It's got a gentle learning curve, which means you can start building things pretty quickly. Getting everything set up and working will probably take you 10 minutes, and you’ll be writing your first “hello world” 5 minutes after that. The vast community support and an ocean of online resources make mastering PHP a breeze. Plus, its library support is like having a toolkit that's always expanding – you'll find what you need, whether you're wrangling databases, handling forms, or making your website dance with dynamic content. @@ -250,9 +247,9 @@ If you’re looking to pick up PHP, look for the LAMP stack, which stands for ** #### Modern PHP Frameworks and Their Impact -If we’re talking about PHP frameworks, then we gotta talk about [Laravel](https://laravel.com/) and [Symfony](https://symfony.com/). They are like the rockstars of the modern PHP world. +If we’re talking about PHP frameworks, then we gotta talk about [Laravel](https://laravel.com/) and [Symfony](https://symfony.com/). They are like the rockstars of the modern PHP world. -Laravel comes with a lot of tools and features that help you speed up your development process. On the other side, Symfony has a modular architecture, making it a solid choice for projects of all sizes. +Laravel comes with a lot of tools and features that help you speed up your development process. On the other side, Symfony has a modular architecture, making it a solid choice for projects of all sizes. These frameworks showcase how PHP has evolved, staying relevant and powerful in the ever-changing landscape of web development. @@ -337,7 +334,7 @@ Lucky for you, if you’re reading this, that means you’ve found the most comp ### Guided Learning: From Online Courses to Bootcamps -Online courses and bootcamps serve as invaluable companions on your learning expedition. Platforms like Udemy, Coursera, and freeCodeCamp offer comprehensive backend development courses. +Online courses and bootcamps serve as invaluable companions on your learning expedition. Platforms like Udemy, Coursera, and freeCodeCamp offer comprehensive backend development courses. These resources not only cover programming languages like Python, Java, or JavaScript but also dive deep into frameworks like Django, Express.js, or Laravel. For those seeking a more immersive experience, coding bootcamps provide intensive, hands-on training to fast-track your backend development skills. @@ -345,7 +342,7 @@ Whatever choice you go for, make sure you’re not following trends or just copy ### Building Community Connections for Learning Support -Joining developer communities (there are several on Twitter for example), forums like Stack Overflow, or participating in social media groups dedicated to backend development creates a network of support. +Joining developer communities (there are several on Twitter for example), forums like Stack Overflow, or participating in social media groups dedicated to backend development creates a network of support. Engaging with experienced developers, sharing challenges, and seeking advice fosters a collaborative learning environment. Attend local meetups or virtual events if you can to connect with professionals in the field, gaining insights and building relationships that can prove invaluable throughout your journey. @@ -368,7 +365,7 @@ As you accumulate skills and knowledge, showcase your journey through a well-cra When it comes to deciding where to publish this portfolio, you have some options, such as directly on your GitHub profile (if you have one), or perhaps on your own personal website where you can share some design thoughts about each project along with the code. -In the end, the important thing is that you should be sharing your experience somewhere, especially when you don’t have working experience in the field. +In the end, the important thing is that you should be sharing your experience somewhere, especially when you don’t have working experience in the field. ### Conclusion @@ -383,12 +380,3 @@ In the end, there are many backend programming languages to choose from, and wha You’re the one who gets to decide, but just know that no matter what you choose, getting started in backend development is a one-way street. You’ll be learning from this moment on, and you’ll be jumping from one language to the other as the field evolves. Remember that there is a very detailed version of a [backend roadmap here](https://roadmap.sh/backend), it might be a great place to get started! And if you’re also interested in frontend development, there is an [equally handy roadmap](https://roadmap.sh/frontend) here as well! - - - - - - - - - diff --git a/src/data/guides/basic-authentication.md b/src/data/guides/basic-authentication.md index bc344e51a..9b5fd3980 100644 --- a/src/data/guides/basic-authentication.md +++ b/src/data/guides/basic-authentication.md @@ -1,10 +1,7 @@ --- title: 'Basic Authentication' description: 'Understand what is basic authentication and how it is implemented' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Basic Authentication - roadmap.sh' description: 'Understand what is basic authentication and how it is implemented' diff --git a/src/data/guides/basics-of-authentication.md b/src/data/guides/basics-of-authentication.md index 9605e1867..5826dc223 100644 --- a/src/data/guides/basics-of-authentication.md +++ b/src/data/guides/basics-of-authentication.md @@ -1,10 +1,7 @@ --- title: 'Basics of Authentication' description: 'Learn the basics of Authentication and Authorization' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Basics of Authentication - roadmap.sh' description: 'Learn the basics of Authentication and Authorization' diff --git a/src/data/guides/big-o-notation.md b/src/data/guides/big-o-notation.md index b5dbad25c..8eb6c8ae7 100644 --- a/src/data/guides/big-o-notation.md +++ b/src/data/guides/big-o-notation.md @@ -1,10 +1,7 @@ --- title: 'Big-O Notation' description: 'Easy to understand explanation of Big-O notation without any fancy terms' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Big-O Notation - roadmap.sh' description: 'Easy to understand explanation of Big-O notation without any fancy terms' diff --git a/src/data/guides/character-encodings.md b/src/data/guides/character-encodings.md index 6d4dcf127..268390d2d 100644 --- a/src/data/guides/character-encodings.md +++ b/src/data/guides/character-encodings.md @@ -1,10 +1,7 @@ --- title: 'Character Encodings' description: 'Covers the basics of character encodings and explains ASCII vs Unicode' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Character Encodings - roadmap.sh' description: 'Covers the basics of character encodings and explains ASCII vs Unicode' diff --git a/src/data/guides/ci-cd.md b/src/data/guides/ci-cd.md index bfd9cda06..135c62b69 100644 --- a/src/data/guides/ci-cd.md +++ b/src/data/guides/ci-cd.md @@ -1,10 +1,7 @@ --- title: 'What is CI and CD?' description: 'Learn the basics of CI/CD and how to implement that with GitHub Actions.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'What is CI and CD? - roadmap.sh' description: 'Learn the basics of CI/CD and how to implement that with GitHub Actions.' diff --git a/src/data/guides/consistency-patterns-in-distributed-systems.md b/src/data/guides/consistency-patterns-in-distributed-systems.md index 25b69c2c1..1f8f5ee2a 100644 --- a/src/data/guides/consistency-patterns-in-distributed-systems.md +++ b/src/data/guides/consistency-patterns-in-distributed-systems.md @@ -1,10 +1,7 @@ --- title: 'Consistency Patterns' description: 'Everything you need to know about Week, Strong and Eventual Consistency' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Consistency Patterns - roadmap.sh' description: 'Everything you need to know about Week, Strong and Eventual Consistency' diff --git a/src/data/guides/design-patterns-for-humans.md b/src/data/guides/design-patterns-for-humans.md index 57cc96169..d3eace516 100644 --- a/src/data/guides/design-patterns-for-humans.md +++ b/src/data/guides/design-patterns-for-humans.md @@ -1,10 +1,7 @@ --- title: 'Design Patterns for Humans' description: 'A language agnostic, ultra-simplified explanation to design patterns' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Design Patterns for Humans - roadmap.sh' description: 'A language agnostic, ultra-simplified explanation to design patterns' diff --git a/src/data/guides/dhcp-in-one-picture.md b/src/data/guides/dhcp-in-one-picture.md index 5c9179378..b32df3362 100644 --- a/src/data/guides/dhcp-in-one-picture.md +++ b/src/data/guides/dhcp-in-one-picture.md @@ -1,10 +1,7 @@ --- title: 'DHCP in One Picture' description: 'Here is what happens when a new device joins the network.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'DHCP in One Picture - roadmap.sh' description: 'Here is what happens when a new device joins the network.' diff --git a/src/data/guides/dns-in-one-picture.md b/src/data/guides/dns-in-one-picture.md index 8cf7bf484..8acb42731 100644 --- a/src/data/guides/dns-in-one-picture.md +++ b/src/data/guides/dns-in-one-picture.md @@ -1,10 +1,7 @@ --- title: 'DNS in One Picture' description: 'Quick illustrative guide on how a website is found on the internet.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'DNS in One Picture - roadmap.sh' description: 'Quick illustrative guide on how a website is found on the internet.' diff --git a/src/data/guides/free-resources-to-learn-llms.md b/src/data/guides/free-resources-to-learn-llms.md index e2ea0cd60..b19ef2982 100644 --- a/src/data/guides/free-resources-to-learn-llms.md +++ b/src/data/guides/free-resources-to-learn-llms.md @@ -1,10 +1,7 @@ --- title: '5 Free Resources to Master LLMs' description: 'Dive into the world of LLMs with these free resources' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: '5 Free Resources to Master Language Models (LLMs) - roadmap.sh' description: 'Looking to dive into the fascinating world of Language Models (LLMs)? Discover the top 5 free resources that will help you learn and excel in understanding LLMs. From comprehensive tutorials to interactive courses, this blog post provides you with the ultimate guide to sharpen your skills and unravel the potential of language models. Start your journey today and become a pro in LLMs without spending a dime!' diff --git a/src/data/guides/history-of-javascript.md b/src/data/guides/history-of-javascript.md index be9cc030c..637f71df4 100644 --- a/src/data/guides/history-of-javascript.md +++ b/src/data/guides/history-of-javascript.md @@ -1,10 +1,7 @@ --- title: 'Brief History of JavaScript' description: 'How JavaScript was introduced and evolved over the years' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Brief History of JavaScript - roadmap.sh' description: 'How JavaScript was introduced and evolved over the years' diff --git a/src/data/guides/how-to-setup-a-jump-server.md b/src/data/guides/how-to-setup-a-jump-server.md index 70642b784..63bcd8f65 100644 --- a/src/data/guides/how-to-setup-a-jump-server.md +++ b/src/data/guides/how-to-setup-a-jump-server.md @@ -1,10 +1,7 @@ --- title: 'Jump Servers: What, Why and How' description: 'Learn what is a Jump Server and how to set it up for SSH access.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Jump Servers: What, Why and How - roadmap.sh' description: 'Learn what is a Jump Server and how to set it up for SSH access.' diff --git a/src/data/guides/http-basic-authentication.md b/src/data/guides/http-basic-authentication.md index 833335990..25b52655b 100644 --- a/src/data/guides/http-basic-authentication.md +++ b/src/data/guides/http-basic-authentication.md @@ -1,10 +1,7 @@ --- title: 'HTTP Basic Authentication' description: 'Learn what is HTTP Basic Authentication and how to implement it in Node.js' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'HTTP Basic Authentication - roadmap.sh' description: 'Learn what is HTTP Basic Authentication and how to implement it in Node.js' diff --git a/src/data/guides/http-caching.md b/src/data/guides/http-caching.md index 41ef7ba89..bb8834925 100644 --- a/src/data/guides/http-caching.md +++ b/src/data/guides/http-caching.md @@ -1,10 +1,7 @@ --- title: 'HTTP Caching' description: 'Everything you need to know about web caching' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'HTTP Caching - roadmap.sh' description: 'Everything you need to know about web caching' diff --git a/src/data/guides/introduction-to-llms.md b/src/data/guides/introduction-to-llms.md index dc4f17ea7..b835bc512 100644 --- a/src/data/guides/introduction-to-llms.md +++ b/src/data/guides/introduction-to-llms.md @@ -1,10 +1,7 @@ --- title: 'Introduction to LLMs' description: 'What are LLMs, how does ChatGPT and other LLMs work?' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Introduction to LLMs - roadmap.sh' description: 'What are LLMs, how does ChatGPT and other LLMs work?' diff --git a/src/data/guides/journey-to-http2.md b/src/data/guides/journey-to-http2.md index 153f56ef8..209c5d2e6 100644 --- a/src/data/guides/journey-to-http2.md +++ b/src/data/guides/journey-to-http2.md @@ -1,10 +1,7 @@ --- title: 'Journey to HTTP/2' description: 'The evolution of HTTP. How it all started and where we stand today' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Journey to HTTP/2 - roadmap.sh' description: 'The evolution of HTTP. How it all started and where we stand today' diff --git a/src/data/guides/jwt-authentication.md b/src/data/guides/jwt-authentication.md index 4d09a3b73..518a9f95d 100644 --- a/src/data/guides/jwt-authentication.md +++ b/src/data/guides/jwt-authentication.md @@ -1,10 +1,7 @@ --- title: 'JWT Authentication' description: 'Understand what is JWT authentication and how is it implemented' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'JWT Authentication - roadmap.sh' description: 'Understand what is JWT authentication and how is it implemented' diff --git a/src/data/guides/levels-of-seniority.md b/src/data/guides/levels-of-seniority.md index f5c71e946..eb7c2f42c 100644 --- a/src/data/guides/levels-of-seniority.md +++ b/src/data/guides/levels-of-seniority.md @@ -1,10 +1,7 @@ --- title: 'Levels of Seniority' description: 'How to Step Up as a Junior, Mid Level or a Senior Developer?' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Levels of Seniority - roadmap.sh' description: 'How to Step Up as a Junior, Mid Level or a Senior Developer?' diff --git a/src/data/guides/oauth.md b/src/data/guides/oauth.md index f56fde7ea..6834d67d7 100644 --- a/src/data/guides/oauth.md +++ b/src/data/guides/oauth.md @@ -1,10 +1,7 @@ --- title: 'OAuth — Open Authorization' description: 'Learn and understand what is OAuth and how it works' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'OAuth — Open Authorization - roadmap.sh' description: 'Learn and understand what is OAuth and how it works' diff --git a/src/data/guides/random-numbers.md b/src/data/guides/random-numbers.md index 15d3d221a..f193dd15a 100644 --- a/src/data/guides/random-numbers.md +++ b/src/data/guides/random-numbers.md @@ -1,10 +1,7 @@ --- title: 'Random Numbers: Are they?' description: 'Learn how they are generated and why they may not be truly random.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Random Numbers: Are they? - roadmap.sh' description: 'Learn how they are generated and why they may not be truly random.' diff --git a/src/data/guides/scaling-databases.md b/src/data/guides/scaling-databases.md index bfc6b90e3..af2f49df8 100644 --- a/src/data/guides/scaling-databases.md +++ b/src/data/guides/scaling-databases.md @@ -1,10 +1,7 @@ --- title: 'Scaling Databases' description: 'Learn the ups and downs of different database scaling strategies' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Scaling Databases - roadmap.sh' description: 'Learn the ups and downs of different database scaling strategies' diff --git a/src/data/guides/session-authentication.md b/src/data/guides/session-authentication.md index cb91ece61..c86d5f1af 100644 --- a/src/data/guides/session-authentication.md +++ b/src/data/guides/session-authentication.md @@ -1,10 +1,7 @@ --- title: 'Session Based Authentication' description: 'Understand what is session based authentication and how it is implemented' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Session Based Authentication - roadmap.sh' description: 'Understand what is session based authentication and how it is implemented' diff --git a/src/data/guides/session-based-authentication.md b/src/data/guides/session-based-authentication.md index e6bba3247..cc30c62b1 100644 --- a/src/data/guides/session-based-authentication.md +++ b/src/data/guides/session-based-authentication.md @@ -1,10 +1,7 @@ --- title: 'Session Based Authentication' description: 'Learn what is Session Based Authentication and how to implement it in Node.js' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Session Based Authentication - roadmap.sh' description: 'Learn what is Session Based Authentication and how to implement it in Node.js' diff --git a/src/data/guides/setup-and-auto-renew-ssl-certificates.md b/src/data/guides/setup-and-auto-renew-ssl-certificates.md index 3acbdbc87..0f7ef3750 100644 --- a/src/data/guides/setup-and-auto-renew-ssl-certificates.md +++ b/src/data/guides/setup-and-auto-renew-ssl-certificates.md @@ -1,10 +1,7 @@ --- title: "Guide to Let's Encrypt SSL Setup" description: "Learn how to protect your website using Let's Encrypt SSL Certificates." -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: "Guide to Let's Encrypt SSL Setup - roadmap.sh" description: "Learn how to protect your website using Let's Encrypt SSL Certificates." diff --git a/src/data/guides/single-command-database-setup.md b/src/data/guides/single-command-database-setup.md index 129c8a5e8..e280f8819 100644 --- a/src/data/guides/single-command-database-setup.md +++ b/src/data/guides/single-command-database-setup.md @@ -1,10 +1,7 @@ --- title: 'Single Command Database Setup' description: 'Learn how to run MySQL, PostgreSQL, or MongoDB in Docker with single Command' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Single Command Database Setup - roadmap.sh' description: 'Learn how to run MySQL, PostgreSQL, or MongoDB in Docker with single Command' diff --git a/src/data/guides/ssl-tls-https-ssh.md b/src/data/guides/ssl-tls-https-ssh.md index 1832b62ba..b21b140b7 100644 --- a/src/data/guides/ssl-tls-https-ssh.md +++ b/src/data/guides/ssl-tls-https-ssh.md @@ -1,10 +1,7 @@ --- title: 'SSL vs TLS vs SSH' description: 'Quick tidbit on the differences between SSL, TLS, HTTPS and SSH' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'SSL vs TLS vs SSH - roadmap.sh' description: 'Quick tidbit on the differences between SSL, TLS, HTTPS and SSH' diff --git a/src/data/guides/sso.md b/src/data/guides/sso.md index 6d3987015..1dd61cef7 100644 --- a/src/data/guides/sso.md +++ b/src/data/guides/sso.md @@ -1,10 +1,7 @@ --- title: 'SSO — Single Sign On' description: 'Learn the basics of SAML and understand how does Single Sign On work.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'SSO — Single Sign On - roadmap.sh' description: 'Learn the basics of SAML and understand how does Single Sign On work.' diff --git a/src/data/guides/token-authentication.md b/src/data/guides/token-authentication.md index fcfac7986..bb82324a1 100644 --- a/src/data/guides/token-authentication.md +++ b/src/data/guides/token-authentication.md @@ -1,10 +1,7 @@ --- title: 'Token Based Authentication' description: 'Understand what is token based authentication and how it is implemented' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Token Based Authentication - roadmap.sh' description: 'Understand what is token based authentication and how it is implemented' diff --git a/src/data/guides/unfamiliar-codebase.md b/src/data/guides/unfamiliar-codebase.md index c6c07d809..41396c3e2 100644 --- a/src/data/guides/unfamiliar-codebase.md +++ b/src/data/guides/unfamiliar-codebase.md @@ -1,10 +1,7 @@ --- title: 'Unfamiliar Codebase' description: 'Tips on getting familiar with an unfamiliar codebase' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'Unfamiliar Codebase - roadmap.sh' description: 'Tips on getting familiar with an unfamiliar codebase' diff --git a/src/data/guides/what-are-web-vitals.md b/src/data/guides/what-are-web-vitals.md index 00a8138fe..bdc782d81 100644 --- a/src/data/guides/what-are-web-vitals.md +++ b/src/data/guides/what-are-web-vitals.md @@ -1,10 +1,7 @@ --- title: 'What are Web Vitals?' description: 'Learn what are the core web vitals and how to measure them.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'What are Web Vitals? - roadmap.sh' description: 'Learn what are the core web vitals and how to measure them.' diff --git a/src/data/guides/what-is-internet.md b/src/data/guides/what-is-internet.md index e9a58773e..044676320 100644 --- a/src/data/guides/what-is-internet.md +++ b/src/data/guides/what-is-internet.md @@ -1,10 +1,7 @@ --- title: 'How does the internet work?' description: 'Learn the basics of internet and everything involved with this short video series' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'How does the internet work? - roadmap.sh' description: 'Learn the basics of internet and everything involved with this short video series' diff --git a/src/data/guides/what-is-sli-slo-sla.md b/src/data/guides/what-is-sli-slo-sla.md index c3bc63fc3..53e2cf386 100644 --- a/src/data/guides/what-is-sli-slo-sla.md +++ b/src/data/guides/what-is-sli-slo-sla.md @@ -1,10 +1,7 @@ --- title: 'SLIs, SLOs and SLAs' description: 'Learn what are different indicators for performance identification of any service.' -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' seo: title: 'SLIs, SLOs and SLAs - roadmap.sh' description: 'Learn what are different indicators for performance identification of any service.' diff --git a/src/env.d.ts b/src/env.d.ts index 2cc9d6465..057b60995 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,4 +1,3 @@ -/// import 'astro/client'; interface ImportMetaEnv { diff --git a/src/icons/github.svg b/src/icons/github.svg index 44124c848..020512f58 100644 --- a/src/icons/github.svg +++ b/src/icons/github.svg @@ -1 +1 @@ - + diff --git a/src/icons/globe.svg b/src/icons/globe.svg new file mode 100644 index 000000000..9c33da5df --- /dev/null +++ b/src/icons/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/linkedin-2.svg b/src/icons/linkedin-2.svg new file mode 100644 index 000000000..7013c1e4e --- /dev/null +++ b/src/icons/linkedin-2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/lib/author.ts b/src/lib/author.ts new file mode 100644 index 000000000..86afb2a43 --- /dev/null +++ b/src/lib/author.ts @@ -0,0 +1,73 @@ +import type { MarkdownFileType } from './file'; + +export interface AuthorFrontmatter { + name: string; + imageUrl: string; + social: { + twitter: string; + github: string; + linkedin: string; + website: string; + }; +} + +export type AuthorFileType = MarkdownFileType & { + id: string; +}; + +function authorPathToId(filePath: string): string { + const fileName = filePath.split('/').pop() || ''; + + return fileName.replace('.md', ''); +} + +/** + * Gets the IDs of all the authors available on the website + * + * @returns string[] Array of author IDs + */ +export async function getAuthorIds() { + const authorFiles = import.meta.glob( + '/src/data/authors/*.md', + { + eager: true, + }, + ); + + console.log(Object.keys(authorFiles)); + return Object.keys(authorFiles).map(authorPathToId); +} + +export async function getAllAuthors(): Promise { + const authorFilesMap: Record = + import.meta.glob('/src/data/authors/*.md', { + eager: true, + }); + + const authorFiles = Object.values(authorFilesMap); + + return authorFiles.map((authorFile) => ({ + ...authorFile, + id: authorPathToId(authorFile.file), + })); +} + +export async function getAuthorById(id: string): Promise { + const authorFilesMap: Record = + import.meta.glob('/src/data/authors/*.md', { + eager: true, + }); + + const authorFile = Object.values(authorFilesMap).find((authorFile) => { + return authorPathToId(authorFile.file) === id; + }); + + if (!authorFile) { + throw new Error(`Author with ID ${id} not found`); + } + + return { + ...authorFile, + id: authorPathToId(authorFile.file), + }; +} diff --git a/src/lib/guide.ts b/src/lib/guide.ts index 8bdfb678d..464d0e404 100644 --- a/src/lib/guide.ts +++ b/src/lib/guide.ts @@ -1,13 +1,10 @@ import type { MarkdownFileType } from './file'; +import { type AuthorFileType, getAllAuthors } from './author.ts'; export interface GuideFrontmatter { title: string; description: string; - author: { - name: string; - url: string; - imageUrl: string; - }; + authorId: string; canonicalUrl?: string; // alternate path where this guide has been published excludedBySlug?: string; @@ -27,6 +24,7 @@ export interface GuideFrontmatter { export type GuideFileType = MarkdownFileType & { id: string; + author: AuthorFileType; }; /** @@ -41,23 +39,33 @@ function guidePathToId(filePath: string): string { return fileName.replace('.md', ''); } +export async function getGuidesByAuthor( + authorId: string, +): Promise { + const allGuides = await getAllGuides(); + + return allGuides.filter((guide) => guide.author?.id === authorId); +} + /** * Gets all the guides sorted by the publishing date * @returns Promisifed guide files */ export async function getAllGuides(): Promise { // @ts-ignore - const guides = await import.meta.glob( - '/src/data/guides/*.md', - { - eager: true, - }, - ); + const guides = import.meta.glob('/src/data/guides/*.md', { + eager: true, + }); + + const allAuthors = await getAllAuthors(); const guideFiles = Object.values(guides) as GuideFileType[]; - const enrichedGuides = guideFiles.map((guideFile) => ({ + const enrichedGuides: GuideFileType[] = guideFiles.map((guideFile) => ({ ...guideFile, id: guidePathToId(guideFile.file), + author: allAuthors.find( + (author) => author.id === guideFile.frontmatter.authorId, + )!, })); return enrichedGuides.sort( diff --git a/src/lib/roadmap.ts b/src/lib/roadmap.ts index 6d12fe344..cb4983405 100644 --- a/src/lib/roadmap.ts +++ b/src/lib/roadmap.ts @@ -60,7 +60,7 @@ function roadmapPathToId(filePath: string): string { * @returns string[] Array of roadmap IDs */ export async function getRoadmapIds() { - const roadmapFiles = await import.meta.glob( + const roadmapFiles = import.meta.glob( '/src/data/roadmaps/*/*.md', { eager: true, @@ -79,14 +79,14 @@ export async function getRoadmapIds() { export async function getRoadmapsByTag( tag: string, ): Promise { - const roadmapFilesMap = await import.meta.glob( + const roadmapFilesMap = import.meta.glob( '/src/data/roadmaps/*/*.md', { eager: true, }, ); - const roadmapFiles = Object.values(roadmapFilesMap); + const roadmapFiles: RoadmapFileType[] = Object.values(roadmapFilesMap); const filteredRoadmaps = roadmapFiles .filter((roadmapFile) => roadmapFile.frontmatter.tags.includes(tag)) .map((roadmapFile) => ({ @@ -100,12 +100,10 @@ export async function getRoadmapsByTag( } export async function getRoadmapById(id: string): Promise { - const roadmapFilesMap = await import.meta.glob( - '/src/data/roadmaps/*/*.md', - { + const roadmapFilesMap: Record = + import.meta.glob('/src/data/roadmaps/*/*.md', { eager: true, - }, - ); + }); const roadmapFile = Object.values(roadmapFilesMap).find((roadmapFile) => { return roadmapPathToId(roadmapFile.file) === id; diff --git a/src/lib/video.ts b/src/lib/video.ts index 22206bc8a..d0eeb9ec6 100644 --- a/src/lib/video.ts +++ b/src/lib/video.ts @@ -1,4 +1,5 @@ import type { MarkdownFileType } from './file'; +import type {AuthorFileType} from "./author.ts"; export interface VideoFrontmatter { title: string; @@ -17,13 +18,14 @@ export interface VideoFrontmatter { date: string; sitemap: { priority: number; - changefreq: 'daily' | 'weekly' | 'monthly' | 'yealry'; + changefreq: 'daily' | 'weekly' | 'monthly' | 'yearly'; }; tags: string[]; } export type VideoFileType = MarkdownFileType & { id: string; + author: AuthorFileType; }; /** @@ -43,12 +45,9 @@ function videoPathToId(filePath: string): string { * @returns Promisifed video files */ export async function getAllVideos(): Promise { - const videos = await import.meta.glob( - '/src/data/videos/*.md', - { - eager: true, - } - ); + const videos = import.meta.glob('/src/data/videos/*.md', { + eager: true, + }); const videoFiles = Object.values(videos); const enrichedVideos = videoFiles.map((videoFile) => ({ @@ -59,6 +58,6 @@ export async function getAllVideos(): Promise { return enrichedVideos.sort( (a, b) => new Date(b.frontmatter.date).valueOf() - - new Date(a.frontmatter.date).valueOf() + new Date(a.frontmatter.date).valueOf(), ); } diff --git a/src/pages/authors/[authorId].astro b/src/pages/authors/[authorId].astro new file mode 100644 index 000000000..61097093a --- /dev/null +++ b/src/pages/authors/[authorId].astro @@ -0,0 +1,110 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import AstroIcon from '../../components/AstroIcon.astro'; +import { getGuidesByAuthor } from '../../lib/guide'; +import { getAllVideos } from '../../lib/video'; +import GuideListItem from '../../components/GuideListItem.astro'; +import { getAuthorById, getAuthorIds } from '../../lib/author'; + +interface Params extends Record {} + +export async function getStaticPaths() { + const authorIds = await getAuthorIds(); + + return authorIds.map((authorId) => ({ + params: { authorId }, + })); +} + +const { authorId } = Astro.params; + +const author = await getAuthorById(authorId); + +const guides = await getGuidesByAuthor(authorId); +const videos = await getAllVideos(); +--- + + +
+
+
+
+

{author.frontmatter.name}

+
+ +
+ +
+
+ { + author.frontmatter.social?.github && ( + + + + ) + } + { + author.frontmatter.social.twitter && ( + + + + ) + } + { + author.frontmatter.social.linkedin && ( + + + + ) + } + { + author.frontmatter.social.website && ( + + + + ) + } +
+
+
+ +
+
+
+ {guides.map((guide) => )} +
+
+
From 44b62c2b2de3ce08dee7be208401df681cd3a04e Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 27 Feb 2024 10:57:25 +0000 Subject: [PATCH 03/91] Add author pages --- public/authors/peter-thaleikis.png | Bin 0 -> 153168 bytes src/components/VideoHeader.astro | 26 ++++++++---------- src/data/authors/ebrahim-bharmal.md | 8 ++++++ src/data/authors/jesse-li.md | 9 ++++++ src/data/authors/peter-thaleikis.md | 9 ++++++ src/data/guides/proxy-servers.md | 5 +--- src/data/guides/torrent-client.md | 5 +--- ...it-and-they-will-come-wont-work-anymore.md | 5 +--- src/data/videos/acid-explained.md | 5 +--- src/data/videos/all-about-http-caching.md | 5 +--- src/data/videos/array-structure.md | 5 +--- .../arrays-and-objects-in-javascript.md | 5 +--- src/data/videos/async-javascript.md | 5 +--- src/data/videos/basic-authentication.md | 5 +--- src/data/videos/basics-of-authentication.md | 5 +--- src/data/videos/big-o-notation.md | 5 +--- src/data/videos/content-delivery-networks.md | 5 +--- src/data/videos/dns-explained.md | 5 +--- src/data/videos/dns-records.md | 5 +--- src/data/videos/floating-point-arithmetic.md | 5 +--- .../freeze-and-seal-objects-in-javascript.md | 5 +--- src/data/videos/graph-data-structure.md | 5 +--- src/data/videos/hash-table-data-structure.md | 5 +--- src/data/videos/heap-data-structure.md | 5 +--- src/data/videos/how-to-use-css-variables.md | 5 +--- src/data/videos/how-to-use-github-actions.md | 5 +--- src/data/videos/javascript-fetch-api.md | 5 +--- src/data/videos/linked-list-data-structure.md | 5 +--- src/data/videos/load-balancers-101.md | 5 +--- src/data/videos/osi-model.md | 5 +--- src/data/videos/practical-intro-to-react.md | 5 +--- src/data/videos/promises-in-javascript.md | 5 +--- src/data/videos/queue-data-structure.md | 5 +--- src/data/videos/random-number-generators.md | 5 +--- src/data/videos/scaling-the-unscalable.md | 5 +--- .../videos/session-based-authentication.md | 5 +--- src/data/videos/ssh-ssl-tls.md | 5 +--- src/data/videos/stack-data-structure.md | 5 +--- src/data/videos/system-design-101.md | 5 +--- src/data/videos/tcp-ip-model.md | 5 +--- .../videos/transport-protocols-tcp-vs-udp.md | 5 +--- src/data/videos/tree-data-structure.md | 5 +--- src/data/videos/what-are-data-structures.md | 5 +--- src/data/videos/what-is-cap-theorem.md | 5 +--- .../videos/what-is-dependency-injection.md | 5 +--- .../what-is-dom-shadow-dom-virtual-dom.md | 5 +--- .../videos/what-is-eventual-consistency.md | 5 +--- src/data/videos/yaml-in-depth.md | 5 +--- src/lib/video.ts | 24 ++++++++++++---- src/pages/authors/[authorId].astro | 11 ++++++-- 50 files changed, 107 insertions(+), 195 deletions(-) create mode 100644 public/authors/peter-thaleikis.png create mode 100644 src/data/authors/ebrahim-bharmal.md create mode 100644 src/data/authors/jesse-li.md create mode 100644 src/data/authors/peter-thaleikis.md diff --git a/public/authors/peter-thaleikis.png b/public/authors/peter-thaleikis.png new file mode 100644 index 0000000000000000000000000000000000000000..a338ab49794a4dad27d00775d431841ab06b319c GIT binary patch literal 153168 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

Rj9t#U#X^92hJRm*f7i8vJy+5x<=q zgCC!0#IH}Z;>Tl+0&q2cJt2Uen~Z;-6=0v820TAQ!0i%n$Kqel%5yj8xHTK?feA%^z=B1tK0D5j!L{RHx5sX@y50?O$|2{8{qQPc)YQ!5LYJ0 z>V zm5ag(xj5`BcaeBJ5NG6~aIPZ^Pqqd~obG{Rqdl;1xD7TAGQhebapmPTY^^Ypi<=oX zmYbnhUJJdI#RB#M0ew-C+%L9L=jhzfBEUVxu9#owjP86FOir~yXR?)CDD5#l)dAz9 z%~c#+!%Zw%;A{j)cv}Os+Y#a_k8sm|RwWCW9Y6-TX&zIVuc-iRgd%@K->-@})@pyjUbXQjk*zq*FszPW*a{%{-r{^`E~{`dd<3BP=M1ONKOjwpc54=ZFg_i+N+2HMS2o!||qmxG*^cFLlS^qb-B+ z?cq`Q;;|}xb+iiKNX$#s{q;->em^H}@yP}S@Xse&@bv+@%;N;ync_-!;vY{5pciKY zm%9bznZOmfe{+E-a5w(-(lq@2g(>*Q)#<=XqR7{0&(d*2CC^i$F^k3<4pPaK>2w_UjsFXk;yEorkLKA853KKw7FLDSWR^>(`8lJKb$kn z!Qd>lWbLmWV68lpJI`7ZefH(%h8E9**{zbZsMxxg4I>DzpX)<_`?+ZVyU2Z44;{pE z>ngXdK8DJTCcx$d)Z>w(qyGb7dksi-UJ;17 zRbg0I7lY+ZNm$>Jg*`Ke;KZUjJh`%20sP$VNqF(G*?8&bJiL5tF>ak)hTBiA!<};* z@%9V*@Xqyv_~7>A_(;_9-rY0G1-}33B~jli`1R|T6pVU+sko0C0jBcew>Omi^u<;D z^3`<}BNO0nKe~+PPOe1Hl%dE=bi}|Id&C7ABV~XE@?xD3FJKl#yDE6oeEJ|!KInm- zc8F(Hi-lG^&R)zMJUuSn2J>^rwK)WM2);ksfUgeM;k)DY z`1z?OrM`5ZHGrE%eVg#}@iu&SxJ3Y-C@%9%{Oi2fxtaK<+~WCp0_hUq+9LdZS%7^* z?!PY2-4=UwKJeN?&2G#6n*#gG^YHJhvvL24HhgrT25)XE#jy@gY^gNB6XU$_()@V5 zx+oPNZ?3@2xv4nY7KG@#^$!oUHM~V?*q)exSI+ zav@kR;B6K-_Y0Kg+Wm1u-s_#7RNU@Kl>a_n-oHN%)th0r{GYsl9H{S$gAHytFxnkE zs@<@w)*G9LyNV0#gjGWv6~I$dwd^bwI55R>Vv-3uQ_RqnXs+srI}_x0lI<`j(-ZS@ ze9)8YkNMeN=uUA#XM`#0`)TkpZFHE#v=x1|Bdmlw8{b|$P$=r8G$9@imhoK zh+R`kaeP5FPW3k8^oCYk*wd-tedXjLy!vD>-aNMse-Y(<`|>V)c=ISezH=ZSWKw*$YO>cnrSr{Leuh}u5)|Izl|VNqpE+cz0S5hQ1jq=F57DZCaf<=Q-zjzw3SfsB3p`nugxJ zf9tMVt5y|({AxH#4-nMDqm{irG#I6)hXI%P{M&J$|4VpBqf}r%%X8;HQ}*nu-U?!z zDN-O`nw5e>qr$MEyB#)mw^5T`*QYd4(8>S0I86^H`i0>@j~ci=E)CC@(3RiN4=jV-OQs%1HR-c%3sQ}xuO*@h10bkD2d(%2~6m=cGp6XS4-gT$A8 zywptj+RnCEnPq{c?akE;{fs6RFqiPoYHp9ojG4zvvv0=Ag|e+MOj{PptttD5c?{Af zsk+FlG~?Xd8V2Z9(_9t79~9|~VKFW!igv@$I=1M+L0FcvGHI>&9X64rF)?fDoM@o- z5T`gW_ND4Jt|Dc8ERa*j4LxJ3A}_icI@WYUGhZ7*ZLPXa)U`2GrFvu+lbC?)PjDqM zL((Fp*RaI0x_-ktlMVkmfaSFkLn{-P+E9yu1%82EwyH`Qrz$p@0=ql@8-PDC8#zai zEz1BF6_ymr4*-8+y7>1~fNh)za7L6dltmb!b)+#eqsuFpbL!h5x4s5)pR_6%)SRw# z>mZEFioo2$B&-_QhH-EQY@68)`U$Clyl$yNC2(q{Z} zbqD^q`30WbJAh|DA5{t}5X#K54n_f3W}9{3;>{Ih0p91*lCW(6`2Bxwep8y>FOtWtU*kY7kq$_=X6pw`VQZaC4B5-vQ zfu2l-rd=T1XN&Rr_;5TsFaVGD^~LQ~?Qwt#Ij5B#CNVbsYG8F784`%Y1B0-n8DS)l z+d4QBHoNgp*1?@Q zN%(eZ94=0bq|(=981I9PIgVJ=Mjuld`+g>~(u{kjq?X5If?M3c5F-e$q*(T)`VOjV zfFW^a7#vdpeZox?z_LUnTM#o`w8<#}S#_}RkyK3s^b4tofwe7B=x>6~u39pQsIV-B zH8RtZPFgt|A;Y!2Drei6@6(3A*FC}ped4QNK!d92UB4>2MYtm~$PTUiEYZZ*0?F>> z5GSowsgMG&)Z9{gicKXB)>CDL1Y+6jqJXrNHmzDMa2>!BGb?BXkCDZ=e8nEsKBc96p-B>p;M5?KOyPoHdBerF5)oS^X=4)w+z6jPIWM?cM?7)z$?ae zz}8vaaA5UdoZ425bGs+t+WuMi{@7yNJG~q~T-bnz-|fcZ+h5}8{eyV%;}NyPsN^py z>LbZ@KBHjN0jZm`(y6e3tb6?GDPQyaoLVcEr6gH$lILemEXAzJ-O;UG1ezvz6I@5M zi1S6Kl-g)hFAzbJi47jb4R{VDO}##*BreftWlKch>58- zSlG%A%QLL7v6Bl9_722}!L@O7dJ=wE&;&nxo{s0+JK**1u2kI|yxiUsPd0VKi*3F5 zxB$<$7NBI$0KDDZA0@j6;MKN1c)7J7O1@xxdzgTJJx;UZe0;tLrB?{om9cn#X&jA! zUztipo~hZj*?c~o$C)TOKLJm_9*&nMN8$B}QTT07J}xh4itYXVv9hZT*5^6n=g9kt^AmA)Y%Lrc6@+~QeDOtJFB}{kjKf1~Vrw^d%t<#yQIsBf2XOEsh=X|_jpW}P zOnCcJbqDZX8WL%W0ThOUnv8F&>!G8!9y)kw$4-oQyH+E>1Ukz@OV#Yb*Ax))KGijo zWw1`pT5^iS#Ilavnd+QZ!vZ~OS~Bc0BfKUbtbq6IK;Ex|ljuFxcSUwBOEl%6F0pbP z)jHbN0AU19!Y9>piXNEMLkqCfjsmc1WF|!eR6{vwSE|Lo;zIumz7D*zvyjoT;aI-gld^(DYJ_u$E$eR%fZ&yQke3g}hG4n@iCjDa1oq^%oPws%$l z?=SFI0DnI%2|q4uil3J?$D19UP_nnXQe6S}(FP6RSF%BY0IRt7%fZTheoHleyN53D zS0gomk5GM&OweNCi{li$xH19n2=99;Zt1mIz>WF9cMFu2T$`hwmt2{FzfKh4DFJ?a zVLVEx%#RKX#EsP*aA-n2mUnl?iZ1pFEu)Ui-w&hu2cW+l58s>|06T|WK$Uy8YaKri>Yi!ST z!~_+utA}iyAjkx~C*kc^HaN&R`FXaRHbb2sXo~(Ea0e&YV?aV> z6h>3wL$#V*edEewV4^M6*a=yTw`DU#GT}(z-;1@@`aFdYJZaJDFR)r@^{vEo04p%f zBvHdanQZ>o_El@9n}JrXTzOwfp+wc9h33W>*p&(_kUH3FpbEfZ4z?P+{|;a=a|caj z1y}{GlL?O|D9`)0Jnz{G_8P$Ej)t%x*m^M>0mWj=tUgD*Bwn<}A%xbu3eV0;w*W$NpTkz=CUOcfE2`cb6e*;*z=KJpee)o!x-(1Gq7j&avU&32L{DzMO;GgdA!;}fx=t{4kS)wo6 zCi$a7N(izVg`#tE7&2mm5bbM^a4%c6A6Ru7R8{|3!Ads@kLL692;00Hzp@41^#7u3#w|CihEz}$j8&IJ@Ci6E>z`w zyxLW$jshm@!^=xk@bUs*cWnl6i?6#e6E98`;m+1v9HxuEdt^Aa4GN$lJ78HS zsUA`ho4b3eWuUXg33$4$2S88Y&DQ>SvW71I+FU$e-<<HbW;;b+u#T_$v zzf7p_j8QQh6yqJ04U4iuVUP*331%iiY(p2Zt)sSFB+8ph(7JnRz!nB*xz~e2^{MQJ z=ohTLCfi*DK0nwPLt^YPv6&A(YvPV!i8d&Rq$^!pgE*gRoJ;l3tfB2@O7}FN8*NO8 z4b|+ltU%PJn_Pnc3dA}SMV9O;U6VC`GecDvKukewpsfk3swO7d_EgztE-ibcgp;&s zai$8pltC&0D_7Z0Gl5nqaT(A$fX#U{=j$ra1YiMIXF8Zo9gLI$mv_|6OkC(lj%H%f zWfteHL7egl#BE|KBddWcx~Kc1f4kZk*DDe8MzzNBiJAf*Tr(Ko?3{p$UrwX@yb$-# zuf#7`Hsay;JMi@GSA_QvUj6(v-u%kg_m7i=_e_}!ttD;zbKUo!0ORcyrM~jGM1bb= z7j&hsTv&&K9tmjF&{eJdO6;3j#|@3cypUGYlU{{8>iRmWnpo21-kulS(ePts1<7Fz zwbEuNoB39x3uceesUDb>Q5CBtw>PIcHf4KbS8jD2?H7ux;~V1syk_`qX$GEe?1c1o#+L_aqhfG~?WJleBpE zTgJKH(rvyv74NUoUA{S&a4rFEF9RN|!Fz)Ih5(m*zli4+D|>fiHeOzyir3es;>E>@ zcy@Lio}VtlpH$>aOIu^th%jvI>yIsk)v>Ez2xhl1!KPdvd^e{hUhN#n!AHDNo(ke# zk+S=VE;`-tR&>!bd}Vf;=4FI*@A4`Zj%a} z!3{8zq6nrc@k12=kO?gUtcv0BwVstUlm1H5wpJSBIoTQf4PI-3wG7&S0&FS9a~171 zh((13PBZ6!1=y4TiwVG{G`%`>O+K|qS}Z?w145h}V1Tp`6SR!5K!oHYcuOo9*B%>Z6X3N&={`@w)dRC|>(nykI{$HfD<0q3jTa9N65b%CR%RO?7z8#C zcE-ZH`{CK{zW9r7^+Vc|-F@)-&``~7{)Vc1hA#BkvdOCJqPo+l!qYVM6;SUI-g_$) z!1!S;-rre)l3P4?pXYvDjkkQwUl*p~F(H2S-7LJjHIKi;-@7pfe;gi$ds}@?EjFw-Pg zDc%#Kqa4vM$P79BjxueuShgcwTG?NmORq1VzuSvy+p~srb<|V9PD*jY^mJEDZe&mO zuJ{4qF5X%pJt-z2lVh^ZJhqt|A@-&cyJAQK3#G)efug04wv$PU0yJ`!f&d0cbTvUe z!Vqa~tYYCBI(QYVS_-BleAFl}#r_ObjV*y!wk1j`rnu3%%q&%kmap|R*P7tD5MFzl z;|G8ZV9TR5mDq}~iV4gDwa%@U$5!?lzzW_nfGao~qk^jdY^Y*kV@J)5_%k^Q#ByFQ zlp~?eYV$*e^B7S@gE+nn;`FczXdP>dj!ACl(ZV0YbD}YQa8oQQZifxCauvYmznG%j zXNi49egF7w8v))+xA_p2_iF|3n}?eFjHl;;=Y;g>g%9xlJAic%3$X9_SX}4VjDF9U@8m>{OQ6{3A?P{*|rYC2gW*wG4At;|r>&K$m#%V~wX z39nQilo>?X)F5A4^BPv@R@V{38S77J>4VQZ1=0nj8=YN)ZnPf`_l?5k;uQQiFB8w` zdOlm5LxA&8vZo&vji7GG!}Dzfcvn9>*;YV^3-Q~=T>P}QD}Gv^gFm75)Iz_1oiex;I}QnFPrfC&Pu$!w;Jz%Si|E=yu3vv z{$V-r`#QY5xd4yoUO&G&6D2bF#ev|>xyktJz;GO!(G+*q_rUS#EpTvb3XV=}j2lZj z^H1dB?Y`j}yy6*%%YS49O1|pHf1wq=9#vaaACdZBl1{p*3uEi%W|-MP4-*;pepXuw z)Dt>_Sy)9o=JOuv!kaLcfEE&M#a>6kYr6YFMl!+|x!6u>u*%)`C2tMKd9&G`Ms4m`U31>NU^c>T+f4*<)Ok*xR1 zl96mw_#3>R04%_Yy%&JrT~iQ?3QO!ODlG2va1J6-99-&zn5y;PXn(rV!H!7qcS3}R z4Qe@C!QaLl{&p7dk=7d)rmFX=vw@UFks8T#qh*nwaZ5%`d*sEsU|5~MSUzLMD)^^8}&Asq=s{q^^ zKds5ZPpiA)7lQnRYW(gn#oUx*1B`j)eglSaZ(Tqz6agZMqAp@QC|}y4dAjESXK5cXZWF9t!iN^*;0(Pk7O7$hU0S=aQo26x>F{+pnV2NSM zM$#Y)si~xgwxRaunc%Jf{;V0@=(heU7G6e>m$ml8p1is^J}3p(Cuicv1>JCKdTZRB z(++>G$ittjdgA`VZ2Yjei+cQIQ(v|2`zw|Dx3zhAw6%|>bSGruif;4C@nw(cMiVl% zRCHtE$BC=ERMmTHQF5P<-dm@jeRgXVUfx|xwcUw#KW)YUtw2)0)9qXLh>H+!by>f!kx02T%Q>((9x@T;E>QD>y@dkU-?lP*EGodw$Ut>6kYebP#~Q>DSei{Y541#f zge``q`(RRLASSn}#*x1&=C!PjW$i++yH|ajDQbgjQ?hVrLR*|2osLt(8{yK}7PvXB z1HPZ$p5Ab4T%XW2yqy+cPgUtkY8(#fDc<-#k^Ltd{A2;Fs&s*{S$Bhbf zQR~;=FK2wb6z^^>)!ys7#T+0wFkIn#U*Yr1e12{oUVS|kPY#S%YJ78bE?w+={2}R{ z$M}DqnWWt6R|kjUDMN%iOFQDiq((S0G@1ihAU1dM!txf@m{Z?CmBkf^hcMPGr03Sn zPIIHhdhtGz_*iUmbY(TkHKTzYR<-uW(&oNcOnB!Jwk7TTu&Q${%xmR|;jsn^-~myx zGmP&WtBDXC z7?I?G{5n?13#age*rL9Jq+RMG(n_jZ8miRF&VmmU#q+cgc%@CPDclII3&C}A5P%H{qoGn?F@aa?Qx{syQQ&pZl0CEx zEq#@pWzENyJeoU|tqnVAdD4>5p_zd)1#kp^5bmgf94>Ki8NB}lI3vLYUFbspSAf6W z%ounl9+m<8{MW-D75F~@tW#fsR!jhX^X3v>^0m*NoxuyfUSi;W{hcT{I09`O|OyJ`wO{!sTivTQdUk4j=>S04xlv0j){JGe&RyDAueK0n6 z4ae^MSezWy9QPLH;mO8fc)oo!o@}AwZ5g8M`A)`{`(zE65T9V&dy;l`HeM3!C&wn@ z`8P9BdX10~M*Oge3c3OB?{CB#J}Ai%8^8pziP^mVH8H6AZ5!n@OR@#^?ArOFS#8ck&$j$gkRj9+&T!cRN;s+jrv zm0fXrWe%>*Z-Xl{TH)&S=D0dB9p}a*VsFo21@ZWBDsT;|WsnB4K%4KT0oi{;;0Jb%RJKbhC zdt(K#y`%95fR*|>N-V6e>{A666M%J_9hC8B{}E!Re*hfAiycj?ON-*gj}&ODXfd#u z4&X-NmC!2Q2|10cVNjR~e-3(ei$kw2QOHR4L3*?cTGsPMt5{z&tK-AC z-3#$G+!5+wiyBUrjDby6km& z83S(wsId5HD_!V~3Sj)So$>H4l>V|qDe|j(@)`k{_9K7xpq4thDTK8cf1u(CB#j+2@?Vs3jc z%i@r=6!NzA!G?2Gi0#Fg{gwHkqiR^M!RPsU~b4tD2x~)$(ZJSq|+(9FXE+ zre>i9UQuD40;?Fuf0I~3AmpEEXzx>My!MPgE?uY%as<0Rhs1|5dRCn@q{$)6N{C}D}L}OZ!HGa z0i43otVNsyx};KpJ1_?BlZN@DGXH0QZ|+h6KmGY2Ui@|hFJwau}D+uEydy1t=cU6nSlGp=G=q8b`VyDbNnlUgit~DxtPpC4^S7hQFg3e66$* zQ?=HVG`X_b$0RosWQRGRAifIvCVFBN#%T^H45_4SaFnEAR>#6NVOW|~7wftuV0&Q-_6=@V876e6w~?sa>DdW-YG!(VBV$aD z*Ta}lah>%rHBJww1~t=#jPOde%VF_GC`vNNl;$p~c;47lYX$k#CaxIMpc0B3*kEj;1Ao^N{ld-E zK4Z!FYR2y?Rg6$&_}L)c(-H~JrfTDV6xCC*va}djPsPBhKQfQC32ZGYub9rAR?XF9 zeOT6sE9t3DmtHoq^GESaDb@1u{R>pI(;o(#J6y$$GESnhwVp&2` z05{}^OpxS_PXLY*a0OzzKrtLm8w403Ez}$tu@30m$OrxDPEPt4fM?*^k;S-wVZEBb z5`Z7w*^6ht9Af-?l-Sd`z$HAse0oNuQa=3k z2ri#ljX4vtkl!v0$zhcd?PrRJD$?%JKy@BeUFcm1ubr{VQiwU@*N^I9B&a!I5z}I5{;J7w7fHjirNechxXucUBIii#mv5X&?NsqX=(K zF-E=2*!SX6ygvUq<6)}g-HjTE3dk+WO)eGHm2*^P1?G$fCqlX8a(p5hJ`0 z)~VTSnH0kv0lDmWn+knvH3y5&@rEw->vM$o+#I|*J5L>7pP8*d#sz{*g?@K(DqbEK zgXeoj;LYCAc)D#c?l10)gT2ErH(3u8sld|-@(kL|a zP>tInYFU1$>mfYwG@pX6>qF&ohLSS!q^l$y2n;1YG|okWjSA*RAxLk zBF03m6Az9wQQ2OyuUN{}<^-4_)6*DDT=mh|#TbcnY12H+5od3TXdBrNuBX<5#X@C= zl8L6i;!cN|h+8cMAM_C-u`tg`pJJ8xVO|C>!!K{!vYkg_j@sr01s@?369C0Tk;NPt z4?7cdS35(MZYjVEv}*6LV_5-!Pe9hi#+o~=ZDf#%E#2~xUJ`FoF#)+AEruUFx{3y| z0326c9}NPH8CTMUjp@?QXc{q)qo0IZvZ zmSrNf-dpk!;1^U~Wv|Ywa|-a67t~~zRP*@t)^4nxU4X2{{)i8#hzJkD%MmM>A27hl z7=DD;(^^ZR^r)a^&d3)a75$@lzE$<|=v&VX!yAe6R>81DcdD-?+VGk*<@+?KsFh+B zlTxfwnpO6qiX{@{rtYRlsQ63Hj!A~BMnP)ld-Z_3U&-{ zi_D#y4LT% zU#pG~_5Eq52Ja7h_Cyb zHWt_YrnuNE71Ym8&BDXO6Y%`lRJ=Yh1#eGG#f!t^@Qm=jJy4AIhsNR+-E4`6B^I7Q zXvfyk;1ySTEI}L}p@&ti-5D>3VShn{0(otwmr~#@(r>D3b!^P?!jcwNn44N2Qxgp^ zjtV|HP9Gy<4b@C^ubNsH#5|dGuC5)Wor|0=pvo8Wc>#akjrVF^H9hq7*HgWlr5a~P zUlY2}6$r2a8aW!N(n;}*SEFr>2yuC}gcNRRpiBp_roI}$Vxh)b)fTm%*i-{prCk!> zY67qw?<>M5<#Pl+1)#GIVm;W=-4zo>7G-vE&`jcGonj003f?k+#pIlVxGX-FT}wKM zWsO)8JiGQSxeox#Othp{%2vcn$CqGMQ9HD5 z;E6c@@>F0$)TA1QI2$3zMKZAT6u@3`u?byOUicdP;zD`8o|6<0H^;zuR|W5&ct`Y$ zu|ce`*>)nAME> zk%{)KVp=B%pN zog08{U8<^GUyGWWGak0Uw3PCgkZ6W63FR;>#t1`W&CrMMBi4=J76fToTe4<6SavxD zYS~;l-etMev^E4tdO3^d)Qqq+ve9}EOXU?Q03ZO@wKh=zO94DFwMV#2fdyC%UajG{ znxQt)l0BplfaFX66W}VJ!1pP@Iwcl(HGpNJOW@T2c6Acd;PoKPQa|eh#PU84+AOs! zF=-I%tF45V&L%K-HGy6;0@{=pCawBMh~veju14jp!JC!wZ&FA(G^t}rfE_;oJh4}! z|6_pPJUa0|0<6n*{-+E5{LMw>LQDLs;^5a8@%Yc$#9uUcL3aUg*X2R1fN3eV*hce>C<&$D_3KhDfC*`GJ#l&U}L2F z6INdXG^lETr0V665>P=|YE26?4Yx%`v?H<-z0kK!7{(W*D1g^6z7rSv%oo$tB-Y(? zs|fH`Z3655S84+5^`FNSyl)?$#Ji`bffr}-?xk#E&>(#K>;m3CzeunzQH2Tb8(IIo ziZ`FwYo2={HL)I@#_zu$Q;pCb|9KMMUfhavCsyF%*GqBb@DgmC*OzX-k6OBcX@L0c}1q&4y}P<60IC3;|FeMj^RH9&5N0XlLpYsS|~nc2*0W=JQ@ z4ftgx$PFP-I%;K2rNDTqhrX&&EEdO~MG5ScwR%m_6*TBYv75V_qqVOUnpZJLdw*N> ziSxtYq#Brz5sHPm@z^}58A0xXYfFaU-nwGk-&oABcRZdRWGs4YK3<=kPbHLf-BsFT z)J<6)V$6JPKGl+d-4d|Zqx9AW+9tZ$bh+=z($YpM?>fAdq>cwGRI*1YkCI=Fp9EO> zJ-X6__3gJ*XBxgEz&8l>w|vbtQDfS-eBI@xYStQOX5r28DR^~oEdJQgAGa3dVE^C- zn8o;XXl=T`Va7BQ^r0a1r#cpen-Jn4oEVhEm^lhx^r)#eh^)!9!?KKWSl-?eYdgDO zWhW<9i)<7D9vLSSS+*)mYhoi;OiXdcuxK*{Z|@L+)d&M>8=w!t9T{nX(RFPZKg-4k zNeJ;)jnX<*Gv$CwVUmHEF0>9{sluXzvQ8zs(B={YYp$})LW?PgO||7EnW5GJEVV6j)PFDrQ8mRwWz>eVGYhGZ^FcVV;G*$6>OjUi=qqR2O;ppmS2=_Ep087G(#QDwndHY1$VqB`H zDrP4YBcv;U^z4xmv(l8SFj8wsofqzm9J<8WbcrQ3Pnv_@o{){W*SPbF4k1@_9jBPnaaEs zC45|RdjsB3vESTXp|Z5z-eovlWR#K1Bri1HG8Ww+=?-{kSTZ78|05~b(q!k=G` z(sLXrE-Y3Mzdt=2uZ~Q?AKN(KEYHU`<2jgj4aaA3))>r?Ye1k8`tlm}r#CZ{kjajv z-E^e|;G=_Madcn=w&(g_Svzwq$}mzz@)ozT#N3wF7~jwYgBU{>5>&~L7H|b%0eN)1 z6^2EYN57id*28`^4EXoTp?81*x^TeF@sVmEmKYfAie7abRjDKybYloy9fBwAR|QJd zozXzI4lJ;0=RO5kva`fOXewt~+-Eb*g_aDyK)$a(J+5jt8o*METp$*Jb#bf$n5HRh z8NfXHP=S5aV|@kfKTQA@6M%JPd30rX1YpVVQWZHmMpiH{jUH^I^}-?SfJG@ zu$ZXx3&zVjC4TYvB7XnzYh3$gE%t95izO4gs!6s+R949-i>_uwGeLcRjz}L9)p%Lb zN8^3<(Jssi0~ptTmgJ7XF&4)@I6V+WO{=Tr zCh2=y)VMlEH1xr+1YZn{se*1n)@b8lq>8Ua6XbMTLo~KFR1L%uXi1h*S&`@LI0!hK zp_RKKU3PPHVJzM)NR~`$U~O(=d^xlO4i#nNf=qg?8irrCkHzBylkxr>LB9Gq-khCD z7keW9I5bKfC0_ji;;oHJg#})j@Dg{qmo1C@ottGQGt*Sy*JqdD z*~vx9-hQ(buTOoBm&fVOADctvorl-QId~kLg5P(K!GpDZadS~OT$tVoUkpgbltymo z#cP=Btrd;$PrwQY)o`liyv8orlofyj17h*@kQnUD_s7~SSGvyDSk&4YpSN+s!q)DX zmS(LsGz^I_QlJj0V}{QX92gtB&}FtmQM9$%Bb?`}^=wj=JF07)CR!7sHuSzSJ*72^ z0a{csQVJ}dk$@X2R$hy91z>TTMOl@?@^KBxgBG|=$^bT#5Jp^7_6{Wfce60z5=@oeMvG>0LSf|Lk_xanhk}@sbQm;#_4YH|1 z?IWzwy0$r*2Ad;|aiM^lTGI-RLMkhOn@8Ax1h}p22~NhG5v~3sz|Vg8@_z-e>`GGG z3kmRh0{s5fl@AcVe}7$p`;2k&Q!1}0@N20~$;S^LoYLZsV`^=V2UOYc4!!4sQ`}lF-8QxWZCnat4csxcSs>=1#uZNsoIGYU(y>*4c`^)ank z1O`QVs?PTLfiCD=)gI|q2C5%XDpfd*kCS*qHL_;7B1ao3nW~RAZif7wN^}t&F*!8^ z8+tUxxvBZMy2z-;C6lUpUDkLR z8{aH*r|+!QCak{y9Pe()Ug9O%W(mSus?sAATa5awvH?v>>zIj2Eea%LHxAO9TB zk1oRFL$mScSJUz5-l_O~&qVyVt(al+5ZqYQ6IbVSRsioGmX2A?t1%umP~~yD@jCY; zWLVNt2%kA3ZN4k7+`FIAtANE_(W@bR^JL^;;b+< zq9O){n$TS?PcuQkU;_+f$k(6hoX_{pqDPfWZ%PJ@d_M!Fz_NtYmZ41>FC(<{WGLut zfH;OVQG_naLSNNE3oECuK$Ub#9l&C8P6x0kuiEHfZlFxkZdL3n@LFgwu%EcmpTMhA z&;JatfNKJ8XX6h5OBaWK!mDe?BCfcUqLnI^0&it!F)deGuX}X{Gk(S{Q8ub1OHo5f!Lz{NXhRH* zGf=HEWzDfT$sV&Zs$)r~FuI4aSep}vO+6E^dr&IA8kUN^gOjl-FBThf6R-*qYxIE3*=@C^G>Qlfy6~E|6f-i?K6CbB??zGReh}zAhCYitcrN##kx5DN-%8 zqHV3|TJ^6frDG$pvv+fx9@hom&M(C6mHqJRmZ5m`Wf7ho9!pgmk9TLL0vBgdl?mf@ z0(@hQQc=7O#%Yz}u5^ z8F$abue&DVr)>mvt7Z>2PsFXYMYz6VIIb-lic3oeFZzToqklGd)7G4R%=@p3$uBic>=cxhQjn;)G#R5cur3idmZ&?F2PyjcuCBz&s zW2_7bv6g)$kjg|>P{p!Y=<;O~RXV8oT(b3~^iFUk&21KN{VJEi%Yj$A5K1F6X{GW{ zfVBxN4PaO2e*i4-I+v-hz^gMC%>@_J0W94dJ^<|e_bQfpJrg~UTi+F(f51?pr*kB8v-M@``cslZ$xu=V?6u;UMGi#aLL>9{J6K(Iv$f-5Up^ zNw_^4)UZH&HB-j)711)>N)1+darPL)xazZHdn&C3MkE-csJ;;tS;aH)w>zi^iIQ?9tl|4HHw2=1P8mCC~oM<&*_LBf##}J zN_zp=M?4EXbP&L*XtT&;sIW7edSZTtFD4|}s6>o>e{FMr?_eXEDGGxa+XkDdI?)4a znPWs98wK&O+T}1XSOd7PpT2UVvt6`e@ENjnL~CrPWnf7HN3xR<>NDnyvoS_=Wn6_?@mHnqIGDPvkXlLzg&bw5w~23|^$B zp_==gLiJ4yq$^sJv2ZvQB*q>c6FolwEDHRu052!Nn-##%f86(90mgp;Sm1p}U46@BciB>t8Qb0OzLHKxRF93=OLyt(G<2X>%m{l~dtCMyM4! zMpj1OL|2S&;z79VRXt+?cS>3X%xT4#tgRCkw06dlww_qtt}5ZIfz8=L*wQ&zEtyD4 zpkx*8%n7B{!Mb)ev9y^V7Bun0)FfX_PYuNKj`5h$Gy;cA7~L=sL+kmYZ-f_mg?pf&P8AeH zd7)2~C(qfTuy#cbOlIg=s}gek&5&KaJlc60qnW*yR3Ry-9X${VmTZfx|Nv}3FulyDq9+$ z6J2MShiK|bZ^vGneNM15QUJ$Vo2V=-SqIjta_FfIEwUCIEG{ykm3q-KVI_|R;J`}y zgjZj|D{H^iMR^(W1Q1BE>Q02%K_;g3KLT84TBem|A7-LOiJgrItAT1eDkjkW)7*I8 z#Z^<^f5L0#EY&Lsa6yU}@*27;fHUbvw~VksdZ@KhUx^(PIFbs$jl!(aG}?{;S3zOh zkTQUq|33#<+~<GwK zPYI}m6y98E{-$Ud!ka9j5{9N$!Q^JN#`cU`Z85i%17le`tjKo8x~?8r-O&weJ9(>~ zL7TJuR8H@f_Fh=q${wqlTc}QfpQqblL5iK)bu(UOOY7NUXqY)~HbWFL3@NN>hrD2W zb@TU+@k4&7H}XQg&@HOx%( z#?GGgaA{H|ZZFKiFRP{H$^flMfK<~sJ&h1Ej+G4xmt+f}>^_q1gFJVYa9^B?_h-i9 z-Ko#;;>alcyrqzAcf_HQ4RK*|2H!ImcNXU3{DjWf-XjH*8v0ZD?9j>ClrWW3?!QdN zHL*28D_3)5dRn2Kw-wrZ@;Ps?4Bk|kgr*hU#dJa|VM0Ux|M8|;G4^J>xtmh4+VXWB zJ@ploV!4c$dj^{zFUSO)WcHr`w{X%bR?2&3c^jw(Xgz8eqfbbAm1!jzR|2wBs_g2j z0o+}b-c4(v+Qvox2YNV_WEsyuEggyL9AjguGO%K8%OTQI+Yc=8{yTtWwmGok$Cy_t zS61a~s#@ung)QkdxNe8 zi@+;g9P|p3e9(h#aZbF)2Y_YCNa9`rS6OYUak#Zo;7&=NC}_=*xn~2q(9IYF=V0Gj zj@o-B;NpSlxOL`p{B~ml0p3X$`U^b&?Eu}|!yi&91>(}@XH*i004(*f6ukci;P-E? zDi>NH7H|b%0ar|7W2MYg;Fpija*+5&0epGiJQbg}r}}oO?}O}kAA)O%#H!_yLIrNX zo2FTS0SX#8VtAT6rnU5@0$Wfit*|Vs3f6V^!lqnr?8vW%T|KK|cb*@%W_w~&rW>KP z#?rCW}>(#JH$^cyorPWFb%6G-O&k--0}9*;46UXTA% zvSm3maWF#*2V+%ey(QJ9u}vAY#bG6F)3SeoU z;!l@6(9I0h9ZeLxGOH}J%d$+Q+t?r`n-z4XQ(=jR-CT@l9|6{xhxa^` z31FQ9>oT!Qp2%*bj{xK8rH@8#>?rhB9SL{!^ufOT>e$$UicGh9eTFSIw5Y7${k)+brbg(YC|GOzDK;`(9}^So zF{gPAEN&l#MOg`$-60<1TSj7FgCGn{s)g)Y9_Uv;1cQ?!kXg+c^-WBWR>?vsaHh2l z+E`RTdkZtPDW{J#s&PlgWxYLQ%bq1xwydF&A#P4WG~OQ@fmizn;NjLh{IEJ3=O?CMN6!GPZRd?`3=2*T zNyDzL(O8`3kKsWU=t3A%`S+U=*pxEQK-~PuvSS?2OAIeF>P>hvi%Fiiq?|?>Xu0D8 zujG%53D`+u1S5skoT}X3%}^<98{VwV9Y2~hFq7Yr_ev&|O{my{g$${-8f)& zC-|Ltz9Ru{&-ZI5Ak+PB$$=-`+6XChl^fZaBgxScNlsR1;B19BM>7SmE(1#@uvDyD z@uLDukvrKsC_C1~r1o=Fb8YvLnA#Xag|Fsp!uZz&zC8Lk3c$t$*iZrNYA>~+Kf?RJ z0r*pJ#aw81d@jw;M193=R-5_-ULC-B^{b$JeQ}}P&^F3Wt@oxg-c1Q7z_q2nkTsIR z83XfY&FeY*4*(A&z+-Xg;8ff`yF>x}*PR`7q4(nDABPCnQ3dZm0hVPXaec*PDM{9X zbu-YSz5=PJuWk)k6j(u9HZ%SB@i*FR^)s!$mb7o#zF+`GWXB>m*$>?aaW3PxB%cb1 z|mPN|Fj@uB!ExdC16AarnYL`(CEXkliAj+W)o-OdbM`P%lpk$Uk<8&|U; zUG^F{Ij|AF9oGssCeS7~!_SM_T00Oou60C!%hDq!0sBn z&K#`l+%$l7uCr=CT4t*8S+_V>bgk!tjxo;2h_F-fZ%U}BuC;0wmcYkS@j?J@Rt9iS zUoHowaC!=5hQ z*qmX9bxkW`eG?O`PBX*O6no5wx5n7;au^t>#bJ_6(A~d0MkEEGshg2XsjSNpUM5n) zd1KVGHbsW73tD*CGgdQaOz47vVL=!jRSSiF?&xe^5$$+WwIU4NcvJP}4Ls6MA9Laz zu%ly59Pb;COC!^8b8>Uso!J79R&_?nj=p%edm!HJC9L~K5adx>A^AgmzP~?8_7>vF zmae$JxD_rgD`Pph>PCZK$GrSn;tB+m;s3iduH@*%3=QjKd zZ7qzEX~&q@sRDtjK)2ZpZQU)=PQ}C4DlTrrxH!Yp5{-Eyiwj%Nv`l^ZIT{Gae9uI_ zXA_=pO+Y(3$%K?KdihnPq8g(UuU|V?nXNWKXD@elD(azJOZ>uPh~X zanh<@iW}Y7Tm}>a#F-i)wxS6lDjFl)+!%EjmV{O|{s6A3Mro$SyejTB)h4fG+h8?g z%|wCK{$Fz=s;>d6R;JokHh`D4A-rsj6|kO;rXS2h0c;ExDzpHs+qkc)Z~OybH@A-f zD|6RZri0g!pGz8mTb2R*p#iukugwG~?6vLzg^YVgCk3Isucg`m(a6CR2^_tncylB>SfZ(`y-K+Zl^fbj>K)r-aA-Bg z(Eb=5>WcwBHpnIbS-h#b@mNR?qBu|=%TwtUWCi2v!f0F^*$@wAw#3slUGa8vA>M51 zjrThTs#d0ArTavc2jcbaeAQH3TA%*%d28I9nu=3{BCv~}VOhEfvpMTu)xy@envlj7t%vmmXM@(uykOM?hDmT4OA5lsYF?Y-;-2z@v3A9 zPrgqTYr<+Pz?#r)7I@1m6M$vEuc)vTa936au{MI=@!hge>;7<(z~t+m_Yhn!Zh$`*wZ-$5T~xj5 z(wzfsi*tnG*&=QqRE;(9nz5Qweqo#`%XVZf9+dKkmd zVtf!k6OToNWFY@uUs`{gzv{w-NblC@x!xKU1WqYV*KUP{VJ8&R3eGC_4S=~76wz zo2W{ZF1Drw)I_!}dpMtMX9(t^zpQ_D=b(T$_|x(1Hu)}ktk@}e{2Y^L^Pb(L?06sK2 z5|?H+!0kn;_;Fct{JN|a{#=oX2XkBE`nVK)GdLPY3v1zE&me5ibj891-pITO@^}OE zd-uQITbE6G+jOVrZS6gZ9V(WjO> zCN`>#Y006O+bA5XTE<~(yM~w_?~AGQBt}vl`|?Kb&l_MA2cd-t6|p!k316iX!yrt*iqr+_+w07UY;jpFS%J|=CF zViCPUf}5wISguw89K!F$Nn~t(pT@lu*Ve^%YxvF}=nfhP?LlJsoc?k@B+=U}02W6B zLMWD&6>#gkHvsy>-+}JXLC_qeSWZ@5Y^lPU!k@*Wh?CwKfN6wxn$mliOhs2LCHvp% zM{otyeck{p>;D2+dF*R|RsL7PS_^d+zY{f*4dyT&-(fx$_F3lMH$`~Iy$0AvTa3UO z3VXt8HnT5`#4@ew{h`M)(0GehngHt#kpO#|E6b&23!@CtlIw_-_5RqkG7(327vt+g z0^k++<>GqWxw@V3?!zAh_|JQvZ9eYygX6gR^C!4`bq~J%{zF{8vKwd4tj8yxv}4bnMtpXF$6a;!a9cIHo8wTC z?h047@S-POTrx7{2eh3$5Drt`hJ#vPxJ~Pi5bYsIGgC&HiyGPpuvqD^)q51yM^D0$ z3L{+Xw8QmPF8FnWH|}o?zRVssW@MohR>3{vDMcEtDWazi@P?}-Vl5Jbg|BU5^Cr5LN>uEBWx>e2FuR1E9I6F z;6K+#N%aM-v)JbJ$wvuzO8fkHYbgHO7=(K(y>PMI2p>m|<8w1pn<(HdCkVA#d|tNr zqM@jt!v?}+5V}m>N2}5M=(HY>Zl@V&cbJYQ+v%varJ9?LPCiR-`~1k#$SY1~KA z!8W&-m?8E)bl)3<`GW_@3~VrRpkgho;bJ8YF*aH;E?S!ktIOXTjvR~y1a$Vmx8!DK zQwgqschaDKavkW2%)$cTsiPDMJoYuTqU56PvU>loUIRS(HNbM2oc}1W%9#IE56A-I z(fs`g{{H_LVBRMI_F&$1n>!dzR9{<-0WcxF2K++w*|HhT>K z^89fu z$r0w}uTujRclz%;#Nh*+(`^n_N|~#cL`y z`)FXZzc$wKyIt&|jdIJGNYqk62s3b)x(eJ?cLcHS8h=*dbAG@WGmeeK zgrR-j^g=5SR{$zHtYi)UU#aqbWxk)$Llj2=v)D6OOb_gtVI(dm{a1xm`Ll{@KPd6` zNEI=0Uu?0`TUzM8|An|G13NRzILv;V@b-m;dT%V`m)rxe9={k}ws0nM`@l;3T{svF zhmX|+TIkuxi#A7dkq=fjMPSQHs>X+vID51Um(Q-n_ZK(gryq9VX99fp?q~S*-Vxk; za13`JoRF$|gFpZD>rq_$^$0HAJ&dzA_v4H22$er2xJ8IBmn_7I#Mw9&qk%6|^l&!A92W|la5}>pM-oh_&UQEyYlTl^?QteA z03U_hVzHejay69^N4ps_bqFF@p|tkFY6DPiJ|3Uuco4o?U|$5+Uq zI*^9P`%`5vwAg_B6793-QQz$IF1j6h$$=>1diHATzS!$K4m&+YV_(2zd>l9h9|x;r zU-&$1_M3xMUK&^zXoTf~rpPgv%s9|Tu25pcOsFB;#Hjb-uBs#zPvlMmT3LAwmMRxK zWhn9tC!&!5mw0~vDFm^2xVyxm?u;I`Fisf?pDJGQ&wKnYE8n zLud&wZM`V5f!UiQp_BlN-sT>Nd*B_WD6w0iC?!HHb}dvL^?H=n7zM;)L{<;H0_O1_ zP}NoWdaC`QM3BX-BLT1&dG#g(_mlwHj*4Wh-be06DAvFddj;z=2kEkOc%SxUei52| zVMc&$XoI{gCnC&kHnOA4(NyS-m2Gj@x;hJo_SE9^v2I*EyAD?_Y{n1Y?ZCBbdvN3D z1GxUnr}**Kr}*yXL40#eWGq7Y$KH8RLp{XkkSp_~wqjiprv4+3beApT)!+bW~ z1Xb5*)1SZu5w^rr=2RrfajvCxq zl7ZWu$+*{%irY=G_;-00UaiZ+{kAAvDsZ7HFT`gtdJ2f0N2ArGFFuZ*gPWZ}c)BlF zv31K=b#eo>=f?`A5(`Csv^^GQON{wGR3yOby#`>t``g&;HUysptKo8i8BQl_<5;w& z1o$KWsrZNx?~gV>6C+2h*#u@-b1d;OMT*V@IE?HoGp6xdeUNWqf&xnuSiV1q^)`$N zkUrrydRq*K?2o8v?;uC(ebksJqtoI8wCcT$x_JtK+X&?flY!XgFphS5Dt38H!!}nn zY;~W4tzOgk8CB)F(u)}vI&9xUo9zI!GP}1p^g*ZNU@Ui0MVs{pDAiX&ipEg*^5hqb zCTI&QJ#rAN$Fo6K9)g9+gJJf;V3;bu3*EuJF}I(hu`%kSxTbB&&*AXF2slkqh2;cg z=u)}os*I3XSB=jjc3%)Rj>_8ur%+g-z9Yp^bX4_#EFd1LsL}5#O6*!Fejdx;ixn(; zrVGlETgr+O5RX&o{|4aE1X|4d72}`9qV9cIy+ox!Lq=-O6=#&yN0Sdoju&NSq-CL*&w8Yxv_NUaP* zdUYtWYQm6K8H|*&0OZs~prJb(O+2Yfn&OdN8HywV9G&KZh!|V=`YeEx`9#?3j>IDU zF$f^cF(#^rGE|Z)XgJf3c}x)sJO*VvQ7XJ9qAg?+)+A2H{&G`%v(y7WZS=>TZGpJI zlezJu2t3&njz6~g$xXd)b~@o&gEhV`GQ{!J`8XUq8>iE$jJa0$HqRbcik)%2+=n20 z;Hz{;oXd8ny*9&Xs2jOaa9KLIe#F>0P zjw&vc{HrWy9EmZ;i3Hm6P;KlF*T$(d3tTL5#W#gc*c&+?Ydpr|aH=70EsMnSPl^a` zgPgb^>vT1qAEKo`UV_Iv;_+4KLhO!E#g4#{*cmhmyMvW*EM6T~OD*wyRRnI=yWxDc z5e|jTpjDra17UNpGhi0>MJ>eTx)}Viu@+xe#v|W&8Ww#p5Ft~Qr2_k^jfK}FRm>4U z526?h>krp)L*79g7dK_WEQs46>RWP!8-3TSmQks-Cje`>D~_=&V#VhV;q*aOhkj_1fe>W0ap>$d9VUtaqIzDEJLf9 z_bVnpD2874tdBKbxgW-k>Ca5u7o)}~*1!6H0^Fm(0$!oI2K+*GMe)nkW@!;z%}EM? z7pV7B^hE28gtNs21iEV?E!rA&#Q|tb=cW*(%ho%6|iFc3U#CT9c5`oPd=27{rx_BBIbAkwpQBEek??X&@3_ zA5*KtQN+g=w8SB~JOt7CK8P*$M{v3m{Hb_ez6;>Y4CQGyUQT34ahioh^KnQqRY4$~ zhC4rxJ3mjl#Sp2!^}cG%zs#5!b8)oJ7FSmI;Krr^3GjoRVR*DB62B8(0q`FieQ{@* zJHBrsz@^4Gn{UjVYK$LBX}gQ9aVgJ|3TcIF#jg0F#2r5rdlF_p{8-^fb@Rg4Ij;CR z&l49){BWf@1V1)L;U*)&{WY1m*%i;c9F3pbVsX7O0+&jBa6Zo+SBgCO++KVRAADcV z9NrL&+pXdFzQ&J@2vt6LAvXEVz$bJ8KQ4{LlLN(y)gmvpEB0GDQ!6(s!?6VTr}zg&NDDn96+>i^H=7 z#KK04T^afbh?V~@3oW5#=}cCXP~Lg0xxB5(lU;)+{XEelJ$DF9^hd+VW*V)kHj<-l zP+k~-)`oa=E>6el)umX!xdxlI)nncEI;`7Tj}_bMWG&fVi;m6Js995t;w4$gY)(de zZ46?nA`xB@hKR~=L{&$zA`wv*hLEB_SrKKSh~dw%6`_bO4VA}?#&}uDHBm^ci9{?F z+Aqr$o{{Eo^PbQ5HwB*NlMrLCj!b7wB$%oq!&DiObNa!b4U-Q)SGw6CRC-K6Tktfj zh@6g{8S`D!+JA?7p)<9{aZ+Cm(TB{>2R$1bk zVl$jc*XD_?g9|yvxK?6|8R?6SEMd0Pe0)YQReAI@&H`if*$|4l$ zPC&|RWq6Jm1Y4?-7Zulk@<>^3W8Oxti3Zlky5oFHHa3Kr$!otUJeLxA0_HQybQ=!B zNABbCq03PT^3Wx>3YOhOxJV~j)RAE&@EEj`U`5(Z2 z1-zqQt8hRDXhc;+NZ?bOsn%_&NN-I+1{;p3+6cI3IKnpA z09MYkV6ku<9GQEAO(r7AVlv|Ol#r@FTrOBA*1QVk=gc-8j4JoB=nP@T3S-twpN}&& z_V}*T6F)8U#O<|<@NjDgo_!dJSNjOY&Jg^y$sZ5b`QyPV!q@4JuglGFG<6ORhSS1^ zO~9GNx%e)3A+8o!;99-~zRxkor3_>ILYVK>F2enKAKa>N!}T&J+^liO-KItOt&MiQ zD-<_c0`YyV4}Pqr%D05!kIoqUzBn59n#1s*BMOhY6YwiRy4e_nZ_B;#MYatNrn2B_0DIj(y{7NkUDke5mq$L}?^`TVH|wj1V2x<4~#93rRd_qX$ui z>HKy&48`FfWgH7t!pX=9_%ePfzDk*eue0XjVxBg>E!M@QawA-*GQy==6I^OE$Ax+$ z950@Oz3CIMBVLt_!$hnMn1)8D339T7-EgtszUYu5u%iYmAm(i`8_Ados`sv9eQ1-x z;<5eZEGaQtN6^**J}!I1EEn2Vs!% z0KB85Sl(5x)vVN8ZsaXC_Eu3+Tuzd`&pj5JpLxtXw9uj#n-EVLq1e?ytXVFA7By~^ zu+)A3PZb^`Hb)aXGW0@!es_IYy?Rtw06c#t0p^K4mo=Lw_jJNLc|4zdEMJ6nXTIiO z80f3O%3?a)-E|NiZjIz*cjV>jKang6t$$Ih+r3Y z<)NT68=37{NFsc(^$7^Cjg=Kq7l(wlG((h{*bQ1Xo5Pfa)7sO?Ydf zCENn|WP+R4k&YBXERG(`z-E!A&}A-PKx8+i`M8k00M~NOa5axWXX@ko zTtZ!GhnrNxZ*ute{P|?25l&^9;G29}@j^RX$g{@9Tx%+}6YaRGgjs-nuE+_WB^cvC zoIbPkLhOjt!j9+#I9%w6`@5);=LpzW?TY=3#9Ux8$zN<|`eh>>L^95n&@tp0;pZk- zJm~boohD}qFxH0wJCpJEVlO;y_Q3CAj5QmfFQPPYCEFU8bDi*OLpXl#OvN8dGVy$4 z4X)NDpxJT?(q;`suC^-DG)EzB>OflA+1M0efx|i8I8zylQ$;@bINAg|{WY<{d91ut z)iSpiRvPxg2D4r`7BUj2qQ~HT$`oA5(WHyf!SyObIuyErR%^NvOZ?nv%m0fbep&8_ z?>fwx!{^~(t{V1bOvYw@@2f**qTXQ~Tt+HpKv@t-Yg%Wk(R7W{J`Yl4UxGW2xmV{c zvD9y0xQ~BNu8y*3!XS7~8V29ls_@mB22cH2a9ubT?zZ}{w$z28>0Hb=)_|swI%XP9 z$0Y5E7^9(#(KCiifaR7aqkGG3M8&Rz%B;~;>CsgCk%V^SKt+uns8C^Hqs5*|0$>63 z2SXJ0T9x;Y8$lsfh>3GSN~SOJio;P{nShe2M3mGCXj4(xkcOhhbO~-jYdW%< zGmz1ghO7?4%Svy{MnY3E!fO%`QXPlTns|iQB@*5g1oLDFtcaG!SpI!PeG>f3B3WSw zu8NW{3#j8;Q<2n>fwUz#NM4+Y(7I@t#aTl;KnJsICSj)T2rQU46gKmfs_^wzCtRv9!-afpoXgb2>BQ+0;uBG7tf@E|L6F0#C~=xN7q5YD z5;gHn$~;_1qq;DAUdma(tZ9IA*@igH`ZCj)w+S&}msz^h5jUzlaks$_H)_0bmAAjh zVup>^XSvv^zV4mV?C;w($dCH^?k2A^j+<7BQI&g8o>W_aLSzJu(o|2#qy zdpwn~+i?&MdMe=*oy+;8$@n&X3cjN){-IEl5kwEawpg&Cu*X9}`};B{Mi)2STkS#@ z;YEjJgEQ50ak5Gi2ePMQN8(HgaIxtSI8oUQ`zZjn82T10M-74D(6^!gUVk}YLyOi~ zv$sNl?M4rP3zallYoZ*p?WaBlVfxb%YC0SK7W3g_ZvanM6F9q?!`{;ZHeS{+E(>z#<2W#qPwkL;n#f_Cd49 zM0EK~mH;2hFu%|++350zxJ2lpCk47 z=Zj{!RjJqx<%Qfqp+)YX@L*j$&R5t|=?(Bhl|2>Mhe~IS`z>yu4FR?!;Q6v3{MqP+ zhYft~YB&5+M5mBvhZ_Y1sXmlZAOx55-Epnl7e7@7<9MPK-`67i)D(#u%~5R7{BWMR z{X(e+zAbgbcNOmVxzQIl`B|>>wXYTO^QG(HeBvy8o2-E!GUwuQ=5$=ipNSu7i*Hsh zz}*%T{Ml`fCxrINMh`q%?}kU4_7L1qY#bwON;50Y|1ky#vryviixQEdzA z(^1&0P}+RLmQD3d#mmC}}lME+eAEp}vG5~A4v#5AW# zfCaDt)v@vz(VQwl_N8SGW>$`APeZJ*%**nS!AzapolVFS;ZYm_6Dq(&S9MIVnT!dh zs+h{Wt1)*d4CV}hr{P$nIB1~UO9#uNEwDIX0V?b!pulVxQgwTy$#XKgn4P2or|aWl zl_P#?_xumQR9|Lb?2Ue1v9F6D?@hvsowU=N!|~TjUp!pm#ysnU+f8=(xz-v#m6=ju z^>8j#DDHfm&(M~t`)#%ki^qHcuP&~X7~*?o=^vR%f39=FFZIsyQqH|*FZ{VU0RJov zqs5KDk5w*Gp+AY4i@lMv3Gf^$Fx4zU8{ZI)N4xX!@23^`=TMb|_Q|P63G&k~8u0u` zEuQgrzpajwjnugUQwi|xW>z&E(w(Cd#~CRcS{{`GuH~=rJCS+ zjvXG>`QvV-53Q~hKZ`xCmwMuSwjHjPd*Zg}kYoK+y@;QKIlhUHZSlpgtv*y!FS-a< zW@&qwvF}tcua_FJanQxj#o7c{3)ic)ajVe~_c|=_XoWpqP-(F%fZu;0p6~F%KU?{I zY;uhk9m6k_Rav94;w(L;}v&z1CvVe-F%Jlvd9?V(~k9iAt$wKcwFBa*|MC0q`4$?R~W17OW4SD02iS zIwCsV3rTsw$RM;?r3uI^Nkn#K8uIG1kl&n*9DgmkSYu6YQ#BW312i#JV&G zwD`|Qnd5k*8oh-^-x*jMu8Fl_>i9Ty0nSv|;A*RzoNo8mhCsaBPF32?`mm>7!+anK zIGDmroPt*$#qn{Gc)leB|E>?l({(|3x+VyZmiZ7uS1O7vZLp23pLp{ASY?hM$`|50 zp1|Lg8RLfv6I`oe#%plE{dT_Y(f~YJ#^+fPihq}fP^ zwHha?q6^Nkk-1!CDjSSv%lLYWgK)pb4L?(Pf6SznPhv)o(89NghPYGgj9bj(7gNn} zIm-$^m$~7GVn;d!LfYt$+qK^KrOp$NmW1OK)&AwWSUg`7L3r6P)!E=ywI#E^32s*q z-ZFhcOn^&uaI==rP1Sv{#2SxQI^o#{PX)YtLu5VQ<&Q^OsKA@(3^p&40AJ%{zil;P z7B;}4Tm!5MoG!Ooad=PBt86i-4~*aH2eZNNK<_ODz;Z%AEvybLtT|OtEUFM`FcpQ1 zOp)xQjad762y>W^MOJg+Oci!3l;uHL1v}$>fnr z%F3xDsLgrEYcD{~;(X+E<|3Pw)0M|k+?U;vCt;PhJM-UwD@rJEDiu44c{iT9S9D~F zV_ZX$w8lbZBU_Rsu%ZHMBB{s`h-yuu^5ziYOe8JMLfqms1-!}jnCUwoBdsUm9TOG2 z$5JvKgYkx=FjjLQzbxh*jX`n|g5*VW(U#_f)!DA-Ot3&h=zQcjsGv1`K32r(VpW6& zcBjtA(Gqi9XmrH&CGNPl#s`nL1mfxTV7%B7h!?v932T@l3m+uFhf;Y=#Ve}us}HHl zyJ)eg$k<7B-j#$`TVwI>ns8ddfH(C!Rqoft9=O@!j2|1QV2uv6o{sp1Ki} z@b~gS+F3Rf8)NWtWd#1}^dlU!qg2f^8G4GDQ1P1hG;t0NB+SF{yoLCYkHKdpz|l&) zJXWLFl>6%rJUL#6$A_!%cz-GW*q)6WOTzFYRZ%PgaIMxEH))l>EhLPz?ZU4A)5S)p zD-?e<_z+Y=l3^~vy%42^^RYU(n!XTMvrO@AiUF?hc)idD-!ZTM(d>^uJA&o$J{yR~ zOB4;zW7_e*mip0|IOATsGw$>FYl|&@YqY@KdNWzK8W!SiyCv=|vBRG$UGaE>JDzRx z#mf(a7*~Sva!(MRQ-%N7>@M@Ks5`XQH<*hrw_4zAjVX??0TOFWi`^UChrM1j;4Q_q zU8A=KVt(Jga#*FX(PAlCu@907Gp3*BIK*3OqAbi7sf!E|={z3+c3N<@)PTM9JgToT zoP8DQ>k;b-zw|(aG7+gwLbw#?yg0X$8q*M65l3|nM@Uu({F8m*73BukAO~2v z8bjZ59_H##g@(=~OrNKMans+2(u4tWsHF0L#z2o%?f+&FM^6v*i2ndA#zhZR=}Rl! z8v}TM9|3S`UCiritBJ^@h0PWgxiuf9twprS#mH;m8*L=CEk(#%%%7K*AZtkhQakfx zXHxp&TnVdyHg9PW3YL|~%I_ABEt2;Oh|^j#kW95rY)F1n@r_A{Yo#K$2^CJF0w=KI zB)~znQBrNwSLRcJGx(Tf_}7HPJjDST{yI=`n2N!sA7FrqGTt*&g|fw1sG5w%*!e@C zs@?}vX7+{F?0#_N?=ypR(2!(>u1p6k$*@MbzXm!JO|Ux66f0siu_bLD4ip*T%R08~ z9geuU(jE8Ld*jb7-uP#mAD-83b5+Z->CV&eUSm7(}`xj*i$3&tQ?ua9dK{4J$@sM57r2Hy=awvskp)XPN}{hhw&JU=d6cR z;K$qj@Yhy<{Ko&wtyS*0(q_&0U?BnC5Uh@3gZJS#MzJrjP+>95Q?K6u%<0t!>b(TO zeV{)~Odaekw-@!&P(ifCT$F`7Aj#VZVJ-{c<3NC|=fa6r*xkn*uEBP2i*SWck}pE@ zA`oAZNSB!@!7c18L|#{+Jl6#eqOT{hE=4{jx-c4H1UWd_A3iaj5@c&H6PP&bLDyOn z8is0^IEPtzCatyFK#ZHfj66;ZpA_Sw6;KO!MGv$9SZrwa24F%i_6L58_xBe77ZJkJ zWd$hWo5-Ub&KK}@m7%h$2E`qf$f~DxZD2JQAbW8U(w8uYQhgKJvt)_RDUol}I&$O( z$?q;i;qqcx1jIE_lIs1&5MrXaSNADEdoydhRrRC9vjc1t{xmorDN zp>6I=MMz@|Tnl}$Ai@Nb-PJM9X)4Azs$n>fW8Ks-$!#Vk+E2tpGZm=m4TajA0hltK zmT7KZxLK*nRYc074bhxtfkFb@mPB|n%&?rQwlRGkJ}lJ7ks5QHYo)UAq`I}n1HWzZ zz{71m_YP3nfmYtrnF*Tb)kZ$K&pFyxf_J=i6zs zck#Xtvt+&4m5L|ZlJI1EBA)Fc=pXU#b|wJZV(@GoE$k{P{^~HP!cUh4;sI^=l|oZo z%%u|M8RBH>TpUhN$B7Ibe3@s2%XJR8yCP6dAH?Beyb{aHij^p-z~XxFi{s3|hihMfNoyD$GSA*GcgKTLX9dLR z#>~TpxRR!aD;WzEol&VeIFqP@E7=zKzQ~%EnlYiA_Pf$Xc97j_;`i9Z?At*V?sCI# zUCy!|EOW!}%h*V+aKVGs?)Y<~51wodg;RNho7Z#30EvMCN>^)UoD1~IhFF|COb+N`yO65yD{DF~>IfJ2T4<^}6Q&2tu} zFVe(Je=ST6)PZ`K9_EDTVwU%8s5?)^benOQq&EaAGkZa8W*=y44~Eskkq9H?nW1`! zah-(HD1Ef1n_)?sK2~PxVq1|R4%V3ARI3#(b<^IicE#Ndt~~j9V(;?B6RN`B`y%kq zfml2}l!)iY2=8%%c|;Te`aGLfmi0-tVovTp-Y+H>e4IzX3V1(N@<6^^eC~z#y;urX ztm(WrS$2wH7nOQrIG(SMz>Brvc+};KUuvBsykBP<;+q^J97|9v?Q@FvoiC$uwt3qtp)9 zSeL|g?tEJ7DhFC)XXbhVualgDd9%q5*PCqdLz5-nvmNd(qYAHCgx}YBmX;@`Ih;O=@~eA{A;uj}k^yubom!snsE zW)hM#hQM2SkeqWZc7$9wa4_@;@!UTAd7#)@Wgv73uhtM+?Qyi+b5#-Tss|tYIqcgQj@okJAJ3yAQ zDvuuwkb^_S^<}Z0sMw8Au`h6cyvO?oiEF||E6QY!P30R)qJ2(l&PG-{6|;@tHl-uB zCK)l+gsL_LNzGK+_B^C8=ZeZ?OO(4zVU>lA7B)g;U167dEOP>FaePC(9DNmC6UB<= z?HJw{$KS;xifS8HA1UFDZiz#DM+%acq!HXa6m2a>-j-6tcc)=dRUj-g956q^5VJxS zKsUw&7Fmw6%xR6y)9jdytzZzd5E?$37|RxUnAUrkpfeJaG~Z_{HyB#;hQh&WB5Vyu zAi_%>RjH=v$g@FLwh=a!SzvFi1&*{>;u~hN@0MEQ?m8FzvC)$!8I^dmFaBg6{(WaK z?(Yu9pP$4~;i#Y|2+tP;^+XO{9m$ldKfd@ROV;xP*-{yCs7S8e{Nj^bc@%3oi`!54 zQlr*Ah>eTyw{f>~G8(b#!76gLmYQVsA0Gw3%Z%+c+B_=T4I z>HgR2LW|2rV*lPVi}{|~s9^PcjZ!>Vmx3GZLHL2!V-#h|GybQgI^E31z|J0u*+apok8$WNifs*H@!tQv=GjG@*1uJ&IQnsV)PV{JV^rG^AHkv?`MkR}_W7%mDZ#cuJMGay5jWg(fuhr(oI~RjAS(j2ZV1 zMyn3OkP&_G-tb-+rqmZo9}L9EF#}%TN2o{lN3WbN0iOck#D;WMDpHy=B#0?gOVQCJ zI-L@l(qvC^I$Ib4aVqV$sGg20d99bg3uwjPi4r+i04quihZNw()yKYpH@=AgH%7k( zn3=aN0ZCn{$XK3*ob`ng;LP>3&`VO_S>Z1s)k`pkS%y6vi@o4g9SHyWFnCu7!L4)= z9P(XZk#0-K_2uQIF=peSq@#>6^OTs2-^aMAv`{)q(9;@>MYP{p(FUkz&TGst#CqDk zj~Z=oxYZQVwK2z!kt4^r_wM>WW_(~vuu5pl;b^c!LL!}K;R97glIu_VMXm+yU*}>1XH5tz~FuSi}c3(p4+U~{N z?1b;w*onGcV@F6GWiPr=Tmh_rT-+A#|8BcC?sfX$58C`cH-zHRX6E{BVfbqYoy^`? zeiv*g4heAidY{vc94VCF7mkQU2}FteQ}Oapyaf2kK?3|y7;dij!TB~vd{JeCgBcds z5TuQES4|WePDJRW5wLl$ABLvmoXL1aN+5VdrNhJFf)50r5eI&iXIFMGAmdl%Ql|HHtUZqmTd#h|4!MqH+U| zYim)qtO7+{CCF*YL3Vwngt!M}0dY)GjI`I`8Nu+2@q&}TEliyaF@NDKsA&@7X(I{p z2q;e+ieV}PF+%w*0{t!l9w^}zCGP8$)+(;kD%4Z-rp4EZ4yIHIaC%!NGcKWSPL)|# zSmcb(JjEp;s&Qgd=4;p$M**+^SE#SZw~4REJqra+V%AM*rG;)2**BR#3xKK0O~N`y z(MD61JCcyTEE74a^N_ot2>F{zkhnYpp)JvHEbxF)qNRk_t0o8$oe79qLR+#T9g$s$ z2xto9{Q(5q0}GQaG0RUI6YQr#Nmm79by&05s*N9j>C*>6Q+)tzOvfPBZw_jC0@S7I zN`Uw9L_Ao}9NcM(>#Lk_f4w^%Zt#=S<`pH4=Dwg6^cTG%dT@3nD+myOUK!n22;>mx$EHybZ^5#-IW za^XC&fZnZE7g@hiS$l5xl(@e9OS2n(?N}ti{hM*%?@dw6{#4K%e2!gF_6NPfhBTg{GpWt_Pf)F1qPyo#D=H($aZ2Z0-?+wQvyFw(uLV=H$n_*v~AvXBW zN2j|6N{uEWa_SJd>WNq=(CPiR;XG^*Y~Se(t9QkC=RU9(^M%LthXw7b*|c|HGWY4_n4F5QP#AL@Hr7Q`V1X0G!<)tWIQ-+GI^(f!kjH1nr z65z@$&1l%(j)rY*s9)cRs#P^8>L@~9OP)fF8?q$OqQr9vn6qnWwaXG^PWF!RfF;#m z%XBuT>!@M6)&z{7IT|Czy^EpB12A&zyBHzHVN-$M8rq9CdcgkxR$S`Iln@G-vzT2) zp3Ugw{q6tjs1ops0U7aB>$v(f3B7=~rv&t3=w!xX+UWK)IsRE>-OSDmWG~_Kc4hK@ zVXb2kQ%}{UZH{W@?bdi{odvuF>x-lU3mYBT5f6`YKbWT2z%aoAb_MPTX^TPPD!!++ zxky@_g_!OXgmuIqusK{pY?b4Pxgmy_;-UdnlZlvMFb)&6l%YEHT}+<#E~ZZ#0R8#H z5aOwU#yop8r0Zi{xh;0p*x=*pg*e+mOSpn6veE&6iHyejXG@Tr-0);i6i=im{6jT( zah%|Nm9K#K6i@myJQ2UHRMgo@1;l5>3?bGj*6}h$Rux5C{p?d(?Slf|#5W53cw-2D zTkMT1C06({Wq}0v3xaz*S`SB~7vN;FG0tRL;$n%j%(VA5@$bbfq!W#NA8m@gkxuY< zr~xlNEWv}-Nx0q`z%055_h`Ar01+{L@DbJYNtYiUwff8YyOW=9Wh51t&nH&4B)GCe zZokO$nXfAqFLuWBJrc^bp?I<(TmpS>$s$?8D&JYW2)}jlJ6jdP#)$FY!&F6Q-QE;D zXWoChKZy+)?>j1@2fwpZ1oxC^wTw(lTxO!wPzBLb2f>F5>-er%pSwT2M-PC9Qg6BTn1^Z~xTy_< z`;2$tI(r11b;iJc;Urj@PKE`usI|j9+Gj)9_?Qu3JK2F1nB<4J+&E-Z@!V|6W&S0+ zE6Y)~z6NzW+fcQm1r^&^o10LzwM7El$g1Dmg1U8tdRZk(yGl{mUWkIW0(lfAI*SF! z1%x|Ks5C)NDC9Yu>nx6<2C^X^+B)PC5lT! zxl0OV78V^)!X6Z@E|ZnFoECct6}p4LyiKTaI%4XQ5LTTiE3P3$+H0ZGqQqka^ckI* zaZ80y%5L2CIV}h|7 zCTfqxBn>5~O&@}Z6Z=AaYCrfo&p=C|i?q=Zpclb+y#ni!nm-yr9 zQr>R&k(a9;cKFC^x6e1yM(?Jx_&7_^vrcGb@A_v1>hm1A&NTM(zI`dO1klg6$KuJB zD8BbFxiaRT>%;MUC!gaJ(K{|KaZybVWlOs(`U!9>ThZX1Ca9+g@M(T0r-Wmv_#eQc zQ}9pt) z@EIXC7Vm@b@nW;_J_woI7r`_7!%ux6eCNCmU)?eAGgE`R^>o-A5>U234;{OG(sOoLavT9lNROJSt#;p4F^{8H5jq>IEn`LDv z?Jkv_V0jeHEIL>LvH-h?B|sKAIXXX5zP4)!g~(I`6J~#ak&}jEl-m39IFOCf0OsNW z1i4qmx;ls|TT>;gYC{d*KqG3lvh~n0*+vaD*|w-pHAH*59=g-#Vt1(?zHGI^ z#TI6~4jcTo!W|D*c;NB+K)l!`CN(4|ApSIg8J8#N@pQaA!4v#+z5?KH2-7*9gkSS` zk-79-wE|#KLW%zmfHR~5zc@e$_Qv7g?U4i^SZ2-Zm5#WaZ-onKX1JJcjxXa3aX3ti zR@wmP2=KKEFFBw1&#i>)aJ2&9F9`7IPAV7e^Zsf)*_tD>@8t?-IUf0s#mu5@et6vB zjaN(AAT9C5V`kJpsrV16#(%HmeOtu%Z_%+tfW@v3gj9AkeJUW%P}HGJMQ#;2Rb*EI zHz9wuhgN%6lB|~>i}xjVPApbDhVZ`R-@PQjuTH#nET;<krU#rM+l-5kr8uOk|@ zmj|LGz>jv)`fqT@%@vMv-R=|oKYg4s2b)5sW10JSG}$O4SF5+&c2w?KC>DU9+8b%J z2g(KYGIWO`N%wt3=#4;#!Dxh%2dz&uCSRZCziw-el*t*G76ikj^$rk_% z2Bj-2WM`Nd!6*iVh+Hl5vMgF|VYNj@4o(k*U7!tgZ0AF5{seq5OBo}l48uFB12Isk zAKp^#k6z6?`64?yDEqDS?z8|qThRPrJDPU0_1M8czM+B;mm+^nDe_h`lrJxlgE$ge zvk~8t#bc(N5*XK#f|#ZxS>kqFi@2S@VBw7KAeC3ZD{e=%C%~sN2zI%yu*!6l891mZQd;NKb-A+AHxwX|N-O{lCcwS~*pmRe zR{O!RlvU^hn`Czw1zN(;(-`{pI?&K%wwOIiUT-ru9*cswg{Vr_LqoC_S`*c=tJna? z>n-Fkz3-bW35heao-6)Z=_iLxVo!pcUik8p1lhsz>R1}HFqP;Fp3vubvR)9tQc2EH zZ7&LNsl4CRDwOz3ae@}f4laDoEPO~|p`Y(3ydTBj?=4i+Wqz_V=(|EIe4Am43n``q z*b=8>jd3Vcn^xLDjzRvZ+6VW$V(@sY$cbzyju7DE1otF!@FD(8`}}x)I`e2CzAJae zZCdEZT_Jeb>MQND0Qfnr?!Vo>_`BOjjsp94eF$Fdh+!i|8~v#OFiX)xd`Lh`pgv1` z-RYL5u(!tv?iV7z<|{g$L@)BEIfRa&9ux{H0M945Idm-PcycHmPml3;C;7N91;7Og zcu$qTR^Bp278cJXM!M~bVnYzhMkE-2@ASs6YaMaD%NiFN%@~oj@loPTtP7ZcP8Vg= zSq??kod0vz2C)fvfzALF8@z`ivr)*i9D@`~6}g#TxZP9)IjF5dwczQa3-2Hk zc!gTQE7~4@NuCJK4nj;pgzR(@nN`46w5%LOtEy16wuI2ymuKo_AHSv*78vYTK6pGu~kAW04`h4 z7uZxILwm*M1{AaP$>SRkFsCogeKWmJO!tcwt8MTo02bGHW18ZT*q*}2WFm_eI*Ur1 zMqm>=lB9hW9alnqg>@G23KbSW`&I?RHq%L}uYlOAGC<~Gk$DBY0%VbW<5y=Rc4a2Q zmn9*nGZsEA;c%}DgmZ--TuS`mR!Buo_k~-cH=IMAU}Ucg9g`XOVEo(A*HcDXxE=vE zL~W8b8e^wpTag|kr7;fYYvNL!DQ&7F?zB7O;c_3m*d_p`>h2Tv@b&e|6B&4VgxT&G z73wR(c~Ojp5&%~y?DU0t{`^{rPnODE0%ga|QJ(lm#3hpqd9uFvD4qaEGE4g6$0`S$ z&oY+w`CP(6oKH5xm(hm!JcJfH#t@g7@o(`nK3tZd0Qe9ah|l>tM;aCO`9KvxEWlqY z5^%dEKn~Wp*W`_VmxkhDi(<|%mipsar#GH0_s2gg{pA>LaUEI?3Ei7S^?R+nqF4PS z0X@YM%6TkHQKExM)EA-?O8kzW{UjT(6Jl6rfuaYVpYsKe&j|Yye$FRH((&vhZS+_C zo3HuUuZ4XU>MP(ComZkaT_`qRQ@sA(NW6SKr1GEbi*RSPJ+5|G;F}u$e{$zy7yn;t z{KlcfRSC5ggOD~?v1d{$!A;j7z*@bKHNO{f4c?NISaNJtknS)6agI|EYBvRoY^TA; zeI6EhYr{Lx5I*4+@QJa5U$QI0@&XWF9>a=5dcDZMIT9=ZTG?7UQ(9;NaNUO;sNLNz z6Pp7d3h98WH#VYjZJmU;u)FNdr6@5d zB)lLJo^hTq^D&3EgP6*wj`3P!@&5GT=p|YT@gf}`cfWoy1;EW8wnzmo-NaU5W5pYQ ztN7;1H}Sf1B+bLBF#rI707*naR4yyoXS={8(F(>%R&dC7mswa`PKsTY zCT(>1;&??d4@WFbM#PF_3Gt%lU|1Eo!Mea59u>g|tV=+2LmFcFMMUKBi->fEfz@27 z&sD*kIYSZZt&aL!OB6=TKx4uzY{=EYM{L~>7whoc(#4f33%L^I?^LPhTO+7!3I88m zZyjgVm2LmlySIPceI+;P?ie9Bf#B{G4pl%EQb4)83+3+aZj@n#yIUZTK!_0`?n)AZ zyNx%;+_mZ4e!us}{Oog1ojP^Ssr}t!%(>=T%TQqg%yCKqmkNCU^@M-s!S8nE!si9t z4>-Yqxp2O5vYyWDQeftXiFiZ>-XA9UFK0Ie=x`0kX+KI3a92*lCoxtE@G0Mk0`LU9 z9W_q?UW{+^#R|8{Nxn;7@2LEp;}rt30Q`!8yf+7T*2UmPdju|4dE)012mCGo->mh( zt!l~VtL*Wx(FJJn!2LEi+-#HEa`AYyB|vPqFYdBZyfXkd_XgwMu_&Wq`h*IGwU*IJh1ey2C zch~FB_?&7vR?2Zppfr34+7rfLThbUDP9BT1SyOO6cP_ptvJw?u zrhUuzB}aU)E7VY7PHH&K@!uF@7>OsSsPMzL((v%zECXKV#9EaWn|QWsh(zFJKKOvj zdmvo5KDoCo$f)W{KFWpi-jALx>T8MfQFHKq_#C|MI~gZ^C*i#)D}0>11m731cUs`u zlWhQeOf2*fIUbb1w<8@lJ0fv?Rgm^LvrqZA;^mLwy;I`|NY0B^vC*xrxZNrMZ}OM? z*H4>C2=MLQLAY~3T!Gm8yRURXOn6Tx89|`mW)Vu_EgsViz~4y6!?Vn7a|{1X z=uY0_cap8~=)F`!iQnV}E#+sRh7=bqscecCZ_EtxY%j=fl>$=%uWG3O&)N$Cq zayYgG3`V0vPZZfag~VwV2pwaAsL6jq!IIu6cX%1;3!X*njQ>Q$^gknb`V;WBd=e|B za_7w_FlEeB+SoID{y;2pngq9y1@KFBRCu{;Q=&LZwg7WM3KpE0v!*p?ifR_3K-98m zTa8+0LRujTTPFOND$FARraJTI<@*{HWb=6hm`cq&`i}tTb^*-Lh7cCJ8Z#B*up$wl z>=1Y-`zpw`{);izcDw;F<-T+~bKXh?xOTrt{Q>#od*uynC!{5MEZ+G~SaUYZ6=Q3K z*oSJ3f*oS7x5*#p2HSGX+a}5SX0Zqxas0M*LdTXsukLn_>S1;w}U6)ouDarGhixCc~8V^ z{!;|t1^76T69>dXH_Jt2k6av%mTT7)4&*D)4>l$Vydk*O;E&(SUGQV>VqIb5TImYh zs927>)%FI!w9%aZ-0ma*`{Ln7Z+RcyxW6kvb8_bAwSug#$_Q0nQCGY} zI5Q2H=|JAfG#-0TE?DPT$YUBIpZ@~^`aB=Y!J`kd@Zeqfj?Rh&KSOvEv>t#H35NO} z3dg;jK9W+1-QU0o{mb!NhYdcjnl9JC30>+t7;AlAMx6khW&JlqOte7YSgttwEXq7a zqb6u7^4&)uUMzFCSZBX^&%$-~b67gRT&zTBLtJ-;lh4T@@O<%_!PYVdTOBFBACEJ-pHg=sV>qvb{v%<*{Ig5&7E*b>U-+ zoU}Gc!Ohr|ft+nQDB4w`08@S0O6gVP3;T2@EJ|L6<(bZEoe3`+I!p`A3Uu6tbj^qT zB!>=aicz4wB?tDX^@m%XKiq2q;8GC;KQ@gt%k$+5$}hkx)=h^ArdSWd)LFgZu;L|D z3BdVrlaUoT5Oonlu|9SLHir+AJZqwGHw*6z#9tKJ;>TKhTpNvBl3>^YOcUhO5PkaI0b&9@aYIo_wB%s~qugwZP2uV6!i<-5-x=op;Ga z>R7ZkT-+C@!O0uXi+v89@pmLU4M&deRHVpv7rJx+{A~K`zSfRby|LK3ALh+{2{UKE zj45*m!^(aNmIp0_PpZ2%9mUqiX~C9ml9Ah4D2kYG%oSK=%(Ua=1*#_?8x$K4i^;;=nBJLN3PLwf{-dRf8 zHWQ!aFU0rN%W^h+zimiO~HUZ|4QkR`%hZgPSNmgrP zjYR==h2i1mK%I?reU%5k&$GqbY@hTQjyHVA;1$Q=IPNkMZ}?4-oOl5~Nm_<)^W1P* z+{Ycc7(U#YX8=qKeLyVxF8Lc95^#G}uuc!WE(-ix-V$68TYfQbA+8iGz|GRdxKptd z*UJ{+Uc(AJY^R0xQx%51AEpO)#TDSN(Rgu3l=#lkmAETF-+M*u?TI+UNr-KJAfNfZ zxDbU{tMKxEJ}NT6{X~wR65>KU`Y=aB0Y1;?$@g%MuR#YR(4Jthy3L^~Hj{Ya?j`|v z-6H(nu@K+X&BVL;<8dNo4E9D1$7-*aP$9W*j?D`QoAhU_7-XUQU)c=(H`q^o4(?)& zT^9_(vbp_qEpr<#_-{2p$9rd44G{-643p=Nf{mODE28b;mlL1`*qG)NB(Kj!#%2L{ zQ?U-G7Ht$ET34Wx7ubcB+Lo=ekXX$9SAZ#ICcwEOcm?~!YKs{1m?!)cNLs2;Q}lLJ zYWt=3Pz$^aQ<3x58^PihRhaG0;iWOEzENddJR@G!*n;-i$b+jN18k^o#bba8bA`MC zDsZl-Z{~(Vwa~N+v<4;e98-a_Hy4Q7=Ba(Aji&MvTr&tTBTL{T#KpTywE|81>|N}q z_Sr6Z1srl*bsU(=OL!RsnGGB%qTB>Hb!)x=9D~p%QD?aT1T{x!F6`Y91mC7Gy`R{b zCI66Y?I!ci@DYF=VdrO!*~`XbnpJ<;+4o0M!VIKGk3(wU%P0{Wy(Vrn)&vdE^_Gst zjK!HGOMI9!4_}wt;g=Ry+}Px!c`(OrnZwd*YFi-Hgt;K|Jz8e9myB#feL3dILJyl1 zI6QM;rBT)1B>7&4w>DF7N!l}hBXPoIFwS_7!{OzFal~O5-t?P+PZE~mi!=v(U+jfz zqP`C{$i-rpsQZyhQLZYvAQT$_cNze*zxj5RJ1!M26?kp&d%g`WJ-h1aVaOa%363KlZzR%|pc=;~gVgW4CChJ7gbXwtsW=#c?7nF4O8_kuz4P73!a1T_&>w;B@0aNX@PMP69@bmGse;? z_rTm4{V;oGe_fo>dcg?HT{sHU7mUGFn{k-3c#`Il_VMo8)496AS|K*U&Dp^ElKicW!r$g91(-!GHd{qi#H$L&RwW^!HdR5^9GKlB9RJnov%pKG zE!kDB5Ssu~Ws7#P=d%cD>vH8-B1yb)MadQA&63kgd}Ilh#YluqjvjnHeb? zGPON2N$hj#Mov@^fJK1`F!Ntlo?UX>;gsd77TUeo7cuRLh*^`Y@J6*KXmAKAQ52jI zr--VDHOC{UJ{taV0SI0diNMw<_^%2>K#N$7_826|`^ngltDR(C>0WSN>3|i13o+k$ zD&{WYikp2A88#M)QKOI^It2LvebEv%6m4?c95Mj=!iMVP`E%*B@mZ0rZZmqdlU-Q` zyi_S2AJx`C7I&BzW*E>4ysE;?3z`4YKEKBC(gZEkP=T>WPTGwDxZUERJ0qMI`}~UQ z035M@39q}4#*r06a8y+IT!`f1>5lk1#|1x^`{B;&WIU4Z0Q*>hu9c`3@I>*pXX4?S z7*XE<+^YA&tx7jsDYC;A$$>BBF2u#Gxww>Lh0D2CxR5aeKV(eB#iDt*TqRbuZn;?L zKvBO4EoSLr1$?i^09J_4CCYarzyFp|Lr{1LF2~mo$miSQfZyA!@l(SL{Ma}PpH)o8nXFMb z5Iq!~{x6}yv8Mu@Z2cTO#{3^F5cQqf%L2o?Szu7FKVj5}=P+?{A6U*Dgcf<$rg^xAk0Mv3`?zcdS|`1U=mt_%$7{{RF`a# zzqg}a{+`@lZ_ILH%QK-Rys1rDYO5`ngj0q21_&^%GK*CV0&GHDDiBj;Gdc^9ygEx& zm>({mZ%{0A=9a?80CNs+;yO-75T)FlqX3(|&GBmzhy*QQ`)N1`e*$9|drvW1d1nz6DyMF3_mbSQ$_Rw7JJ-r)8qjmXvV zVdX+XM-bnbtP1QL;|Pb)MX+_B39A)ip_cW6zyDAHcr3CaMj+d}7aD>Gp~b&9HUtd7 zuD~I9W#t6Cl{N<-iv|6*b~&!D^Txy7VLDa`Iic15aZL2JuDtLq=7KEdutNKP0a)h_ z#~BL&u(a7ZIQTM>x)}ep15A>imL^pWOIXI2?C2`WW++567qqv*>nDZ2$e^?50XIEHuZ4 z-u8)3M0$RFc}}2L~nZ-XEk( z>M-y9v2g}IFCQ!FI|Uz>OvH)Q5!ezs0IS@4plsRm$gt^#sCnIBKkCnzDFBb_{cm{r zMGN%n{TB=y{sP8Nc^TuU55bf1?$ zL7dRDDh&y8j;D*8%-mk8=Qy2b?Vftn2$bdQyxLH#5Hqq@=Zabur~qo6h{`HrmoFS; z?=C~8Fhg*2g`Yf%AdAC?8Z|2RH=$xrlUB5;xCED~%V1A+^{z(kt1glIgqSK!^);J6 z66?}n!Es;0%iwJW-wdsT0db`sslLo{2}8ceMy|h(!xKX?mN5EmdZM%%LQ<3bHqt-Aa2_;z4H+Znb&ihvKDpBWM(k zI`_w`uA=;|!*SGp7~Tq;hHsMXaj`^l+PVnb>k#FVix&=X_(%YLt+qM_XI`jSd44ZbtI7nIDywlmTcLgQxm-}bl#9$4 z;tW3PiqDx77Z~H^{608i>qIK^fqgH;E_ea{^LoK%>{A%e{`Ow~hW>s3g1#?3fgvM%VC>YPm^yEwbpKq^3+@uY{)~Q{MLY)9?hRSGK=w^Wpus>@@|PI+uAySR+W5p44KW9wx?WfCmv_<}m_ zTdzj~%*32!ZU!sRMLWtK=ePtnc})h=J989h0-Ps5BfzE-r*Fzvc(b<`iOQBBcW03( zZoY!d$lhL{>RYg?hD3%wB15cN3kW#NiPR-GBqooR@cW8zl1C`cZh zxIGI&t5<5_%B$WF{%yesT@&#b;6%CD#I8y~NJR{Mb3@^t>Wih}%VFg;2iESBFyCPS zmfLqnZ1`BD_zgjZb58+y2->|R7xw6nt=>bhKV%$E#ZJT9>2vW(ktkuavyRzv>=t{Y zjD~+IDwT*J6WEU?=xm*Pox!--=BJb9--(!lQ+^|G z#&4GcOp}5l$E5OTZ%J-+X`Blk%PuB|sl}0gZkH8@pHHdq` zsu3da3%9|3qVeoI~;F(1Ds&BfKM1^6X-mH;g9%HRAbbUfad&&g`_jXD?HsCSju z2oW`oFl;_Q;~}{)aJWY&BXp$-EKGH&Dsx>eR#9;YtpsmB{6y6Ee7-TH#Om}1d74^0 zctgI6)44jm^3Hy-{kwv2XQwx=Z??ye&2#Zl$ymIZGZ<%cM&V%6P_zg1K&5MU6g&4q z=CU4$uz3-V(_X;L5l>-6-~WSwegA~s{r&^J2Rw-Zqk3Y5<#3FhHX7q*O~ll>1b8|u zZD%UL)0WLq3q9Xw9_%6)OM2yw;L>2kHpC*8J7#VxLf$Sp{||_`9j-&op?Xv(z~usP z3CacF!p=P8uE`RJ#ZAgN&%$lNo^l1aP@v5_B>C+@xqVcaI@*NNLt>i`HY>ahM^>Zh zPzP2WUX6zR0`#8d|E0KPo8_XhqXCuM>NLu?)MytN0p{Y8>I& z2yF`$tDPWLJ5B9;R6_!S%cG4+7}0oMhGOZ89taB@i8${8NOA6g z+JM1m_8fqAkAB)LzdLZWIJ`+XlR6C_=FP|V^~-Q|jhp6)_jUwJuE;TA0?by!E){-T z;yo6#2s0Hc*@)MYG^f;AJp%B(?Gd=WIYcMTe_Ol+@5W5m7Rq;mEyX&I#;a~)@qVNg zev%8@jm9Y4krd>?7J1!+C5H08Ce~SJAk`W+ngy+00?BrPWnBs$)kolNWdMFlv%>`e z_}3)s#{gf>kQ`H-#WyjQI3F?^XMG3ZGg0CTc{cd9c#&9fFFe{9B)N7d9_$@UwZJVDiuk-ubW%#;w2Hwjbj@Q%s<9L!>NW=S}(eqi9I(9>@eNQAV?uHQS7hpH} zX-pgS6h;pG3kLN6D|+_(E1v7~ALuslY4jP{2ZJXJ!N?h-G1*G;-Y(^xVLu16U94f{ zIUmcy7Qr>i2_c1n3U7vp6XB)mRvZyUJXoX0>Vxcys1lVd`~Lu(yDl5K;yiODh18+b zy&`&tYEW{dPNVFYfOu>b%8xXObzb!t;AVlh<;WT|i3$^9wl!DnY|#0{1lUyHs_pd} zxvPtGr5pw!jxCEfEc8G7pN&Cg0!$076*y5?waT;ygjK8s;U&l>z=W5<+k~}HJ^O|i3Y zhfl?u!BcV4eGHC!jKc@f^ELOqx@skEu1&<9Eu!Ry#ENp&4X(cN&MNtv0`u!sVtH(T zsR8l&bXDOyRsNFy+T)j$MFOwn!HHJ*J=GeQ(-+`srY$a{%)^&alW{I!xLWC-a;))l z!9v_x<&N8{x#qQZm)nRia$=6z>STsEJbWWw$8+zUjmMog<8e49|t-O=Fr z0?J%^BFDZD;%vLYZ*Di(PJR}aTto?<|+Ou=25mZSWE}#IDetFu6huXUwIo<&3yO6z?uq_2v0Xh%5Kk7(=QY znGt|XHqk=oB7a?uDzJtyk+Y>hMXX4ExAdr}_R)r}Xx6;9@_-2D!4_3t0!#&NmfM7w za}sKHH|gXe0&E81r5dwWnt3mS3k$GU+M;9^lW&gkige3u6W)TZD3>?R0v18$(S(>* z*~}3X;2mA|K>+5_RAR!*+k}_E7VRrXrC5rR1C{z1hmV;5vT=jszLBe95!;cZ`7hVQ zO5T)(#0?q$t8m5JRAC?!L=(9QWJ%! z`dIjv1i>xC1c%+xH~Tys|Dp1j`DqM^3=0AhFNB<8Z=t6khcdYrE1KKNNc6Qd1}ogaVgDK0lpA7S0N_A zzh%tFkCGRE89fmfvgYARnH?^b+u=dG2d*?Z;CiczLW~_OGD%*{MF`kN$|0ZAv3PV= zKD)sC;7z&B&&=^%0r%m%=|*9T6BpRrK?}{k@ON@?`?YjjdnFOK4=3V=q!GW1(*GnE z#UGpP@KrVI4f5F&``|!i4{Q(ZjgVh$r!(niQ_sp{ZZ^S2&K-w(d60(t#W_8#{ldM9w7od1@Gt2#g~-}aG}u-7dz~6 zwbR3BdN?E&^JtWIXFWPXMdoA&Il)g6-WX%p=ty+ejt=2?xI04g=WAPoad*#3{Jz>( z9QRzD3Ll5#esc1{|F;BN}t@k>J}F0D(%&D{dSNy&fD3A~@I2R@T{uTAcE zh}y5hqeGm~P;3;sHl*QRt>n%X{j%%6AH5dM2wup>K(OzRnav?DMT%(fDn>KfZ5TfiEhp@j<~9oK75w17Xi$SK#y5 z9Mm1F{d%I_yD!S*f0OMx2;nw8w6A%_#OE+!LU)Xu&`s=eck~|r9G)Ke5B%4Vr|_2{ zf5(%R7NeF<#5l((u=JjXIU(Y};&>rWB(!ITWXoJrxRYoiI=@Ow{fb9=3}bZz)FU<|2idDxAGO8#$5>7wsxjjsv8_G;=Esv z5aZ)Hxy$E9=eUHwA*+O|}eM!mVk(^wLf5FZTI;gE-ia{2@SF^aZj*Gno;)i-C-D;HUU!Cw7iC3J4Xcra>SRcnP(apqu zt`Ap$Z|%u5a^Lsc40ylTDC)LGa?Z5^P%9oCD;KLPu~*bizOOs=qVhF?xLYcDbheX# zYm4v0X3BAaz`IBR<`5IdiP7Wo1BMiTkTPaI4)FH(JGQbU5LDr~I5-uX4A> zqmg)cM4op_Zl8%VKt3(UGgRaxqZ0pCy3wV@iZh#A-paxKGg-L#Y6`9$krZNoyyU(S z_@&*mYvc48csp%0PDJ&^UjKgxz%O81Xg{?34?u-mZ{*4UC*5TTLKpRg9s8Osdt!oR zKa8B(8$&Gnps!_bJU8(L{P*Z*@F%g=PmJz{Zd3bV(EO1YVK)w=942F|n#~r|5gK8Gd34PY;p-6R7Bm_`fOphrv5HDtEwB<~MvcI0PVY6XG(+d?3&flW zMb)MH5@1d{v>?1ShgWG!q_%c())8~xu9h$YtgVs~{|YdJ&@z}W@Jd5~&HUF?WR3@O zT$oj97O!ZfIVQ{{bNs48;8x@V@A5!IwZ`i(6RmZ|_Iy=d20_l)B{}r&B2{9xO>#^) zX=A2d6xiWKfYUdoB5rLgLYspS)D)}$`_~5RWC-TgG3{{(styu}1@H<_vC>Q75IqmG zmc9gg#~}#t7%l)0MxMuD6gl=mgWEtfI*FBb?x90Ud%}m}wbV)YuxKW}ty`b~GiT#A zEcf>W>hiDz7zcuk zO9pO=0^h3+!JQIs0oVnjP89=$4`_q2S@*CX)gq5o)b*mMKK44mmx1 z-HdcCUfnpBh-(L9aedcHToU{IW0RBax%^(1ddZ5Rg zff%xMEXKJ`#U!tp0`F{ChFa_XTGrtUu^?&@mI=Vl8E$aN@q}xxm##z3%>}qVd1Olh z5<60LVot%fV$FrC1mtoKF>M!6w+aWF3m*f_f>x;jOxs<5v`HbZIn=DdX@xp@XbZ=b zYgLH}t{H^aw9^EfAoECo3GE*j0H7LMup^}EXbUP1H*51XTfO)g@AFuFtXbhDxCEH0 z%iv?Q&V-h>n1v}G{}t%6L$wMStq83&A!ZQhfSL#$HX^*C&C#0o(lQfXGZbDqno3Nd zS=i#F2DVn}7rWkQ*GyTTgqYRQSji@ijwl518B)jX;juV3fKHM2&NQ)UJ3@r>u3j^~DxZfupe_@lL)azNnI1xM86v z2X_{5)fqgx1t~WLKV+zrTIa)Z+$ZKO2ad|&)p5sX{w#GdXeQk(qf)hgUSE4lCctAURR@p>Z< zJ|S=)sW5Wk-353g7d%#@ABq#Xnd6FI;uhnFFe_Y3U8(?6fxn2Hgx_;)@Sx6BSL?u5 z0db9wv2(=sP&`=gsk<;yW7NG5PGr1>zI(*}2Tn>(NGc6?1o%n)Q#g_#Q%ZYeZWSf0lrsHP*W;D`?a*2zPntfi9Y z@>nT=5@Zu*=EtTAb7+adoR}?@x*|vmCsr7f{w+9DqVha@IIeMXfwAKD8#ha1ejk8)t4&FAtS;`STlFX z^L7gCyNdM4$CwjyXvrL2;*>%*c_enEDZm_0PFxd@$kr$YlmJJqjzQR}D7iR9s$J(g z(@~ttxIP}C^>T$Ra#4V1E`AwHmJNcpK%C+=8kx=m72s;Q+Sl9nRe(8he4Tf1UB=~f z`b2zKItyP^&%@8HcDT~%f;-!Nbq?#Jy;LSDZ-fCc^Tk~u#s+9R!*yJhLo?j`jI)m} ziQ?T`9jGg2a1z71S%1b(>uxwJ`+hVC-t?2?+Z1y(EKgF3mY!c;{3nc0z@u1iTzjH!F@ErUcWsPqmrs1df zSz3j@R%j;*?1=|*LBUpe{~P49YzsD4>Db8GPb+jkqie0pMA_vz`?!CRx8(c+@hbxJ zEAreE@_VllV8Yu~)H@!hi`i*Arht6+SOTu>4abGee)y@w6JOWZ;k}$0IFmREudRF; zCq#XZ1^2;$Ko+rv3BV)KA^;bA4MmFcPy~u&T0Z*)OrP>Jrpz0N2{ywqY54?9bDyDk z?E=Yb7fD{bIL#F{2@aSaYX@slS;m5RJ1os`flHw;Jh`x+5Lz3hg24h8Ta7g*-Y2ogC~UDhO^Dgp!ExcDtwjdt!cOziHZ&e?LGAu# zwb5+a&=oBI1z=k0I$a^hXzn1y463!x6Bhf-Ai$jPZ=tPSN1C+EU3R!pgJV`zue9i6 zH79s%RDjuK!X^!d7O?h|YtR;RSmBMheJhFSyOol zFDuLp-e)n&oK{HX)tO1Ez-c=0&IJMD+G7yh5Q4zE00cLOA*?l0o@ap0zHE*=a~^WW zrZl;lh9S7r9WHVbTdx?7g$w(@M*vRq9)k>rfv9vHf@+8UXmFBn?1d(Y4!2&|9M~U+ z<3{3a-V~fKn~iUqZSmVWM;*VsvCRwjcKREZnTwY157TwD?r!qI-A*3`mxUoVNZjcN zz>O9k-9q$XdmwJqc;J)R>DV~u3A9iAf3RW3-?7iOKTbN2$GM<6_#(+3zZC`GLA&IW zasolVQ@m1d#A^ca>k_XD(8oBbq14uNiS7=Es_m7{!~UA*uF;l>(g{L)~9i_MF1ccZBA z9&g<}@ZkZm+lS?O+{@?{dG4z$Zm}xQ;$4){Zpx{G$Joski#w7|T$JzSyEQKOs@6_Y zk~uh&Fc!xn2H<#DUn4^M<4EWL99}sZn?r}A(SJCKy@w;#$xz?fohE%ay;NF=UqUpK--$AefzY=S)1}y zUCr%$c%;b0v?nXbRN}x|u3Mg{RcbbEu+6w;Pn8z52r?n264Oo>a7lZnpjpBM z*a%VQV;q34*RCseTQT>g0uw}|)LyI*{}Etw_=xt|%!y5a33rOv43`2QIOcijltI%z zn~}4-7zO*wk-x7@?{j+&)NdKZ=Dk z{%tNQklL9lSJ@!=<-5Q=$_92W<1ugEi|}1O2&o=pkh;9T0$gP;3helj=E19+`=ZUg zC)WG)#7;TUPNt8+xsn<9tZpGLtX_f38(cJQZS^v&w7`3Rhd-{aamCd(7u|V~s*4Q* z$VO4&^`W@m5r_vZ{dJlj-c+GE(Lqv*Yn1`GnCpfM84ma&+#2V7rr`t6 zDR|duEY7)&!#l3S@TSMhc*|!fe$8Ga_Sp{)+Wa&Rz9SZzQyta%A6SX|TYPY--5!@Z zmf`j$H$2?or8_rr(K)r!g!nMsN2oC=flh=MGp)`uo#6685xBW47{9D{!`IDAalX_F zZ)Z%x$(4g~Ol4u?#;f)md61bSe+Dyc?O4?Nu36GKhwZFY$7FqIK zZS|5UIntMs zmve>1e#Ea%hhwg%S{W)U3sNS$w9vVGN>F&9LLugTDlV7R;iP`5wP~ee+ETPi$f|bo z#uUkaIWs9h0rss9)aS&?71eCd#hSK>3b%NN$}2AE^Oe|1+YB-pq~*32)sWY?DW;=VGnBqp?X1M zig9b3A1-b3#P@B>@oAMc-pikcGpVDrXmvEW8(t0ViPKRp<4p8WoQxi>v%xxi2B5;D zKQdegA#h1gSX(}UsT2PPma}_en%yYOa<#;QkomAnSfS^XmssB5=2%IgQgk{gi>Oph z6Jl1F{pHAMsH`#*P=d^hyxF_J^OfSmAk2IXK9>-4v%Sbw@km~iuFV_FceQ9GqQ+pe z2f-!8{2A|asFqb|!drc)1(gR{P`0;8A*LFe(3-*g*ql;m?)E@{%{8;Q7@h)Ld7||( zxI7ZxLIIS0n*_MzV68%2w7*&brj=%4i}^4=D93g=EJSG8pusRYtqc`5L4N$D$OlW4 z4;|JJ3;S$$c*~bbE6f6w2{7$5mDdcaG1Zm;6L!MO_DY@T-WQH_$$Ly@onN(rbQAiTij;aZTf3`DxS1y8w?{qAj*8`68x+B!K2hx`hK*939 z2EcZ`QMaPEC~!Y*t!#7ejx|2ru{&xIPG(HNImv~&2>eg09Tei<*SX@-T37tkY^S^N zeOWvYUzaX?ykF488d2m%FI_Ezy8)sz2KQ<`akJ75m&#o5bAcmHhfKx#g)bsk0E>IU z0x5k2?dVT{wQta=?dy)9{-8Q0%jQ z5gj8f(9qukErTtvcBlomP5Ez}SkwofhFaoIndG#q0(A=Fl{!&ZF7SR#UWa)*=Pk1X z>mVCYLUhHB`@4L_ZE#mXPXpjX{zed9uBIUhePzuG+$F@j{B->I<|YpvcDo=d{zdIV zf!7jmq>aYO7_rJB-Ebu6d7KLCjWa6;2*iW&O2lC73>b(OkG?2&>Wd_YzHp!a3}#RI zE2d0&3bPizgt<-=G2ds7cGhti$V>9tz`7_&pArzg#ux%3e6+}PYE)0=yFC9m6_pic zoNAU1%zbqr zUcOuok)^K9(HSQxqS`UF0=d}tp!j773Z91*iw0oD%;yj==UEgu4b_FwY8(fl(QzQ^ zmiI!PeGl!-YW3)e4)5+dt?fYKaJ-f`3GY=|;k!1;Q9IpnS>l&A2YgkvP{%Ys&YP`^ z;&HdW?@N~Ar!sr|TIGu0t2}kRDy)vc{RVH{o9JSxGcJ_6;KS5~I2|zyTO3|SirDDr z9u~+Zbo2Y;wZOUfImZ|GYGQD!ECSbaB!A8g!mYv}Jdn?ZrUZ#tT+VUBhyIq>Cb{(b zaTaJBZh?~LEs*nH7O3rQfdeyNz-NIo@Tk}u*Yg~3IaeG&nX|FM+0J-8+8mDStGRFi zm6$_eQO5jds_8*d)4lRHw|nCD4pHE}zVet{SUC2*KR~VZt#!`0)Up&;+m}ne?1uZB zJ#clcy|zhy*Jy(eOXR|mIt<5S2jFOUPZ92C1mLIfT6iA?_*D1+dAWYr>eCm^ZoQD_ z&$tg`ynF90g(|l@LVwh%V#_TpV`l$ z$a5U>mi0%2x^60qNux$5R+ zrxUDjeB~^(+4VvGgg+teWyy<&{~KCozKBzj?|mL)k59wx@L9-Wd=qAeZ=&q*bAkh| zWqaVa3`e{rpJDy9Kcj7osJFnoW`YH_%=l02ThK$T^!b2k_%&rYeokMG>!o6yTO)uD z0d=jY?KZgxZ138nd`Bc6?updgmDBz1ij977fUA*6jx09$##VP@`B&z<%zqC^?C@29 zFE=m2#Rgj)JH9LK@;WP`aoK>WaNnP-oH64mV@|}POn?Q~>nUQp zWLE);)|vAbS&e34i>gUzb=XMW7=y(uDzH8-AC6Zlg32(jT4vsBUbXl|@Tx{N<`>06 z0td@!b6LPLfi+j);Fr&;H2)b47NgjZK^x7ymXB!;EGo=~5WWr*q)%6xgPI7j59yTj9IEbL}>gZ<>cA=_&#GFA*gx$6+ry1b0C zW!-iCCAL;}_zcpVY+b+*Yz-fVgNdVYrf>$nY_-Rw&3?GN$qzrby5iG{C3rvIN^JB} zd{^rvC$2lbuXDxcWy|qN(NcU}<%r(|h9KNDO0 zN27gte-uyoPlY&lpaoVVc9j_rdmnUfAeQ_3ev7fj7;e zA3Uv}#{yB_iIz`e-U?A)ugRDnG)L6eLHF6>(#5Gd)iqluBv2t)cOi7Fy|4zt`RXj7 zCJD@(_sjEx)>&0m6qpM@q{>AfUf(S}MCZ>&Pj z>SB$;wPi?a%tl;gGSZuJ41jf_v4Cp^@0*ViQghP2sxJGU<+fI#2{N0u1>DM`4H~>{ zwpSWMOw5yw=5F4mB8G{@bI$kHFe}is%(Tk{nBdVeo3oJ=PElXyA}@uQ%F3S;V76Ye ztBbe&Ys1t!vB`toTFjOC&*9(A?99c=hB)0#k=v(ot=+hyNW>(%AwA6%zOGZSXl8dT z8vj3tUNH!%D_%yi+hEjq3`Uh>A5_}Eh(?#*X!9C~)xLuT;NjR1JQN3##woyG$jSNR zIybS>F8H`=spNO_@ougazNvMCC` z@S(_lI3NnVJ-8><`Sp?uWj|Cq_d?dP9*DK;4+lwa=1hMUV=bS-Ec=m|<16+#(gv>C zZn{lvVtbNKCNO)EsgCBnbaUCK6v>6nU@gWNf;1MOU=Ut{n!USJ!R4||46eVHwyi*a zu8TxS=yT0=-Zo_E`fqIO0ayoH^@P)E^YfKwa9K69rCIT{Mw6@LVoZDU-Xl=sJ^ z9v_r1!W+4>@kaJ+yp?B#ADTSybBnj+#Q}2k!*>my_^QT5e_GXeKD zB}<+fBN2^To8ly*ab*Ku4zpLrushE>T67NAi(=2 zSKjKc%hX;KfPbp6#t&ulBt4jmkFzJ?^|+UDB(yvB_&$l9UjHWP^WRmC4@UOIuHfF- z;NKk`-o4P|*;jkg6KuO90JlI$zhq%AEEeTu^TsIlK#OCV7cd*ktpc%SEl83dR?c=S`S;5t{WJllg=P?7?t)?hY>WxlKUQAaW$ygI+?N)*n7747 z=d?*0+Adaob;)CZlWNmp!R0Qweh-7d(mK;h6J}iwQ$RKC^B)zM@KT-s2rqB5dS^~* zFk33kp(N%EEWxvQWfij+Hi^qsQ3)LFGnJUongH|2pSzX%!ll^z@dEFBzPZpH0p``v z{9O)5X^~6rYXMAbj<#;Lr7FN&+>V==b8)aZQR1YWAS9-GAS`r&t{FUM?Ek=K%wG__ z>}BM-4nu|K@W(1#vg{euIQ7)N+_k~O(CXV8oe=|YG;*E*tgGqg+Gj-^eYT^HFE1UT$5 z#Md?kh!O|l*47~0*y5{w;JSD6W^Y4*53}DpOfB?XaU`_OKUK|Fc)uty)Rz+#XqmTr z{uz6`{)A)UJ#jK-5DrB2#b$qT6W-m$>GVOpxSe7rP95w4pLyM3J>f}AnD}>$n$ZJu z+$=FaYyp<1I;$W=)W_)9EUV2Xz?^)b>Z`p9#%(Gx!6mR9z2H!gLM-Pd&lR=K;vA{I z1U89V-3q*a1eo9wVxD)pOjH-dT%3Yx&iBAZ4~2M-u~rtR>N40y$xbY8V?-q;yvD|8 zHM&A2b6^G`W(Ar6v+|tXT7bNcVhsXJ3!PA%3Jb21XD)w9VE@>aMHM#lT?TKP!9o@* z(*FvtX`KnM3A1?xsH|04@ zm0UB}wja`*hpGyfc?>~`Q!nJ%J%w`nZfGaKVMBD`yVaq6urF-_-l$lBw<~OMy1+_z zj(<}u^tqBncqe}mJ}h01&#FaX>jc(X4~;MDB;OTV`(=X{J{2YYxQf=>MInB#XgS`^ zS&X-{Z1Az<$Zsaw;6&I=yc#$QZ$>V_J1duHHTr#ciYzsqpE z7GjC>DK=`)e<*du_oDDWRJh|uQF~hT3pL)N%0BXQPh6<=$FKE%xZWP6x$otUa9mj( zfm@w1xZWvHZ-^2lj>7FNqS9N!ac64;?raYiXhU#qgBLEZcgEEX&SJS;b?g@hLxJ7? zxVhFzE6~5UEWvkGbMaN#Onh3%+;=bzbDE&!y<0skaKKO0H=>v3zPm&EU>)K0d=X7v z;(EM>ph&*|aGTz6o!tX7$NvrECOwZ)bNgYQ?;I?Ov4dldk9J!{x29^2YfiFxT)UBT z(djDo_;K-yHoD6~o0VqT-9gy#u!mD zms#Bqvtj_Uorh>H%yGzEyMLg>;W@PU4Zxbv!3Mw~{je`%q5}M8l`UQ`w8AL?_)MNP z-YQt20Dn-j44+iGD8OGg_zAea3h-y5zMs|m;A2tbPb9vmXJ4=YUg3hzL^Zz>cz-Ar z*s~q+L82|rMcd$`gk|_5%^9DhIpBP}9o~tu!P&66IN>`L$9yK^WS}L^gw4YFw8a9c zC$5W{-VsZDcU=swtO{0;zbkfD^}QdVNjS`!~ad%53?rw|3%}t?Vuf4SooB&_zbk^n$Hg4SC=Jgof@2ltG^Wtgvq+l}M z%N&j4D|;%uJAD46aVX#!9G0ASe@Jg^3+j!vzCF?G)ekjZFQd$J7_yv4AZTHK*vtQA z`lRPDX+~d6v>lFxA&amy(G@O*0SFam6}L7^SCQ0pn8hWsbC3|5wHKa$Ccw;F4S*BX zc4u|91~W26IT>70GDptkj2%?sJW=2rh1dYMSpE*zzA}cIIO3u!c(QR6~b~V&7->%90s=lj>9#7!seBhjxe5BT8AVutRa!nd6 z2rom|s}*4Xm;=*R(>_yu|Nj8~qXPf2fMr&u%@#^ROn})>N(HtInTKhiR#=iD;FS0p z0Cx=&5ljNf+}O0sRAef&X`#&mmkBTxm(cTy$e}GxN#t$*qiAioM=>{9%UI3L$`f@X z@_;MK=^WJgXDS zTLK55(Z3hkLi%D)#$=qSw8dG8lX+r$vu5B__FTN4YbAEr2B)*E@p`&7-ppBo4@+F| zMYT7+s`bTZqJ$sTxZ^xkSuFJrjRCr8_s_Jp<=*(c)Ca#7c;Zr_7cS>@K$2MdQwPFJNzwg8W;D3FNZz|Gs6RS`24j`~P*ivg)4h%2?1#g{sxR#3^vCq+ePB6%7^c`yg6&F2EKT-= zi(G_5n^WX`$kQcUbM_d5RC=gRhd#}7k&x-aik+EyKE!v#>yVSt*K7=rG6*IkPtN0l z1IE&@RNgq&S=2V?D8vfz0b}BhIZmu2F#P*$HRd?5aKm}_x>LiB%EznRaIp+-O=@ls zm?y`a^+k_^yB2cDh{2ghTnCzy7YHyn7*DEBc?_`v%&V4}1DnMw-q**)0+;~v1DbZ) z1i0$h|AyGK&HP{ln0beG!pPN+C3`9`3s{o^W@&KaZsxeO(X`J5l;Bc92_= z7A_u+0lh6SdGNoh3a5LFMV`-S6nToBcI<&d$LCPx*%M8EebMOG11&+l6yP_i7KsYm z;CS{798H^wS2JfTz^^1t$F8tZ*dTUri{A(wjh&5m@*MCOHWByRqVb@8B_4D{0PACb&REIhsB@D&u0tNx9EGT+7%h6y=JI@I5K0AK&f_8qko;bnoV$cKdQBQ)C1)nY8CYD`Ft|_#wjTGox!ouy z@N+HbwB~HJ&|FfM%gGX8h62n&mN|69+ke~=!c<;1VbGSCMJ@9Kv+7KU8MM*5{!iC< zFx8jJ%N&#yJ_5|~Uly>YhgoB`*uQ0LXRHo+Tvb*m1z=Nw39wm_=6x2X*yv#vy>vjm z>$N$KM`(2^+wHl!eM{`BIHZXRbBvJF4!O%>d`q%!K<-}<0FMk01my=HOh67yaKci@ zNfGjS^32CpWGy?Ti+-;1;!&U)S)CTRzfZTr690#-{0IO4tzUlTDJZcFOpb2mS9tqIsi2i@+ zdhhtEt1Rp{I*#w?jAIv(DoqG2)BqunPI~XX_uhN2r1uaY^xmZ-&E63e5ETVc5gYbC z<8#-uPj<|m_ufDD=Xa75l9O=0zvo$Nuf3K$mnVQ{;1BK#%ky80<^Q55QtWn|sB?-~ z^8|d}7lhB(`r+%1e)w){kboPlYMy!hxKHf9Sm$R8jPZ2d6F8eOL2VT=akV4J2nVC? zQOPS7Y#bKl-4j0n+eLjhrp`mR09+L@4_SUD2zHnPcl)VWv0?_SoabV>-!fQ7JHat0 z2%Z(O2ye?m633y4vQ-`FL5+Yyfb00aJ;_9@SWw6TuRADY!X{vDghGJEI$zcKo+p5%76k}&@Pf_T|3E5DELkC!PEaBzp0X5u=XYIK- z$%N0<>^G<}2b@*!T&ZSlmkPMW1C=ORTcJSa^oN`-ahu%+fVsH+$pkDrQ>yd8D)trC z)kg`n`VQG?`Ng<8PW7v$l~~6ZRNmonu&yK3@i4)qbtYgu(GlDb4ZAE?Sf)9`N#Z%G zuRhGr*mbzNGN#qT%?K}5Rv&a=xuvR|=BbgrhE01aJ^N!3U`{^d{#>5eq)KKGHx{FM zSB)xdE7?>r*x)V^0c9ZyCNf)SQZzXO4mkEx6_+qhKvPe837 z`w~Bfj*v%{0uLmP$BxYDI96_kml_>#uGJN&cs&Cli163Wys7RC&W;K)$wLK<*Rx zdj(brDgN6f3fq~0U&OBew>utKC9!a49DePL$1gpJ_@-TMD4o%2zxZ06H$JTM!l$kN z_^K}gxB3O>-XL7>bit?nZuoq_OX6TAvf|YB{Wcr9G0juO8OKw{D0pd^4@BOLy zTSz>#&m)7!QfA;#x(Rlq%|(BbxSgmuC%mN=;XuVy^uOAMu zR$(Tyc)E$RxCn9926lfbSI1(xpR7;l=_Zc^m5uCpuATt%V~$(ld3%Cfy^nq6T2u!d zHSJTBnBztXaoGmL>?Nvlc4xj?th^$y-2O~!%2s1l)Olb+%A}Ry9Ik5d1cs0y5_dJ@S)$p7t0GXUBpFjKi#CgZ1?IDAtUfp3b#aH}X3U*-qlMwSn*r@P@= z`U-rQYK@Px9C0Jx9Ute(D)Pj~#a_6Q?}`gqwm6q&p*m$g6Tbj2WiG~rQagN5=YkI# z-EgIO1+KL_;^QtC)v5Ex9TDmj)`f=Uc&>03jwX-AlW`-kKjL1wVcmlR;rHTTIduU#6Xv5Vd=8R*rXk368XT=B!`9XqmQHhE>AeJ2VYaYO z@sjsYG(sBF5z|+Qw2cy*?rBEFp)NJTgbGY6O^7vwT-+j}TT^udgXexr1wc+AR4|Du zQoK|SNZ#}O9=r+gF~_6iZL3Ba-CKV?l2+xZm9eG>`I{;Q;(AeIR(x^TLFZ6S1=-SD zvU<_kR_>Shb}!$j&8p0oi~Z;-w#)ldz%3Doi$#SwRWq|IAGv+S$nPyidTXwlQkdOY zXgJKkAHeg!T>OE0Usx+JA=MU|3Or<;<(JY28Z@{BmZ0j*a^CIuIa+F6g~h!$?%VMs z5|4H1^S!jpRA5~)NoD5Y1-+lkV}(3kNN}mdjFA<{tW9Nhalhf*G2tb|)olE}u>esk zBaq4CiW2*jY-YxhSx9*uqDs;2$?&NRK|)6=3fC4Qw<`;A^5(YpG{vkL_hRfLMp!)Q zb_82Ih5|pa&_OdqfhVBS?OxQl-;2(m5o#~^K(+~v7B0loRm*Xq%TpC|ydnUE3uhY6UKbTH{hMmnGf_vBHJGWq8ATF<$qYkFzT#;FRqs!?GKL6L#ZpY{fVn zb{Ho&6p2giM_{MJ2<&ir5Ifx;#lfHnIFmR__3wJS#L{rus?7l(bvWVk)uO_iL-5%e zFPyJgBG%bh0lYWrK5-mI*y3k|y&-quNX!Uz_V;wkB%DYWc$229;+Vd;S!jt{fFc1n z!hI?nZN|aQ<_Xw1%!ZxE5;%t1!7L@IkeUUSU0z$3bTxo05i7LS!KOdq~T@aiU4c-tZlO{R_8u1GtkUL>jHGb%W}5L z9kkCmh^h@my2RLoxpYhMP`Mj#B;1qc7*n>ho*^(%txYriV>P7+>XXkMyS0 zEKnf7xMVWkkT>!>o(u4rt9x0`cqVxwPSHN6&Bm_8nOGAwRrTYpjF^q|fa&ma znFwpEaj>+VBI;`byMX0ziFJWjmOuQ;A`#k1hn6evkwRp#{#o9ajC-lN1h`b*n}s{; z1Zq)Zfsv=}sszYVc^?<;6A_TuwTQ#`w$&es zrIeTooYpU3iG|PGSdHv;oWRM`Ol;_G;7aqP4+!Zb8v!nnSeX!W@#aDG=Cx#do%)_x z{Sr>DE=4+3xVK0xjtEL=%|fOqa28J^bw9IS*=HU9>cS4ay`&Gq6IO!D350s^6F;ZZ z^uzmLQE_z~thbY>#I(?igISG571jXj!VF!{%q=Ak%!cJ?=N-Q1; zqvETZ$X;%wOANsoO8ps#ZHPc}Yn-yt{2T#hoXi6iyh(Uh2dUP!+|=|Z* z<(5-nJn>FU5p}ef@^^$RegrvA<5BHB36-uRQ0{m)R=VGVPQQDxIeN4@UU)QTCSI+z z#KmSuyxZ)A>m8oB*6e|+jo!FgFKSrrgLfOv2hp z_o3%OBdogD2%8`N2lhO6Kejz^2aX$$#PeoT@rv~fydiI90{oJzF<$nXj#vB)hxA|a znT_*ozat9$lKpf%YhjERET-dC3;BAhIe5cno_c(0?l|li{Wq++OW^#wz$!P3_2M+v zJRl!GB(RP*!q(|V*fswjc+z47j=MaLvw`w+lg;pMp&i~Ucf!RI8@ySxTmgK!$`T(n z+2ivrcYM9tPi(Y5-d$;p6Dbq0J@_tc6lm8AyaafM|Lr&yJrYl+OjZCNkDG);NygZj zY=VLKSy&l99htt95b8V@ZW6ZGt(XXNQDAeIIam>739n=?1Qv%Qv?d_f`2DyDAV0^w z=7McC^1h=DuM`EZ6Zl0)wlye-3$}2})G$I+-3X2_t8IW++k5R0c$1-xF56?c^5#Gr z&)1T_sYdL1skp~tq=?f<7aPqL-<*NCrgWr;3Ue8Co*e)e?-sPq{APvQS+HFz@q%Gw zsBU6M%dG36b-=Cx9$r4lm{@0|xwQK7=oSIi5EEd=yHr$t5=rm%5?Tf5X2Wy^E>$GF547scf>ZI?aWGG8 zQG9tMf)d@~lEjYa~ithKV%$oJnS z0Ph%Mgnd(P!_!O0;8o`txEQ@vwE81~Z9&m%7b`eYg$$vI7xa8;7`-RK#>< zAcoEQ2MU!Pt~<=FBf{2=N>=^J`<5|l0R>3jrxiy#QGTQYB?miDB#?5c#MfR!49Y9x zU;?b_A_R6qOL(;!6F>q?HV858bj5D|TV|eH4Ipo4WfluZIK03Bu2x^d%hysa>~Qhs zLXhbm)HqY_W6RGb%(V8j=BcZS5Z{@Dq>dalfIPN2UBW_fAB}PYYe*T)LJ!J3|1n=2 z?=F0S{6+?A(J-|4%e^mdVl!K(_rY}WhCV___0v5Dh+>c6!f1ttnc64~%feoP}usiN?)duzDQn8jzj=0+Dg3r2q@pV5(ltkgP zwlKU`A@O033tr1`!nqV@ydLd{^N|iX7ifo9Jo4^BdzdBtQ9^^Sb>kiE!5{N|fbo|!QQPfd6P$Hw1_GschMwZ&8Ln)w7=b~eF#p7U|le<{97a>37e0YGgG(4ULn zTcYt@V;H`y^TVe#p7^NB8CR+#=3VKAo1H%Ry3Y^at`5YPJwCW5zb_jYJeM^K`$GSX zZN5g>?spq@1>PwiKZt_@^-~E?;B2ad8%eWpAkG-u<4w>PH4AlO&oe#7BHUp#JnhF~ zh5aO0IZVS6$C+5>X$q%kCxjG7AfYu4X+61OUvrQk|MwZTzp_iyy1cv2=J4Jgvsco)i3?^?w1phpnQR zRc=?{mhWaA0{4bl;9>ap`X->2mmq63)&NtDd2`_R=e1~Sjac$h1#nykXL}hSj%hN0 z%N0?Z2qP6t(@qTDJ<4{nmXzg_%s#IXaC;5jT;!dLWo6tGW3e|+Y_RZK9~;zmS=U4l z)t?Tg?-_UUF@Y!0tjc1`PTih_-Tzb%Uimy{Byo}gv((H)>-y&6y_G24UyY(Yf9(@< z5%#LXO{xJro4<3bfE)GktqDkNPe%EM6141PlQF}p*v_Ayir|(=c-IA~eZgD-n1}sS zDx={YZVL;mX=>WLu_%gbX$%uJ+1Je)jV8WVvKWhr{K-S({Nd0>i6yD8utvEe>O*dq`hj~-#@GMP9PhZBu)B+V2DGR&|wc?tRx z%~0t-6DdyP5G}Sj$Z-l>?U}fm4r{l0SnjnL79rMf&-7KQQ?R~VsZ;jGQsur>b(Dlp zs`T+#2g(n%2%Mr|a-&Y$Qik-cmBc;8InLw9kY8Ru@M7Z{ZY`n!uAiZO6-ZNvOI_KHgA` zjJ_hJya_FtD*ok46bl_K@P=2$8UW_vH{l|@^#=vQqifLd)OrOnKSz5(fC(@EIAz>x zl|Pp5TA&Z?>x2FLSZAm8DG)9uy7(L|HAh#(v?L;>CsRG<9x+cUF+Ns634~N+JtLSg zaLK-^A%HnPl%Nw}RpcS?7Voc9gU|T&{2qzzNn)kbQM;uA9S54x#-`lz8tUXfoq$L8 zBqBf*hCf4<*3q`q#=|Gt0hXd>v!1vIQzZ^Ik>w~hI&Rrx$akKM3il}}a~_Q{$45}@ zcptj_Mqzv03>-_FgJ<$g@rtPM%LNPYboMOO^Z3)gK-^drf-5UMajC{lCABUTx!|o- zTbz%x#>H40T!^y7g`lN)H*guQ23g>Wml-YyXz$rC#8t<|_;7_OKD3#S>-LNBjh796 z33bKyV!v6$3*7X!!q;Bb_}aq?-@04l2X8xk<-8o9*e}5~0r#r?B3yHl$F64h z$ZaXEc`n6OKT})_GR5_X<@h{l1-{Pq#I0gK)z#y(azEUt@KJrb*aGOsRgw5*O$>fp z7mNRFjKiN>6D95qz_m74b(4Ct%tBe@ZJxJblZz3yxEkT0zY)&FKZ+MJr{i?SEF4aq zhy4QY_6$p`O|?Ruga8>HQxRr87QU9_;AKArP7X6+<7gt*c|I%xEMObs2%o}GBy{B} zc$t%7x5+GVbJ^Su5-}(hXe*CMsCKYf0h~VwaO&oAxmF-cR61`@gMyc~nxgf;0IS$n zgEyGS<>z!>NUJZCSovG4)nl?lW!2kOdg&yvE|Ah7>&5+GZZ}cAsp2}0{{I4)%FEc7 z5T_0T995Hmu!X>I}yd|+Q{ro}0FUM0FyxVbrapP0m zYBDIUw$1urzaHwut8QvXh1T0mOd_$xXJn&ch1NyEuRK_7>(E+LeR))iHkk^{*p~oP zfeUwYT45;)c9p4#3$)p^+$H;J)!=Ue%#Zn8N>{2_<2*F&szaAp|IUMKchZVRaRw~2 zOj?_Ppymh#Fh4HeAnM(cj)-($6$hKnc@&eyMo+un2$qlEhJe}kAk|7@V7Do#ben)G zx6u+0KZs_}5!e(p9S4)=;7s-cyjWm{7Yi5Rxx6_F;A`E!>Tv(XdRM$%>5NO2?zmX& ziVNv>0!HYMSYjfwbqT?~F$6Q&ls zZGI@>!i7p}yqGf|2SP?*gR>Df3%old_I)z&E}Tu8pq}I3_|4lx&(8y#VkRt z`x9!P-FMkYxGo(9XPZfIcAf(}`P4KV1od2QH2RG6_++ycXXxo z)&MiN#00TEW58hvEb!n~6Pc>n-=PGOu{7N;0p=>%T!*}YilM|6Ei)ZRvOpZaGF7QB z6M9B!?~5DQPqNRIyy(3wy6!;pA$HFjQ1eV1pX^q%lK4Y)p*IH?bM7f29iDw=+^b6~ zHOy39KF{Z9scDzlm4kc13ffYMIZFcIUlF2InAH|htI@$ovsCH4=Y&S?bt11Df-%yb-0 zF~QS$3-LmML5bg}w!-CRC%oI@hPSHiWjQJpz9hEv&2%fg9&3&_;+ElJf+gM#GZz(J zjQ4}haXr);SACb^BQJA&>MoYp#}+?^xZ%%ee;^?QND2coB7u}p`8e3{IfBjSV`U}F z^C{r~0S|M=7e1C^u}$%=(|q+kKMS|P)!@ar8ZH3G+u(~#7u+fmV9R|~udM5(67yEd z$8DkbWgtPp`^!KqzUd3amtB6i+3t-`+uZQ3{C?*O7UNXXH0<>K7dDFe?)JMIN5V&{ zbvD@;&j`FvWiP>=)I}JGGeu+YToigtQ}Bk`Jq~Znv2d~&3rAZ~VE6g3moUPbV?ZVL z<-Tu}+*GsISE?i{1%yZWcvd$(&4msis5a*;iq2rJquH<@*nDg({I^ilQ6 z2p7dA#8lsyx+DaZM8G>Y5Jq~KS;KyV`tocd4;XhH8$kc*&1#mZ2AHz1T9XV;A?MPC z;(GC8t;k%o%ep5P)tHYN3lm`4W#=3ZHC)Oi*HZz^*)$wsK?SCTW_Fr;z)W&+-_+Zh=whGmLmH_i6 z!RZdA>&sBwBQVP!Dlt0UWC^~g<05fp}`TCSGuab;mZ|FW-U|0yx&f> zRQ;RZjkm^yki~dgtnJ%g^KivyDL(X+xYlDCKKHR!0RK124~P*^;{@8&2m`2GNrW{R zf2!ww`zMLkxS1{ro$HJ*3*7`> z4}4zbjn8WR@Kt*leiHTl@8&f8zBvg$iGBXIKU|bIKqax>tFpptdGi&d2g62Ullwog z(fv;B4;_sY@zZcRc_yC85cSPnio;pvD(>xwoQEQTH^z1>{FaY_ugye*b+(xTJG<$y z73=I6Yz_MuC%C5hiu%fXVil8`4JzKOKgoSny1_mpfVr5=)r$aAd6~fCV*+fbyEe3Crt<1|mrJWJ zRhQs)omr>KD>-+J7P{dO&)gH{E|tw@AxGKPh9Q8tWyEAvWOY1Z1!Sr(?Q>9JnA#IJ z;vSe`H4Pna;z45qy!y-*HBFo|P1OuxSv)4iePGq_TK;H51B?f)^l+eccABsc`-0JHsUNNbe(T5dtHtZY4OkS>vGTw|ZRS>`B zyAZESJot|50$lgC#8-j#_%_%DKZS}#juc2^gYbu_?QfC3qRIgVpko6K07r{`P7K6< zqCD`mzm3GcrV8H65?g*K*7Va53w#-GhcD6{6u4jKyW)!iSKKI+RW1P62jEt#*yL4l z_<4OIe%+X?;QfA0JK}F&4%L^8O<*rt-tqv7i8dULgm0at0 zwx~wa9J16q2(X%Mdc0p9P*!`vgUKlkuwKI<*2yic#>`gJLNf!+q!lf+ccGtJK1Kcr zsR~D2Te4wKc3r_x$4NG=Q}M6B%D6X=V?+gFUfGhCgU%8LvXQl|6xocAcQaF6r=aI) zB&sa|ZroY>e*v7aJ_lj#G3uxpx0qOwRoIt@3c35`)+8Y^&J}JBCYU?%Zp;)FUNYu3 zn90>?!W~Gno`OQR=_qlXgldW9nmi_}KGJ*R=Zjr5$FU4kRp)yoa~7U0U5wZ2tndcw zrR!GU?HYTWFSEwQGF!Z!vkWgJEx>cpbMZpBiGujD#KV^(%<-DbOkDI>h!1_1<3@lj zzVNfhtw1MHVh{Wp;e+o3J@8$CyTrwwqRjrvUNa8{U6(ny z;%%zp-p`_~@I|sgb#LZ5;%~rLyNEri*$9sACxvBbWmL25QDhbt$xr5Glp59F&Xu^#xXwvaep zRN`K>g(UE1Y|2ychIYy?*PDXmP5H>&QHuP1)$*UaQk7ovG*Yv?hE@T1rTju^ zSKJD4!4IL{_%7I8RM}G{v3`y4#y3HZD(UrwzcsFVFT)l0g?Ptp4&L*ghmS(d@L8-4 zzDjk(*EufuG}{i>a%^!W#~N4iY;mpJ1veUf@pX5YsBH{xbOhmsfctT~pDGLeq}@mL zxW3Zhg4c_d;rY~AI2b$IK-+F{5Z zvox~#X^A1vt^zwpvWR0iz|2Gw;sS|@X`vI=6)6j?|1Gn{R7@S$YO6e4g}L-$0|HEV zb>>;`|0?@@z)!K0U7_UwS#kYu6XiZQDuNey$ zZ8J1?SH}%Sd8xv@rmV|CNJl&ZTVoW&38JR7(8UMqB@Qw)hiAhvCagN-hQk<`Ni05Y z*j=YiCvn^;0p`zOl~(EI3X}@Ow9)0=c_@%OUs9?stjs3Bc+~A!Fz)YIBsSVqAaFn`iSzMd>;k+Rvk-4YEyDTG#VRxXez+Ar3bVndVfN~T(ienO z)_0*EYW)!IgeG9=e;4g-sX!BT0PbJu-yw+<$77{|LesTcs@%) zgqVrg7w{<7yWfj-9*<&o#4MZ;V9yk};begu_RHh7F-uYBKO0GQ>5oOSYl!lQNvIt<%BVxiSJ-xFO5 zVAUR0LL(gm*FDv(Vq_L`qzb@^YYUMqVG)y9-22s~j2b$vw7Skg14($PvRZLzorlBB z=d|V4aWfT|s!Mpg&#qI8F)tHWgqMjchM_#*&nxpv&4+r2a!+it!*juem&Xb7`^8a; z`(WHlwPoBJQ5uc7>O>?rq#>;-6Gm;~znfXOA>V*KdFr<{DS$ZvjU53x4)mbsFgxh2 zQV_Gt9#`wpPPKHrOGx#>2EwZk^=mcO0P9mq91g{Hpg|R(@D~-15RfBlA`vb&nlo;K zszZ^`k*olw0#kjt*PFGaK*7r^w}1$6P+Kg5I^z)~K&S5(i+xaT5(nxHgTc3!D+MOB z+y~~B5OW)e=YzA>iG^;BR#u&dz1g9#O8&D6a6v~V3Rk8eHOC*05}VDP^iRwiXNZLt ziW=KI{&#rIx(5kXk1H!(>M{wnE>qASYJz>K%Wzn%@v%H}oG3C=T{=#eo8e5^Vx`0{ zRxZKI73O%WTH@kb2VAJK#~US{=OK}VFV#FN06fzfYge+1JzZ+_SD}l=e zNK1SaC?E!lJq~rkO#${+gbQxPxZ#TkXM7xNjcWlGxE8z&9|Q~h5>Hi!TC~(5HB1e2!Y5?~}qc=Wn_EA=u@LucmQP5s#=bR@Gyx-!E%PU>*R;3MI z$}_{+w7EDMIS~gVCSX^{B`%3po0cQ?Wx8T}rV|?cO^|3g3T|`$i3Q`0 zFmwEESTOGqEVF(Biv(OV*Euj_;wo|lyfY-6$_YkfT`JPLi%~3sQgeW(*$l_~7+bcS zW>%L~fmBaJBRZB(Qh}RK^(l)@CC=Yni}X$9Y7t;%qX*+)RW3QWb)>V-!xdEntYcp) zF0TZbHP9*peSmFs{@*pv8eoD;fEn*<v#EdAB*wmh1;R;w2qMd3kW`yOWgq26rVOOg>K7T7Zl_hB&xjSA{yfpcR+^ zcMbyF!q+Ns5@YSKrWjR~#l#r_=9mngOv-LaRsFKk3WMS2w-iekjKo|4c(y3;qESXL zA7cdD$$v+X=|f0gF$sl^lThU}8J+$!u`|&eNAtx-maM>;3LDjG=ZpY*RuuU8%B6T- zV18Z{`CP57%1pl_U|bSA{dT1T&KFwYh2+IJ8#x=N{EhK+z-+wWXMz{KX5%HdnRvz3 z1aG*{!v(R`@A{kLgCGlhEb4qM%tG1ixBTX-T4~zvOJR%fcK9N^5j-DngwDmK1i2B& z&4tSQS-Bf-)Vkx71`ph9^H(+7jBis9xurY}Qw5iRbf{ zsO<9bcw?MOo`a`S%y1}qDfYI{}W<{svQlciLzpOnLk^ z%$`32rZ&?s*WMV*{mft$Vgc7=H}xi^RZMOd_p`nR)q6Y9bcAu^8WmrvZ0_lPb-b13 zle)B*bGr=No8lDD^eYvnm8R-4c4lcMPbU#VCZ7nZuCf}w-#Z*&LOcAR0k5>s1ej{f zuhU5@U3EokOkfEvZL_k;$Ny4XEu?9330>rW~I!u*@pHX^@b*3gn-=+j{P-C zRL8iCd-btGF5NFkcLrod77ICepx&pz7p`e8@XYdnPp-EDI7H&%5K-Xx_N1YTEZS$S zz)F2LWFxvi1<|Y05wR*2!K;!H)R%B1|Uy4YLK_#p7>>*;pf3 zPP!c)CjUgt^0CNuoGdo_3AFmo#OAmqcrw=t#|mt4veX8rD=cL#Q)h->tdn?G7OnL8 z`V|U}cbeVst|;8QO{~;%!K?XJcs^+nPKM6J>451t>1~V?Zj*7s;c=X>9gkCX6L3Zr zfqvC@qVt2Qr#N@?^jI$__8ek zH#-9GRZj@MS{16`{j@&-*H`)Boeo#L*<`O4x4_=0v%~8w*eEu~vpI`!Hq#WR(-){@ z)rm|C97?yr?$j07l5CH42@dFvTY*CNIq;nGZZjLDL%qe$b>WqJ3rs-(RvY3Q9 zth$oFty>D=4OG=d+&0PT6F0QELEi9&lHRVT22}EgiPSFkV&e&+Lxzs21Xy>IWG0#i z8VE4$v_65vdSiWjk04TYhr>5qg*B}FS{)Z_tE~$;2r$1+tFN}v+!~_N>KIovCB~%y z09EHnL_t)?yTbvd3UjHgSpk^4KU7^V=Ay_O0j2`S*Ci|U^-S|YU|xt)-@L9u6!w-N zzq?2*w0z@E`J*t~tQt+zuE;;8eO7~nH`J)!JXNgI++2-vu@KcfowVNorp`v|?i-A6 zsj#{amLBRo{NO+T9iQiRQ9z+TT#_8&lyTz zZ!H`GII=$tQESo>B9DE$M2N5Fyy zkY+Ou6>i385z9Cr@#_wWgZE`E#}QG~r^>Aq#Lw19 zh+DnY=#{r?obX1GHD1m%m$=vzXT#?xH9p}v2`8K<84lfBkHT?>F?h;p9L~B;!dcI$ zct${f#&0T~3!aIWqvzmU!eU&=ScdoWZShfst2$Bib+ar1_XoDd=?lZno?zAAitv8a zW0-nmp95oEuV;rpFVR^6Vy69SolX;A= zdhjHX?pR4kSp`L~beZGuyW$E%2zfu|prXG5m8&aJ)>rlyz#R9$ zZt)zxWU$hPkq*2I5aN`qK76EXk7!X5B^iJ2|csT zVdbG>ot)s1Xs?twxGWe6O$mr=iB|xp^rS0ms}J$(_&0HVmI64eHyPn;(-FEl72ch( z@ac*}gs89vxMoiss^#_ZAOkD0SW?+65EI~9d5!=RTuw?P$SM6<3SefhiwAO%-=2cB zst}~)dLSfp36?E-SX9^u^T!!sp4dS`yi}~AlRzA~_mnCnuTH?DN};YypY8iMw2jXN%uvYETU z0Hi=$znmnobJPMn8#D`Nyr+sHPrz}PF*xQiN)&n$o((Y;Xy@Uz1X10zCAg4nfe%WZ z@M*1=C~XkF?~TH519A9yHQUNY;^*~o_;ziS8oYL6RS-Vz3&6ErA6#GShfg;K;iCaR zyx-@6%iXRD;ByVOc%{M$&lO56pRrKEdmwQxcE-=cR*8K##x2MC1Us}xSR%(^7J?Rz zQeno7hm6#@YU7D_VA}M1F>T>Un23Fz<2)PYq2>q_hm+Wps-|XS^yVR@GYf?Sl}ed7 z2#mD|y{87$;*@(%yYCV>S`W0z|5c~j*30<}D)l|kp#Wx|XhO@~9$uM` zDqUNNxY{J8yl%;!DmOs;T-aHN>b2Es5n>}|mI*QcQ0i{SJ>NryVZ$78NFT0RUMlr9 zbe!illI}K1XjxfBVD*GI+G9e@j|nlAcsRg%ONqmlqidqncu40&2L&+WV6DK(-DwJ9 z#=(q#RWE3PH*G@>;ssIy9NLqll2?S5@N!Xs6F1~4fYkwid5u)vZf4HqKeOn|djXQD`M90grz$f}D#R5X^J47Hw!(=@M?Br&f~UoTvTxQoi7hX7d#Uz2?{s>} z@*P_5^#tJRst|lEcJ{g`Bc~W%Yw=YL0p2T@ey{*Zw=M>s4@Afc$Bnfi zxVG9KAFmI;T6r4(4p zxvF(OzzlP@Qeq~x_>GwjqkSg4RXYsuGWOsfjbj@1xJE8^=i@bRW0}N~WvXEq$5vGB z;uuClmwGNXyylG{XLSK$>SI*=>zu-}ITsZJN7Y8DagnJ4aC%>+D0Qx~(OjGX%<8H5 z{#3;FC9C*1c2h2*#Zvos#lx@D0C3X!JT+pJ-8ag&Rj5pK&k+ufs#5^7n1cW_=1mu? z99R{G;F<^(cefmFMNXfnz?y92cZxF9MlQ{DEdIBslZ;z;}toOV9a8(xPCwCE$Gf?9xSVf;_X=edTjSlb75Jdq z5!dPrgT%k=3Q$#3KdmFEiTG`65`N#7jNf)7L_S6sPX1tH8Pd2^Fq5b&aJe^Yjw6m0H4iUf}`mRu}|#t&ZH$+8@m|I!Shkz zIve3;V_`MvADAxSP8EQsic^_6^=?d=b`QoI-wWgAkE=rpZfV|#t&3Ir?p0f=QNOc6 zbqr1I%0%v(VpU)?6yk~c65v%&twqn_KD6zXH~tO@$9RsG+2y@d#6~stg#DXS1`3cY zVNf>b0q-z0eb=XiRJM4qv{A>vgp|sv3paGphCVBx?C@YAhM}amLmjgv!2FnC@@r^| z4UOBIRg*eCCSSS`s+}3=!CAp8_Zz@v$f}pN&}9Qvh^ zDC#Owv9J-l&ohI^c`)p-LBRNp8}_xTYt6O>)NU6Gyqg1zn^gA#-H1!u3w=jodlL2- zD%)yM?4k|g)i>9wJ8A9qmGa_R)QnUn&gyqIp=fOxBB~1J5ij;Rc|#u3c9klKnTh6h5{o)wS7)dZqw1L9 z?iw}nvGr)X{GsKK_M{;LUBl6la-+$WHxPfWd$xyi1gnmBqjEzDN(AE4u5{!z#-Owz z4n@^r2#d0S`O-%*W9sdgD)7#kd>dvmCYHw*Vy%5=-ib`BQK<8rjsa2NT{)Ht;1^om z@ob|lo@+42xpslB&jDBaT=8+QFFxxHRK*gUBmCv6F#NbN9zShL#y6spxB6r8jH6QAOIh%3B-r}LAWTd z{jGXOH7@m~GHX0nv>YdM%&<3Q9=62J6?je29&UtySJPmFmWxzrI9`JkjwZ7a?AA zdc9a@4!btIpDMQV2wwdVWQ2Pi_g1K^Gvnc~u0%x1=V_mrg=PT=Yp7QqVbKW-P+Cy8 z&#(tvwNswkA~t`Z1RP?h?y;sMHQ;>g^T4FEv@=ya0Tq-yWZ? z^TgL{gYcaw?ziiraBFP@z8#1b_!4EMh`o)&cWV>WRD?gbW~mXO1o?+G3F?5sj}p)R zx>2qhV%7KH`a#t6m(8)Fv~l=jR|0<9Bk=Ev$FF-ug$3GQ4yEJQquKagK>l=NC_Y~w zito2`%3uOM-x`Ul1A%yNO#t2%EB$VtpNh%f5%=(Jt1m7!c?rOFIK_hr>89A8Fb``Z z7oaIh$78;;3GAXA5K$h7 zT(N`oTN}{2rx~40D(y3jfhD|jaugJ;dYva$qxbYW^vP=3&z+J61u#{Pz!6+SZE?Hm z5SiR7AyjuZ;(M|c$b^`&?eKyPf~(K$Y89s98v0~4sigsK;Z#8Zk7oorkFJ#)&MFmV z63*rW27tNvb!u?gpf%Quz%oh9Todz5t$RDwBEa0LCBR(dqs8A<~RsKtj8j@GZ3ej9|V|ofIeTR+RMZu6zy!3m{;P- zjn(QT1xv#CqFLFc?<%!hR;p|B?sf$+uTcBK`oCgPxux1BfKIE@uGT} zooQ<2uF6(QOciFESoV8PUX`ZmpR@K=Du8LHxi8GErPTGg>PR8sWj|?-Tp`3%VwOxY zJFOt5&6odN{yYL4kmrla^<}77Bk;=IyIx}C@~#Y2v?igvF#*|fa|w&FhK0@Jm@@fx zOyIueAi&%kUjCTG!qfhSjO8QI5heaU=GU9T z)QFOwX>GS9;s>#{KkZDxPdn1_{nivw&lEMM7yAo=Cri}z&wT~JzEZ=-hsuBh#lW6i z{C8J2{@g8JyC++|Zg3sS0}kc_hx72~;avRoWHx@2>rV%AaC38vC~y?M-zGPKUCH=- zON;;MuqZ;UQ)?B#RAKE5Sq4mtO5o@SX=S+xH2XEP7d%*up^7@>btrc^3}BWvQd!lRfFo-RO8{=$->YCHz|0?Mi1}F8RSeH8 z^K<(1+^*FClLx}9)t8VatxQw!IwyOm+UKl}JQS`fRnKt|U@oH)4gfpA)*#L5cp$TS zYUzrr1N`C5%08L4Z#=qR6r21xy`(Z|zeb;o;MV2F~^2C#C6~G)v z!ki5gLAoWYPA2i=+zsVO>n~7NI=kK zN{IC_+m&q~$N<6Rl@^*@X!YgfOqNbEN5B%ujqh^&u?V}=JT;&&zpCJWKYD!dAnr(5Y z!aN?B(U+-O#~@tw%9eiY4tv#>4G5=Sc>aHzyw1aThDR4l>yW?O;S z0asRe;}cQgTYHj~t^IC)8ou2pARoxU4^L*|r-OO;;E0&AOF&xqxG+5j;DrVdJBgzGZ#ZEog!fn!KDgw#dT+?$qKa8 zY~DyU)?H~-!Nvi$mEsm-n_7fd158kx_V=hoc-0{8y@suz|Am+UGwvO(x|#_-rfnv; zyfUPuB2#?{CAW`w)|ZPQQ;i8Mx0VPoAJa}VCN?VAQ#%AO7ptt)uqU}IX$wt=2{4~y z(x71TUp;|J!h=K&Zr`v2`(ssf|X$%6Qe7jt~=G4k{~HFP35}4>nXNfW!Mz;NKQAbg+RInmw|b zPj;z^4uqE$y5mHb0+=dH1+J4jF)edib26eT!r_R!7ag?tU6&Ka7T9c1TiNkXm zVv$-Ni1dnJ#N~LvC)gZjW@9jY(*MEKk+-SslDQ8X!CXML6-5e|`FE5!Pr&-*Wg?2M z*k5FUJz4WrUG%Gs_Bhw#fJ=RzxVj+}U+hlAHwV(x`g&gqzS)Sn^RYyJz^;e z+@~Ydd=!>Sj637u+cElIMi_aI5gvWy|6ugkdogO_Ll`slVT?0=7_*m6ghTK$L>C02 zs4E-I%r5W!OL>_fts_M2n6ar`HLT-y5UZ+qP`>T#S_LtCnDc~>0#<~B`>Ir2DzCP_ z6c;9-Vw(c~H%DXBxdUp#Q7D!v}U#p$%WtqtsV#$!d^? z)c1e^-r)dKiK)J{%7oS>#Z#R;N@>a%vd>Ij5n?_jye#xk3e0nrOk&YSQ-K-l>KK^t z(n8np{NoNLscMD*X3CuaGY&4>)vRnZlUR&{hi07$yu7mzWDPLkRi0k}W+E%SCl|q$ zkz%7=U>R)-`$Px$N-P>KK+#GQ;^bDb(h`djU=~?$;vied7VoWA08@2&sDXzYxVVKx zxK|!;SBnX(=Hu*|$paV$pqb2Sl;5k0c1tW4e-10dn9wQ`Xe-tit5q*=s@e_3s;DEi zIvlB$;ts^-`$;TpwPHGEPQMS6N8f=dqQ0}lLi4!c;s*>$oM8TtY9F&b+XlOGt*|@W z3@3`M@N%6qUT=2B+g)CGZy*StZHvRLy{QV^ulJ;=#{~GNP%1;S_R6uiHQ z8vk*&0e?Q*47}VAywGQW_~~{5Sw8m^ZMDFAq{aa8QTew=oqC0`TTs z+}f0hZ?ohZ@N9YBA21iV-^CvS!~0~#m0C1 zn5|j$;RD9MRAY`r;Ud8NI9*;3pX1jkTP^>NHvoOKkDY3`{!-V~YIPp$S7+vjgUhQ1 zT0tt{5@c8em%J{B;HLGYrT7*vO6vECNjB)rTA6BtFg& zd(AdHRN&U9y3lr#V)i;zWo2~(77QAF_foes|RVQz%`fU{`6$_s( zad2`?1ma5k;h*3DNB4y=nL83sJbo9ZjJzE)M~VuMFv1+Ml*{DmZ*n(s9VenYb_q78 zTVO}lavaLH!Sj_aI7fTh>V-?a0l2;?24C$IfCb!J0`Rv2GA;BE2Xpc3u_94nf$xkc z=;<2#db;{AfL|4OUzGKHj{)GbqROX5flm-ziHna5#7C=@`r<&j09**{E|OJ(AGT!U z%k`-OuEf8a({N)$JU$r^S^J-Z$q&u>(gTJ3%a{4B7O`(D~OWp%iW5Tbtr3RB%8e#&flUU3` zcO5c7Y>10{)Z$~kwM1C8@@kNWD=?LpwwZ2}@RAF{N@!`F2{2<}!pdY70UmCl2{4se zC9(825|@g91>iRBlL^Es1I?`5b{>GQRu)>1pip(CPj;%q)@(S&EVNEyF^x`zCBQm? z#jAd-0anMRHq@x*UCtRkSQNArrh#T~OmtQNQ-u>76OrAUtqvIE$l{>xvMrUU-d(Q} zRjHc_)YcJ`SKL10;vO)&b+8ehR$(q?qp7|M=wlsdl{b@`i6rpyIHPx-w`wxLgc&Qi z*p7##m(}vdVryC|az;}EqDz7hEH@d?Fl$)Y&A@cy2QX3M;3r1ht~z(jf7lQYGb^k(kycL}xs64DsfSGwFz=WE6 zz`Rljsls~XC_z>Lmp*CeZ^bEphLc8rC9(82k}BemJ0%<7sXEUihW@rZXib0#FSF1p z;Fmj=-bT{EGULcKJfTIv2{DgS5nz{0Un~h(rj}EZtNb#dh^iL#mH0GE-T;I+y(bIV z{dp)6TU)iO4!PUQR68BQ%jzq3r%d0NZ)kG2%h1)5TS@HB!3l?qg9$M0H1~vS*riau zAFIE3HrYb}=5a<^b!F2<>3JA}1tnGT24sOprda7D`TEGBAcUrS!O3qa<}4hIaby0D zF%k!J9LsF6((`%nU*6=-Puzhxi!o>jUW84l_Sl~8hRvYRD!(Rbxw|$PaRWg(-yv~tlP6A=JK$iRwJJy{ z_n3mv#rMHt%59h~zt>p#y++7&l=pV03FLd@CamSQ~@*PPu{iCQ+zJpn4E|t_eY^dwhr#QHn zz+$&R+GkF8pzTm`15Y$0~m4MW?G_{i2Q<1eQS1EAq-j%9YgH=~lT{gEP&|GY4$4L#WpVs3@X{DLeQXn5U zSZPjZXB(ZUhFI0w*1f<7Ud8@~k)xsrA2oOymK)v_gd+)vXh5!jsL>m~&%)l_kneljBW^5}qxP*I^m+@F+>$nOt2)bOGFtq+-!>cwy0ZNvY0QmN^p&u{19$E$hx6S;nWIRn3cHC^_H z6udVWtG@eK_CDl=*&-rPe0*VEAGs$tH!<&xuy?n?R`WJ&aks}NpPkqe=mt|*Fpeai zK~z}+YDa4Ys+3+i`HLLLqC9*Jq`@1cJbORaWB5(~E zP4JS14e%O3X0tsH;huYC31ktx>jAtT$QsC6^LM$%jcGT;gpp37WHySMc%?*U^qKxI z+XriB8Wo^qVfw|C%w%7Fk4#Mf(>GQCvl@#zU>0)_yjrlqosrd8vdghThF`0+{^*wa z9!4J|bHJTXCY6DCJcJQ`SN3IN^12EW%4tJV8-2zE@X+HKJu)J*uLw~!2@2xq>R6Ey znQ}w93gT)3ah-fH_1BwJLQlS!kqzaVg0@!VG9@z~lX(eXMxf`vwyblb31G7D;@4Mj z?bBNdWTux0WVS)lctS){e-={vvXCtPEyvsP#6lhhJ}5pghusmt^p|-ETC+&C0`>{_ z{F7T`) zj>nn#8hzhYW#E}_+|eKw{b0^sR`n+;&?wqyUhPH6e4Vmy-c%(DXR1|B6V%NA5}-z^ z$QVNRyAAqAG+N_33}ijy@jG*RrLi-LWsw9h%P1+IX$0%lO&tzZDyCQH2YF^3AMjn_~evd4yl~~;`CbW=){x1P+nRg{D`pOb{W`2%A zto5aoeDjQ5jO@#ipKTA=O2|1=mQ%(V#3FzRR?CEsN!>wclnYv!#U1VK>*51Xp>e4P zg<~~HY);47l1sQy8l_bzNlhs#r7NeaP%ofq74R^i#t)uTLG)ZnAC zPX%K#H3BR7@pvQ*^{%)IAGasr+x?mNlbI6y<;{BhY^4@ITPnx@x>=!Rl)rvMz$gAH ztF6?3r9b<@u(0Y7{^3<0{y~8J>ko(UPah57Hv;lsebA0yzt@IezSn}^eyL4COZ&%n z#kYR55`X=q6hC{tP}nyI|NTLcGVgbmvhaB#BKN6lC{dW8k& z#PwKwa|BB-rZF!Pc}A4ayf7)tbs2$X=_Dm|hrHJWFnf6k;_H%8>ykkbS*iH&g|!;K_1jMNb;uQEIHP%&Pk-y?tx z8BOLTfQ{(0A)~F@o{xa3Bralt_;&$3{&YhA>GT(sSoDGU#u&tPvvDBMz@2E!c$4$S^FVN8xUMA|4fA#>?6meA1DQ z?~fJZ&lYR&7uTxs)5Q|}e5nGzeIo4ptR4UQRy!bUjF%#-Uv%N0U-aOgUiK(}ejuJ7F7|fu-av|L6E;a+`YHJ!OI5rvQgRi+F(O~Eo}F> zz~gKX_9UG|c(G;k&1}yU2Cmd%n5$2Ru<&dMbMk(UKORt5gl-Af83S4UqHbF6^(u(j z&&w3j*hkkifVKGThq%TRlE`S5J!;ea+IhZYUl)zIG}8JgG(pVWkUoh5HPDEtxb|0DMuwV8@^kXI?y9>h#L#7|BYe!fOyQxpn{Y>FQd|ATmqP2CV)r3G^fmK z$Y-XO3<*8{;etLlf1wJgV+BZ`C_=2TVajMeiWaJMdL~(z;GLHH@43^B=4(yDz-^Ws z^;|Wo=PFSlizTaGn7}S*?>ShVX&=4B-><zb zpD>y~q)KM^@vwFu@iT3@DgN(`R%Kvz$X)+~hgkm3BbCkcec8*)mO!pMN?iij=)E;q zm=Wm2ULgS45Mx-7BeI(8O91nCXa>_7V`SGi@9VgowIUEQ@A_&i%4bV87A5kEvhVs* z%JpVMp^c6s!|4A~N>iEAl_ZfdZA9kfTIW2ln(2lwiZ{r(1T9lZ>j7*SrL@)bS8rhI z6OJ6=ATP^--W-`jzl1v@0ZbWffQD%#hV3=a_Jq?V2ld#Cc9GC*zoP8mC=|*Bjl7A* zH*#G@5r^RqYkpW5c;pF(dd=t@q@3OooR&pE0J9n;ueU%XMU{?PVdF#Z!(n|L97x|O zzAyny$vngkBLO2pOy(sU)9UWEW9rM|cYSMB8CX+E0%8K1kLSL+q(Em0X69T8vZhNB z)1Qj?fh?qt=UEO~yWOUNEKcxfl||NYr5?SjJ(@da@T+{TLhH7RdvlRccmYSw1tK7X zi+vk>J+~@|y={fXHi{1^tbE472I>A@XgP5JqtVB(D1Pzn+|zhgc@ZDCB;vE~Wc*+# z6@NOHD@7e|{rhz*!+66~Fnpr5NLHo>(HwKfhn8 zeZ_x%BNv}vNyd+_rQ#>IvIV#q_|I39@ZDJf?&xK0b^P*JDqajGVW}7ihnVrR@>KH`V#v!de zOP%fWMi(gA*T%`X-u9p z+6mV;GH5v^$%b*Ax2|I9?Q81#(FoS{MgVIu$C}@}zAMQHVDT~6KJ&WtdkJ1z$#8{& zlKwEOtorYd>T`@dFt0{o2aV6OTKMdaPPw1_kPXe;i6%6tLwU-%S7crVuG~0%Pujo} z`J=LXT5@gTXs-&*KNwZ^HD>%6rIf~tH<}uz-k4RJd1?u8(N|{MAxHOc25#YSxh#N0 zT&RdaTtkW;;KV6~oKD|yvrp%9aloFYpPo$U5P?CSSFGbjdDO+Mf|=!^w7R>USP)-o z>D#NA`uM6!X&Tc>j7U#>B)|8iX&q5px?GL?d68hfNr>r5M#gA1@}^5rd9_m0NxiRk zYkG-|6&ws-yI`4PoikdfbFB$%vd-zO6WD#$7v92P9)4TkBL1-Jc8i?eB|zS7V}+9r zwn+9jqcJ=L?Gd{%8gmfW=<5&{$`GVMD68`jB z3VwW@BY9Hr`AQ!C+bxma0&aqtsi?oaRfeCgl;EdJ#rW}bhWw3K{K<40KEIZSKb%d) z_r(5SItA}`UcmE~c)Zb`j$3s}=tw<*#83|$bl(DRk^SyYHt_V^2~RI)c>21+?B@z+ z0@!04oc!!z-s=q0z8%;VZpO*fLx``wB#McfZl)~EbhWzKa*=^eS_s2=WNIQ&7e)Rq zGC0bH`6uH4F15;MT&q=ieUC;hEXbyUcR8~7rVh4K`*2lH&NM2bX5U#x&%-g4&om*n zH3MOV5jZM}CcZmY7`;o`SF5bJPDDWs$lB(4!n|@yhk}j0@&LF79c+yN9V16WnRXS`o-m}o~VPBvhT+nnmvy`PN-NLfx{w+iIj2yjA(7O%g@L1Y{O%0sKB#D|hy{cWC7IHmxe5?_h| zEiyCxUL%D>2|e~crS}Z#@3*4xYB}=eijh2=fwbXF04{W3&VM`3?-2t1EC!7NPX=w_upu{S4FIBr6)oi+B_S|ikc z6Ow{mk+sJel>(gZiy^q0c^r=`&*PoO3wXcfoSrH4Y0o))d+1=#U zuHP6=#>f5fcvK&Om5ON06~v$=_Bi7An6P`N09T~Gw@Cef0C%`~I>T82>?8no@^^uw z0NBA>B=asO_#8CD`;a4!#swj@<|4}avr#dcgTlU4@#!w(QpIVUDLy8jXA~-@%Fr#o z@T4fW1z|bXLTexJd!mS*O`&tS9d(qvE8RMKO+VXr7IhlsjZYup&Sy{6ZhiF;ZhZUz zD<9s+@|W(adgBP%)UE=YE{Vc{tdj`MKPP^0ww`GCMS+F@*mAJhlmfWtB@d~;qLqZK z;is`7hYU~f8UUu0Ryi%}j^HGC31X&|bc*CUz#7jnzDH9xk^!(a;2M4~ftu4-q$)GH zIYUnytXpVN;5Li3@HiR41C0az`o#D%GBf?+wyT}mmTAT5_5?54m*8cSWY8w z88o7?8jH2i+#z{VIsx2yZ&*RhTp4eayAXFO-l%Z{1hCQIPY@eHERDj+_c;rf0Pejz ztT&O-m8a>X$jVq;tcpWIQ<@&=#GBw7z|u5cIp&sWt!Wklyu(ANIQw@(&&%nO9lu?C zt;PpEdVgeR-XFV?j9O^=!s8!I3%D0`(j^(V=t>!~rt(oRTa1z`!m2#7Wu;!(ckoHS z4i)HLZO7QtAv9dA)}vrpTtZ`xn!&bo@xQ`Sj==Z08Rn4fFnMi+$(5jZ4FOK#4~sPQ z6BZ5>NqyYW2B%#%BG%UyMWOEKjoOEWtT5axJb`<~NAR>NOlMBLYC5Td;yGvfTVt{K z)J{pH_jHjr5b0P!3KbeW|2xz}6%Hg}iS;D+o_*#1`ey>rad3zkbBFf@bOEjJ} zU&O=OGnmf{Ma!iJm!NvXMAwE-~=iKyp00V%CTHzw#DLX;W6w_2!Kz7EB3_t;$+TIk%^Zzciwrm z850kNG52H=)3TOE9t?b>65oVeO<@Uu!ZpE*I!p-71YejC5${0SMKhZ zf;UqZ@~N_D?9V)fgW2IYT^^+joIhQQva79@_{`fpy-itCzLm+am`{hEh&^E_Y!yu?m(H5b@^4@BT#LLboyweqj54sZYk;wi}dNc6Vo^%a}eMS7xS2fXi zF0%Yt^<_K}iF>2q6lOCIV=!?y8svW@2HWGLY%GED8ay}2>yg*tZ`uYgvprngglTwd}sw)?HI z{h$pTj>txK!V!Be?LtIuIMN%FG>6_iTdM=~+pjdC{dy~g1k4PY(oLFw|GJKlC4g7H z_E6cF0i<40akX=;C>pN9#hN69q++ zyWuJlyjGC-txeP?flcOS1lkCGS&?lyU6Q^eYn^on z-5TriO1Z2cUOR@A%?zv+W#E9;D!W_9!%_L(l-{tBMq>FHqtSXy1oOd^(Uc!@a|El= zS;2vMY-pejJ|uvfg@LmXQFc-5qAyfjM*d)t_`jT5IHu2^dXstP8J)Ss4Tb&^hwSl) z4P!bXeH`W!+a3=nfLUp#zOsDZ7;Q@slZ7d%S*gXGGN)1|_N5`UF9W4>B8?YoP`My) z@YOm!D7^ncuhvNq-tR@*aDE(j9vh9w?BVpSB%2J_G~TY>VS93<34;#kH3W!~)vH^4f`3Y+&@!+!r}I2_mt z(_shriqE_^au*KA>_trJ1!T1)YZ|U@rdr=e*1Y%L?UT=U2tA_mI#&C1+-%*o7G3P| zjj0GPIFG~W$FV0l413Z};84z4oUM#QVrLF=N6L{oT8@-~B4mzLpyGPFvM_5ET4b~B z;t)mwF@0jKB9t{m<762&<`C5Mi)jV}8@B!wK`j}kZ%cVhQW1&F_0AI(s4OX)wH89m zDCFR}XPQWdwTo*=wOK0cOXh7_=}~LDV~Hts%6g}JNSN9C1qk>v5-RIbi~r6jszv#5+Z< zdOL20uY;JqjRnX8rUSx)CrxDJ4Y=rGg`|KD$lhyvFF@YzZVNjv2iOa69rwDyL4fOYz=VJ^0XUHsh8Wpo^E)%qJXek0 z8?C|$#fYi7fCGuaFrD0it^2I8?Z75DgxbP2bO#&{*<$BDTlpJy@Hp%QuTUp=pKw8F zd79$m_6LIT*3 z)MR5C0jvjaQ(p6QlC``r!K4!2rQaHOe3q`+S-6CHrQG zd@h}-NAt~IRkM8UytdpO)Zj3`ZoJX0elbBz_9lQCy}l^-ZAE~N2$Rz2fABiV+^_cZ z5_BS~#ZMtnXgvbB?#C0_ebgeLBKs1+WZ(hHkhN)k1Tq248!}`ycP0bC1hdYY7S z-zSJ^9I->dG97U=>x2S1yEhM=w|c=LW;%&bfKDS58*`C3##8gtR0oWdzs;jB<~6UV zX^9VckQH~+1@)PYqfaN^n^m8f$BWW<7#h!HDxE1q{ZfOlK_{B7H^_p@Mg2mJ&gWvU zaL+A<`)hT22brVmMmvV?^`q~$ywSoGtU$_d&qRDlBo1E;hG&Q@TK_HZ^4kWJd6TlS zgD|kk&Kj<^8{jPL=d?k--e8fk1lE4}`2msJ$K|=_1pMa()RAIQBFE!>HzCP)Gcp3U zA~RqMvV<9nb~~bMuNf8lJ>=?v;@xgY^L9Y8j}xNJ+Ys)s5eMaO22(0;u=wHbVjip* zbKZbmo_6pO&^kKXV25c7?7fABMK(L^F$w5IMj!Qre?$;YWSvA}eLQlzvvdeT8|%hz zcc5yr6z5CA;djXsF5wPp9;cl}N;|>rn1izL_8__Upv}VSTi_J31v^4E!Q+^tu(An0 zr`_Oxj@4YA@RrTVFVa`cAMWzle8vM_=Y8Q583^BtyATq;ABQr-5!0BCjDb>A%(n;# z`jxe~M!EP)1-J#X^(dTcKj-0xbkhS5rEu&9p{r&Zv}5U1UIeU01hiuU+VMAp73DFnQ^K+w+er|Q ziVi1;nSZ6vOR(}yq9oytMh52B1T|Th0M;m;Ffe7b;pFp5-+;hnSe*VYgH{~i&GEAQ zAKWkoB(U%d?btUQ+W-OYd>loemx4Q#M|#0Ir^? zP!MzE4vjus(@YHpZ*^;gb@)!7`g09a)hO>TL`r3}p1v1y!V3Y1T;Q?G4i2tX!f?XI z9$R5HiCh&HcHJ&O79e?SCmUL0_s-X_Uw|7dz&;>g4i&~cDlBqD}fR%t&%v&D&GKeO!-N(UN)qsf;HMAjr8(h4$ z!zI8O9%IGD5>d!qsroc_nn@Hr;$!+v{_<+gAL+91qs1KU7rZ1S~&O`u5bpx0m> zxB)i1H(|?uTi6~H*?!W5!02Ec$q3V)@x8WI1%IEH9{|4ahw=E2845*+lOaRmPHw^&uG4szn8kEx6NEvCr zu$ZOKeOd{M(wS?Xd0PgaD4i*NnLeUBP;*6;%VI04MCR8lbCPD4TK&x)1utDx0+{zn zADKsu68I6tm$2*d9#~BY$R^(q3Ck$*qY1t8YmWOQh?%?Piy?b5P(`qk-U;4D0aCkY z`fk}>df%AQE8oar`JJ)17S)*aX7XDLHXaEJ-=9Fwoe}h@iH{@tzE5Pr*dx|Q3xGxQ z*UvSI#7M)5>~OV96;a4&&l1@^hASdp`25O=Y6UyTbn?w(K!KY^+nrwR2c^^}BO4Vq zWMiIWNAPkmJuA*Esj?}ZlSSYTutJDK0^S%@KUYWi2)oeNZJ4i?*HtC{O9kp?D|P7I zu=r{NE1ha%cf~)JYqz}7?emQa-lDD?WY#4hrZ7S$GVTfYhIfb~oV;yd=k^*LManu0 zXj~jNz)8U5CRa}Zua5xAPmI-DWJ!W}uQ2g`0rJ5u7CTH(3*e8Fq2=dC1*Au;Er3!5mX_5doKc;daVJ!Fx38ATE@iQahU) zhT~}maU$UW_KV~X6d?PCxdE~fP42=Zl9~YCE>|TZ@-Dz z_jo8bXFy-q36;i-ES6YKKbH;Qfw+c`#Sea)(25~mdD{}`YCZ$j=_qcHGsSRF@dch zeqyO()>IF75^=66Pq;e}$ONtdULMBW%cg~=^o7a73kqNwZ)*IFIlZyoRaTd9!k(o} zjDRH5PskhDFZ#bhBt=F?7S0r$!?BFx2+uu(Nbx6Yrt9RjOzLy$W<}nL-@sYBM#l%E ze%<#6#qsY~0JEmmIOmlt%mMZUFj<&op^U1tScP-4INhW7QNO(AQ9a3F;9(zHmYdLg ztpV*fTC@kaW0`GT%_|{cm^Z%sGBRr8kyah6 z6oipJGxqfWLUpv>DXk!n_NdD>XmlF@2Lii0*AiFzp6wU?(uRDRu zG?9-Tocu-h?lr-ApGo{wKkU1-51}c?5h*S~Mw_t5P|+F#SD=`6;0*~loO}@O;+y+k z@WS!zP;r0G$U=>g%{Kw5jd6&nxPVLI>z++Jilb2>I4Dwh*Ex~pr_JgMQ%ZZBa6&+Y zC-%mM;7H0LoGm(w%T<@fUrZO!id3GfL*+sPYQ;_A>}RqpRiWX&5pV?Cu#gAIg#a#` zZcqSMiu|m^whR8BRDG?v%IrhUOdJ}bM1VFWL$rU+oN zXa7Ty9(PAovd}v3ak0-UV9C6F0%G>|jy+?-dqE%brg(ER&)NUT?u%Ir3y6pAj$?4u z5{ag7VC0957AiXWmn-8CmdTwa6#L>2i+^=l%g2oRQ2s7w3eo3ah@O53<+_gbd5is` zmd+`b$FW6>&EFisqG#8L|3eVZyqd?-$17SY$u>pS=T1DI&}o&7h&ImGsXXjiYQxBC zA4X+!pj;l6#{}-kN*4x~gpF^=ViLcXU_G4@iUX0ma5Q=^l0`k_u_2>1UH#$E%lqJc z$Q6!(Tj3}mHhbH`!_yX?UbgTN>FUjij?Nq5Ax!KglG#_T0d{MFCSl$H8;dERH&}pd z*lzhcM8LXd4Y+&B+`_nl@_T+;EU7Cmxtcaxa=rAwIfTyDZ42Dwf1CXs;Oc9yb<5=kRNi++Vj1uuiI90UaQc!BHkJYmgE5DMK-y$|l*@W4nQKU~GESWc{o?TjnC>^R25^9l+s8hkq&89`xb%RLl+T|X7-T9MsD4eJj zrO|+r$$Dh>6bmL2gwW38Y+fWTm&L)VPiT7dE|cmrxGFbrRbI>$*)`_nh0VzentVK? zrbmsgv1iAfVFfY|U>O8 zt^EJDEF*N8T_rjTWJ}FB_$OIj9)SM{!RM@;fq=nNu2Kky;AMNPbp5T88@BTG`B7H8Vl* z(mi5Smf+O@D$|NTcty{>bOx(bDaV27q)|`#9 z;cC=P(=BUL2By^KW<&5+%hy!`YQARObVNbqk5nPGEmvHWWKp8AxKI?OcBwQLRy;76 zr+rVzPO&HeTM`Ly?X7D9;47GUBs`&5EJLH)rHM8AY$KarZ(an#Le8N3&IDQ{e>SV*}&(^x= zjE+Q2%87tmnc@#mKIR|3>vT!NPC&M;%W^V%c#dq)&ofP1+pT3nGRQAL+wl`kI%&mHX& znkR_KzO=bF78GQY52n=!HkG{u8zrzXC;?0`voePAnqcl00CwJF`x6;*T5pETJOAc2 zwdp4dx*3e!ov=t~F@n|-)EZGx62R5sA7phE;-Wb0C$i5WB<7H|tocL)s{^0WkuOZ# zgs$a&G|si@L0R1GR3*skF_Pf~E+gbjE$}hH%ODp^LD_Z1B9(d0!4?pcc?n`p-nhm& z*ld>+0QcSQR@qEz5mv}+PZy~iql|kbIs|^lO&V2o6!6;nZBpQFGFf4>hX84x1NNTr z(|mAjegxu*E(s{YvHyG!JP$Yv>)C2=E&X9fcMB6b3#bSzv!{bL6L@$y!qZJ2yV}Fc z*$(cG+XTp4VA>{N++zK2W-DN|6<>P0NOLO*17S5&l^O)A zo2gM_^uBSrM%cFj4Rehe&}1uV<(w?+tF7uE61enJX#_9}E?5FN^}!9yeYE-ofV)JY z5u9YW`jviRwyrN2xMZ$HjdGPBCtLHm#+B-lZXwCQASc#c8;X)f`OL5B>lzHqH9x`c z$vWhEsT^_N8>@dTm|gtgALvvR$G;n`D!rRL&F0OuwDrWoj&Uh^Kev>feYg zjz(nuCACZWvLO`3z>0?l^Ze0rq3L$HK^`1T|99?nVc+}H3N+g2>Np1OjAH1X_^QIn zqXO=MdlSmEWM#t$#-3Y43UuCx!Ms=Bye=Dq_!N)lWuur9`N@t70qu%Np{0IwuMDC0 z)`(vDb={0ehu$*8R3#%qcAL=DQ`j3D3a>Lk*d299WLh+Gx_D-43mRrym67$loyV+% znbuv60X62QX#}qUX8KOdX|vXw%^FuZ157|X%_5L{eagJ-nBlzW-sN_5Uzay@v=oJ1 zS?ar;N-k}PV2ZQid5a5pG$$-u55+u-OYzzNt4Q}A~9A9mGT@WrffB$veq z6JN%el;b#bVK)Lpz2LFO6)t{ua1sV~_O=tVhpYEan0zcml%b5H;~c@ey}@D6cC&|z-`AM;lLhh98{nA}T9D@#IlO*&Fa;&4eA`9!pU_tY-< z9`=w8$^{;xj0omaX~zKvQh;0V~7>2eg0Fj!NDipf$<#W9M{Xg+i2oGaFRMU>3d zCbV8{Ub8l}wrlNZzuuv>zs!9yh(meFC@mw$U2iU+`)y0rl%5E}^o}s_ZuALY>0?r=_KHmH)F?0`ye(`HSc~nnuUcG05&^R&2D|bxfk}5# z-9TQ~0<#1zZQw5BI^#-2WM4*`3BKfp3=|JkplZBc*DC?sxX`XGwEUlB+~TQvwQ6DD zO4(c}os(O#5m%q4eJ*&JK98emr(rto3-|BMvJG zqU=jI;1y4YyJ6Wm#fM>#!zeD{)u?f$4@?H;U~%@P61-$!j+o`lD}uLkxlMDtC4G6a z>z&5I=-tY;EJU#Nv&L(}vbJ7U*ed1_uu*|*=POcm&vuyi*l8WKm-xE8hMe-j+2o^0 zDv44MN9SL_h0Iepdf}h~#y!LZ&f*6<`8le&_&BMtnZWEL^3=~$&CA;?65AE-W*3;< zoZ;c>1a}ulI6H{~5Fqb#u(g=5w4>8@oz-k_+NS2{L_+%gsMeI{B?>iHMfTP~ZWn=-vG=47=pHQxeGlvdo??u$B3*B z>BN>grczdVQ78Uy%VY5`pIFjAoQBF_5|q+R|In9ZX2in43=UZU%)nKXtl<_>eY_IT z4YKJIz$^kWKuiE@oJW4IOUQ_6Pa4%FRpNS=kHF^72wbYpj^#cjlG>SOq_*a0#F;K% zc1HoKCmK*Q*=Sj>0${p*+?X0f1rW4kV6ra3%=XB1QO5-DrOE{LcVnxPkXE08^oC4i f)TAM`G8z95!5~V|!+D--00000NkvXXu0mjfwQRPo literal 0 HcmV?d00001 diff --git a/src/components/VideoHeader.astro b/src/components/VideoHeader.astro index 3c0cabdef..1d3717414 100644 --- a/src/components/VideoHeader.astro +++ b/src/components/VideoHeader.astro @@ -1,40 +1,38 @@ --- import type { VideoFileType } from '../lib/video'; -import YouTubeAlert from "./YouTubeAlert.astro"; +import YouTubeAlert from './YouTubeAlert.astro'; export interface Props { video: VideoFileType; } const { video } = Astro.props; -const { frontmatter } = video; -const { author } = frontmatter; +const { frontmatter, author } = video; --- -

+
-

+

{frontmatter.title}

-
diff --git a/src/data/authors/ebrahim-bharmal.md b/src/data/authors/ebrahim-bharmal.md new file mode 100644 index 000000000..3763d5972 --- /dev/null +++ b/src/data/authors/ebrahim-bharmal.md @@ -0,0 +1,8 @@ +--- +name: 'Ebrahim Bharmal' +imageUrl: '/authors/ebrahimbharmal007.png' +social: + twitter: 'https://twitter.com/BharmalEbrahim' +--- + +Full-stack developer interested in all things web development. \ No newline at end of file diff --git a/src/data/authors/jesse-li.md b/src/data/authors/jesse-li.md new file mode 100644 index 000000000..4f7049c52 --- /dev/null +++ b/src/data/authors/jesse-li.md @@ -0,0 +1,9 @@ +--- +name: 'Jesse Li' +imageUrl: '/authors/jesse.png' +social: + twitter: 'https://twitter.com/__jesse_li' + github: 'https://github.com/veggiedefender' +--- + +Jesse has made several [interesting open-source projects](https://github.com/veggiedefender) and wrote some interesting [articles on his blog](https://blog.jse.li/) including the one he wrote on roadmap.sh. \ No newline at end of file diff --git a/src/data/authors/peter-thaleikis.md b/src/data/authors/peter-thaleikis.md new file mode 100644 index 000000000..635f41a86 --- /dev/null +++ b/src/data/authors/peter-thaleikis.md @@ -0,0 +1,9 @@ +--- +name: 'Peter Thaleikis' +imageUrl: '/authors/peter-thaleikis.png' +social: + twitter: 'https://twitter.com/spekulatius1984' + website: 'https://peterthaleikis.com/' +--- + +Peter Thaleikis a software engineer and business owner. He has been developing web applications since around 2000. Before he started his own software development company [Bring Your Own Ideas Ltd.](https://bringyourownideas.com/), he has been a Lead Developer for multiple organizations. \ No newline at end of file diff --git a/src/data/guides/proxy-servers.md b/src/data/guides/proxy-servers.md index de53315cf..560245872 100644 --- a/src/data/guides/proxy-servers.md +++ b/src/data/guides/proxy-servers.md @@ -1,10 +1,7 @@ --- title: 'Proxy Servers' description: 'How do proxy servers work and what are forward and reverse proxies?' -author: - name: 'Ebrahim Bharmal' - url: 'https://twitter.com/BharmalEbrahim' - imageUrl: '/authors/ebrahimbharmal007.png' +authorId: 'ebrahim-bharmal' seo: title: 'Proxy Servers - roadmap.sh' description: 'How do proxy servers work and what are forward and reverse proxies?' diff --git a/src/data/guides/torrent-client.md b/src/data/guides/torrent-client.md index 63af12295..536b57cae 100644 --- a/src/data/guides/torrent-client.md +++ b/src/data/guides/torrent-client.md @@ -1,10 +1,7 @@ --- title: 'Building a BitTorrent Client' description: 'Learn everything you need to know about BitTorrent by writing a client in Go' -author: - name: 'Jesse Li' - url: 'https://twitter.com/__jesse_li' - imageUrl: '/authors/jesse.png' +authorId: 'jesse-li' seo: title: 'Building a BitTorrent Client - roadmap.sh' description: 'Learn everything you need to know about BitTorrent by writing a client in Go' diff --git a/src/data/guides/why-build-it-and-they-will-come-wont-work-anymore.md b/src/data/guides/why-build-it-and-they-will-come-wont-work-anymore.md index 0d7a730c3..769343c6a 100644 --- a/src/data/guides/why-build-it-and-they-will-come-wont-work-anymore.md +++ b/src/data/guides/why-build-it-and-they-will-come-wont-work-anymore.md @@ -1,10 +1,7 @@ --- title: 'Build it and they will come?' description: 'Why “build it and they will come” alone won’t work anymore' -author: - name: 'Peter Thaleikis' - url: 'https://twitter.com/spekulatius1984' - imageUrl: '/authors/spekulatius.jpg' +authorId: 'peter-thaleikis' seo: title: 'Build it and they will come? - roadmap.sh' description: 'Why “build it and they will come” alone won’t work anymore' diff --git a/src/data/videos/acid-explained.md b/src/data/videos/acid-explained.md index cf9097b8e..280911d18 100644 --- a/src/data/videos/acid-explained.md +++ b/src/data/videos/acid-explained.md @@ -4,10 +4,7 @@ description: 'Learn what it means for a database to be ACID compliant with examp duration: '5 minutes' isNew: false date: 2021-09-26 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/all-about-http-caching.md b/src/data/videos/all-about-http-caching.md index e749426e2..16a48f554 100644 --- a/src/data/videos/all-about-http-caching.md +++ b/src/data/videos/all-about-http-caching.md @@ -4,10 +4,7 @@ description: 'Learn what is HTTP caching, places for caching and different cachi duration: '13 minutes' isNew: false date: 2020-10-04 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/array-structure.md b/src/data/videos/array-structure.md index 99f473e7b..127a19352 100644 --- a/src/data/videos/array-structure.md +++ b/src/data/videos/array-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about array data structure' duration: '10 minutes' isNew: false date: 2022-01-09 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/arrays-and-objects-in-javascript.md b/src/data/videos/arrays-and-objects-in-javascript.md index f67c620bd..979bb4831 100644 --- a/src/data/videos/arrays-and-objects-in-javascript.md +++ b/src/data/videos/arrays-and-objects-in-javascript.md @@ -4,10 +4,7 @@ description: 'Learn how to manipulate arrays and objects in JavaScript.' duration: '12 minutes' isNew: false date: 2020-05-09 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/async-javascript.md b/src/data/videos/async-javascript.md index 12a95903d..6622b9e22 100644 --- a/src/data/videos/async-javascript.md +++ b/src/data/videos/async-javascript.md @@ -4,10 +4,7 @@ description: 'Learn how to write asynchronous JavaScript using Async/Await' duration: '15 minutes' isNew: false date: 2021-11-14 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/basic-authentication.md b/src/data/videos/basic-authentication.md index 223461fc4..6c6adc45c 100644 --- a/src/data/videos/basic-authentication.md +++ b/src/data/videos/basic-authentication.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about basic authentication' duration: '5 minutes' isNew: false date: 2022-10-01 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/basics-of-authentication.md b/src/data/videos/basics-of-authentication.md index fd10bfc54..28e029f42 100644 --- a/src/data/videos/basics-of-authentication.md +++ b/src/data/videos/basics-of-authentication.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about authentication with this A duration: '5 minutes' isNew: false date: 2022-09-21 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/big-o-notation.md b/src/data/videos/big-o-notation.md index 60b2b1e0f..8509e7bdc 100644 --- a/src/data/videos/big-o-notation.md +++ b/src/data/videos/big-o-notation.md @@ -4,10 +4,7 @@ description: 'Learn what the Big-O notation is and how to calculate the time com duration: '8 minutes' isNew: false date: 2021-10-25 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/content-delivery-networks.md b/src/data/videos/content-delivery-networks.md index 6a47235e8..505e853ca 100644 --- a/src/data/videos/content-delivery-networks.md +++ b/src/data/videos/content-delivery-networks.md @@ -4,10 +4,7 @@ description: 'Learn what the CDNs are and the difference between push CDN vs pul duration: '4 minutes' isNew: false date: 2020-09-26 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/dns-explained.md b/src/data/videos/dns-explained.md index b3efdf901..6ad370ed0 100644 --- a/src/data/videos/dns-explained.md +++ b/src/data/videos/dns-explained.md @@ -4,10 +4,7 @@ description: 'Learn what the DNS is and how a website is found on the internet.' duration: '5 minutes' isNew: false date: 2020-08-17 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/dns-records.md b/src/data/videos/dns-records.md index 9f8aeaea3..3aa4cb4a9 100644 --- a/src/data/videos/dns-records.md +++ b/src/data/videos/dns-records.md @@ -4,10 +4,7 @@ description: 'Learn what the DNS is and how a website is found on the internet.' duration: '6 minutes' isNew: false date: 2020-08-31 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/floating-point-arithmetic.md b/src/data/videos/floating-point-arithmetic.md index 2aab972f1..d5e3601c4 100644 --- a/src/data/videos/floating-point-arithmetic.md +++ b/src/data/videos/floating-point-arithmetic.md @@ -4,10 +4,7 @@ description: 'Learn how ow the arithmetic operations work on floating-point numb duration: '4 minutes' isNew: false date: 2021-10-10 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/freeze-and-seal-objects-in-javascript.md b/src/data/videos/freeze-and-seal-objects-in-javascript.md index 397064de3..81d100cfe 100644 --- a/src/data/videos/freeze-and-seal-objects-in-javascript.md +++ b/src/data/videos/freeze-and-seal-objects-in-javascript.md @@ -4,10 +4,7 @@ description: 'Learn what is object freeze and seal in JavaScript and how to use duration: '6 minutes' isNew: false date: 2020-10-16 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/graph-data-structure.md b/src/data/videos/graph-data-structure.md index 571b235c8..71cd08360 100644 --- a/src/data/videos/graph-data-structure.md +++ b/src/data/videos/graph-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about the graph data structure' duration: '13 minutes' isNew: false date: 2022-09-08 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/hash-table-data-structure.md b/src/data/videos/hash-table-data-structure.md index 3b8547a43..6d590206c 100644 --- a/src/data/videos/hash-table-data-structure.md +++ b/src/data/videos/hash-table-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about the hash table data struct duration: '8 minutes' isNew: false date: 2022-02-21 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/heap-data-structure.md b/src/data/videos/heap-data-structure.md index e78066ad7..67467adba 100644 --- a/src/data/videos/heap-data-structure.md +++ b/src/data/videos/heap-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about the heap data structure' duration: '11 minutes' isNew: false date: 2022-08-24 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/how-to-use-css-variables.md b/src/data/videos/how-to-use-css-variables.md index e45ae8ef4..897c1ef0a 100644 --- a/src/data/videos/how-to-use-css-variables.md +++ b/src/data/videos/how-to-use-css-variables.md @@ -4,10 +4,7 @@ description: 'Learn how to write scalable CSS using CSS Variables.' duration: '5 minutes' isNew: false date: 2020-07-03 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/how-to-use-github-actions.md b/src/data/videos/how-to-use-github-actions.md index aac637de6..96ff1c68a 100644 --- a/src/data/videos/how-to-use-github-actions.md +++ b/src/data/videos/how-to-use-github-actions.md @@ -4,10 +4,7 @@ description: 'Learn how to implement CI/CD with GitHub Actions' duration: '6 minutes' isNew: false date: 2020-07-13 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/javascript-fetch-api.md b/src/data/videos/javascript-fetch-api.md index 92a548e80..1fdc57398 100644 --- a/src/data/videos/javascript-fetch-api.md +++ b/src/data/videos/javascript-fetch-api.md @@ -4,10 +4,7 @@ description: "Learn how to use JavaScript's Fetch API to interact with remote AP duration: '3 minutes' isNew: false date: 2020-08-02 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/linked-list-data-structure.md b/src/data/videos/linked-list-data-structure.md index a6284ff62..86b282c3d 100644 --- a/src/data/videos/linked-list-data-structure.md +++ b/src/data/videos/linked-list-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about linked list data structure duration: '11 minutes' isNew: false date: 2022-01-31 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/load-balancers-101.md b/src/data/videos/load-balancers-101.md index 171064787..d9a54b6da 100644 --- a/src/data/videos/load-balancers-101.md +++ b/src/data/videos/load-balancers-101.md @@ -4,10 +4,7 @@ description: 'Learn the basics of load balancers, types and different algorithms duration: '9 minutes' isNew: false date: 2020-09-18 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/osi-model.md b/src/data/videos/osi-model.md index a2fce9cb6..7ae8a3b85 100644 --- a/src/data/videos/osi-model.md +++ b/src/data/videos/osi-model.md @@ -4,10 +4,7 @@ description: 'Learn what is OSI Model and the different layers involved.' duration: '7 minutes' isNew: false date: 2020-10-24 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/practical-intro-to-react.md b/src/data/videos/practical-intro-to-react.md index 6c61c3485..d84b194dd 100644 --- a/src/data/videos/practical-intro-to-react.md +++ b/src/data/videos/practical-intro-to-react.md @@ -4,10 +4,7 @@ description: 'Learn how to create a React Application with practical example.' duration: '40 minutes' isNew: false date: 2020-07-09 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/promises-in-javascript.md b/src/data/videos/promises-in-javascript.md index 2a2bcca18..9b64540c6 100644 --- a/src/data/videos/promises-in-javascript.md +++ b/src/data/videos/promises-in-javascript.md @@ -4,10 +4,7 @@ description: 'Learn how to write asynchronous code in JavaScript using promises. duration: '8 minutes' isNew: false date: 2020-07-20 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/queue-data-structure.md b/src/data/videos/queue-data-structure.md index 7d7460aa2..0ad33608a 100644 --- a/src/data/videos/queue-data-structure.md +++ b/src/data/videos/queue-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about the queue data structure' duration: '4 minutes' isNew: false date: 2022-02-14 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/random-number-generators.md b/src/data/videos/random-number-generators.md index e680cf50f..cce5d9f99 100644 --- a/src/data/videos/random-number-generators.md +++ b/src/data/videos/random-number-generators.md @@ -4,10 +4,7 @@ description: 'How do random number generators work?' duration: '8 minutes' isNew: false date: 2021-11-03 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/scaling-the-unscalable.md b/src/data/videos/scaling-the-unscalable.md index 31705439b..8fff50164 100644 --- a/src/data/videos/scaling-the-unscalable.md +++ b/src/data/videos/scaling-the-unscalable.md @@ -4,10 +4,7 @@ description: 'Learn the basics of System Design and understand how to build a sc duration: '10 minutes' isNew: false date: 2020-07-26 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/session-based-authentication.md b/src/data/videos/session-based-authentication.md index 12838e055..318b0a2f9 100644 --- a/src/data/videos/session-based-authentication.md +++ b/src/data/videos/session-based-authentication.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about session authentication' duration: '2 minutes' isNew: false date: 2022-11-02 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/ssh-ssl-tls.md b/src/data/videos/ssh-ssl-tls.md index 9968a953c..12af0ffbc 100644 --- a/src/data/videos/ssh-ssl-tls.md +++ b/src/data/videos/ssh-ssl-tls.md @@ -4,10 +4,7 @@ description: 'Learn the difference between SSH, TLS and SSL' duration: '3 minutes' isNew: false date: 2021-11-25 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/stack-data-structure.md b/src/data/videos/stack-data-structure.md index 5b07d9483..c0114692f 100644 --- a/src/data/videos/stack-data-structure.md +++ b/src/data/videos/stack-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about the stack data structure' duration: '5 minutes' isNew: false date: 2022-02-07 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/system-design-101.md b/src/data/videos/system-design-101.md index 34483b9ac..90a1bf317 100644 --- a/src/data/videos/system-design-101.md +++ b/src/data/videos/system-design-101.md @@ -4,10 +4,7 @@ description: 'Learn about all the bits and pieces of system design.' duration: '7 minutes' isNew: false date: 2020-08-08 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/tcp-ip-model.md b/src/data/videos/tcp-ip-model.md index 08dd9a210..341fde8b1 100644 --- a/src/data/videos/tcp-ip-model.md +++ b/src/data/videos/tcp-ip-model.md @@ -4,10 +4,7 @@ description: 'Learn what is TCP/IP Model and the different layers involved.' duration: '5 minutes' isNew: false date: 2020-11-06 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/transport-protocols-tcp-vs-udp.md b/src/data/videos/transport-protocols-tcp-vs-udp.md index 928e408b1..53c6ea6e2 100644 --- a/src/data/videos/transport-protocols-tcp-vs-udp.md +++ b/src/data/videos/transport-protocols-tcp-vs-udp.md @@ -4,10 +4,7 @@ description: 'Learn about the Transport Layer of the TCP/IP model and different duration: '10 minutes' isNew: false date: 2020-11-21 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/tree-data-structure.md b/src/data/videos/tree-data-structure.md index 93e39d340..01ed0018e 100644 --- a/src/data/videos/tree-data-structure.md +++ b/src/data/videos/tree-data-structure.md @@ -4,10 +4,7 @@ description: 'Learn everything you need to know about the tree data structure' duration: '8 minutes' isNew: false date: 2022-08-11 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/what-are-data-structures.md b/src/data/videos/what-are-data-structures.md index 10b68816f..54bdffd0b 100644 --- a/src/data/videos/what-are-data-structures.md +++ b/src/data/videos/what-are-data-structures.md @@ -4,10 +4,7 @@ description: 'Learn about the different data structures in this illustrated seri duration: '1 minute' isNew: false date: 2021-12-12 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/what-is-cap-theorem.md b/src/data/videos/what-is-cap-theorem.md index da9966cf0..84693bb42 100644 --- a/src/data/videos/what-is-cap-theorem.md +++ b/src/data/videos/what-is-cap-theorem.md @@ -4,10 +4,7 @@ description: 'An illustrated explanation to CAP theorem with examples and proof. duration: '8 minutes' isNew: false date: 2021-10-05 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/what-is-dependency-injection.md b/src/data/videos/what-is-dependency-injection.md index d5c0a764a..2eb60772e 100644 --- a/src/data/videos/what-is-dependency-injection.md +++ b/src/data/videos/what-is-dependency-injection.md @@ -4,10 +4,7 @@ description: 'Learn what is dependency injection and how to write better code wi duration: '3 minutes' isNew: false date: 2020-07-04 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/what-is-dom-shadow-dom-virtual-dom.md b/src/data/videos/what-is-dom-shadow-dom-virtual-dom.md index b669eca20..6aade4a57 100644 --- a/src/data/videos/what-is-dom-shadow-dom-virtual-dom.md +++ b/src/data/videos/what-is-dom-shadow-dom-virtual-dom.md @@ -4,10 +4,7 @@ description: 'Learn what is DOM, Shadow DOM and Virtual DOM and how they work.' duration: '6 minutes' isNew: false date: 2020-07-20 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/what-is-eventual-consistency.md b/src/data/videos/what-is-eventual-consistency.md index 6cb9df11e..78ef37a77 100644 --- a/src/data/videos/what-is-eventual-consistency.md +++ b/src/data/videos/what-is-eventual-consistency.md @@ -4,10 +4,7 @@ description: 'Learn about the different consistency models in distributed system duration: '5 minutes' isNew: false date: 2021-11-30 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/data/videos/yaml-in-depth.md b/src/data/videos/yaml-in-depth.md index 9967ffde6..6fc6e4a54 100644 --- a/src/data/videos/yaml-in-depth.md +++ b/src/data/videos/yaml-in-depth.md @@ -4,10 +4,7 @@ description: 'Everything you need to know about YAML' duration: '8 minutes' isNew: false date: 2021-10-18 -author: - name: 'Kamran Ahmed' - url: 'https://twitter.com/kamrify' - imageUrl: '/authors/kamranahmedse.jpeg' +authorId: 'kamran' sitemap: priority: 0.7 changefreq: 'weekly' diff --git a/src/lib/video.ts b/src/lib/video.ts index d0eeb9ec6..09629cf21 100644 --- a/src/lib/video.ts +++ b/src/lib/video.ts @@ -1,14 +1,13 @@ import type { MarkdownFileType } from './file'; -import type {AuthorFileType} from "./author.ts"; +import type { AuthorFileType } from './author.ts'; +import { getAllAuthors } from './author.ts'; +import type {GuideFileType} from "./guide.ts"; +import {getAllGuides} from "./guide.ts"; export interface VideoFrontmatter { title: string; description: string; - author: { - name: string; - url: string; - imageUrl: string; - }; + authorId: string; seo: { title: string; description: string; @@ -40,6 +39,14 @@ function videoPathToId(filePath: string): string { return fileName.replace('.md', ''); } +export async function getVideosByAuthor( + authorId: string, +): Promise { + const allVideos = await getAllVideos(); + + return allVideos.filter((video) => video.author?.id === authorId); +} + /** * Gets all the videos sorted by the publishing date * @returns Promisifed video files @@ -49,10 +56,15 @@ export async function getAllVideos(): Promise { eager: true, }); + const allAuthors = await getAllAuthors(); + const videoFiles = Object.values(videos); const enrichedVideos = videoFiles.map((videoFile) => ({ ...videoFile, id: videoPathToId(videoFile.file), + author: allAuthors.find( + (author) => author.id === videoFile.frontmatter.authorId, + )!, })); return enrichedVideos.sort( diff --git a/src/pages/authors/[authorId].astro b/src/pages/authors/[authorId].astro index 61097093a..c3d9c349e 100644 --- a/src/pages/authors/[authorId].astro +++ b/src/pages/authors/[authorId].astro @@ -2,9 +2,10 @@ import BaseLayout from '../../layouts/BaseLayout.astro'; import AstroIcon from '../../components/AstroIcon.astro'; import { getGuidesByAuthor } from '../../lib/guide'; -import { getAllVideos } from '../../lib/video'; +import {getAllVideos, getVideosByAuthor} from '../../lib/video'; import GuideListItem from '../../components/GuideListItem.astro'; import { getAuthorById, getAuthorIds } from '../../lib/author'; +import VideoListItem from "../../components/VideoListItem.astro"; interface Params extends Record {} @@ -18,10 +19,13 @@ export async function getStaticPaths() { const { authorId } = Astro.params; +console.log(authorId); + const author = await getAuthorById(authorId); +console.log(author); const guides = await getGuidesByAuthor(authorId); -const videos = await getAllVideos(); +const videos = await getVideosByAuthor(authorId); ---
-
+

{author.frontmatter.name}

{guides.map((guide) => )} + {videos.map((video) => )}
From bd3fd8bfe2bc2f0215a330b5f9c14c3bc93f3d1c Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 27 Feb 2024 18:32:00 +0000 Subject: [PATCH 04/91] Add backend developer skills article --- src/data/guides/backend-developer-skills.md | 270 ++++++++++++++++++++ src/data/guides/backend-languages.md | 2 +- src/data/roadmaps/backend/faqs.astro | 2 +- src/pages/backend/developer-skills.astro | 26 ++ src/pages/backend/languages.astro | 2 +- 5 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 src/data/guides/backend-developer-skills.md create mode 100644 src/pages/backend/developer-skills.astro diff --git a/src/data/guides/backend-developer-skills.md b/src/data/guides/backend-developer-skills.md new file mode 100644 index 000000000..22218cfed --- /dev/null +++ b/src/data/guides/backend-developer-skills.md @@ -0,0 +1,270 @@ +--- +title: '8 In-Demand Backend Developer Skills to Master' +description: 'Learn what the essential backend skills you should master to advance in your career.' +authorId: fernando +excludedBySlug: '/backend/developer-skills' +seo: + title: '8 In-Demand Backend Developer Skills to Master' + description: 'Learn what the essential backend developer skills are that you should learn and master to advance in your career.' +isNew: true +type: 'textual' +date: 2024-02-27 +sitemap: + priority: 0.7 + changefreq: 'weekly' +tags: + - 'guide' + - 'textual-guide' + - 'guide-sitemap' +--- + +Whether your goal is to become a backend developer or to stay relevant as one, the goal itself requires adopting an eternal student mindset. The ever-evolving web development space demands continuous learning, regardless of the programming language you use. New frameworks, libraries, and methodologies emerge regularly, offering different solutions to old problems. To remain relevant as a [backend developer](/backend), you’ll have to stay updated by honing your core skills. + +In this article, we’ll cover the following set of backend developer skills we recommend you aim for: + +- Keeping an eye on core and new backend programming languages +- Understanding the basics of software design and architecture +- Understanding databases and how to use them +- API development +- The basics of version control +- Testing and debugging +- CI/CD and DevOps fundamentals +- Soft skills + +So, let's get going! + +## Understanding Backend Development + +Before we move on and start discussing the different backend development skills you should focus on, let’s first understand what a backend developer is. After all, if you’re looking to actually become a backend developer, you’ll need this. + +A backend developer focuses entirely on writing business logic for an application and much of the supporting logic as well. + +That said, there might be applications where the business logic is split into the frontend and the backend. However, while the frontend dev might have to share their time between UI code and business logic, the backend dev will focus most of their time on core business logic. That’s the main difference between the two. + +![UI vs Backend](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709056806118.png) + +In the above image, you can see how there is a lot more behind the curtain than just the UI when it comes to web applications. In this case, a “simple” log-in form needs a backend to contain its core business logic. + +Let’s now look at the most in-demand backend developer skills you should focus on in backend development. + +## Proficiency in Core and Emerging Programming Languages + +One of the most basic skills you should focus on as a backend developer is on identifying key programming languages to learn (or at least keep an eye out for). + +There are some essential backend languages that the industry has adopted as de facto standards. This means most new projects are usually coded using one (or multiple) of these programming languages. + +![core-languages](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709058292005.png) + +The most common names you should look out for are: + +- **JavaScript (or any of its variants, such as TypeScript).** This is a very common option because it’s also the language used by frontend developers, thus making it easier for developers to work on both sides of the same project. +- **Python.** While a very common option for other types of projects (such as data processing and [data science](https://roadmap.sh/ai-data-scientist)), it’s still very popular in the web development world. Python has many good qualities and supporting frameworks that make it a very easy-to-pick-up option for coding backend systems. +- **Go (A.K.A Golang).** This programming language was developed by Google. It was designed with simplicity, efficiency, and concurrency in mind. That’s made it gain popularity in the backend development space, making it an interesting option for projects that prioritize performance and concurrency. +- **Java.** One of the most common alternatives for enterprise solutions, Java, has been constantly evolving since its first release back in 1995. All that time making its way into big enterprises that trust its robustness and ever-growing community of developers. While not the easiest language to learn, it’s definitely up there in the top 10 most popular [backend languages](https://roadmap.sh/backend/languages) (according to [StackOverflow’s 2023 Developer survey](https://survey.stackoverflow.co/2023/#technology-most-popular-technologies)). + +While there are other options, the ones mentioned above, from the backend point of view, are some of the most relevant languages to pay attention to. Here are the top 10 most popular ones amongst professional developers (screenshot taken from SO’s survey of 2023): + +![Stackoverflow Survey Result](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709057007054.png) + +### Keeping an eye on the rising stars + +If working with at least one of the most common backend languages was important, understanding what are the rising technologies in the backend world is just as crucial. + +You won’t see a new programming language being released every month. However, in the span of a few years, you might see the release of several, and out of those, some might stick long enough to become new standards. + +For example, take a look at the period between 2012 and 2015; in just 3 years, 9 programming languages were released, out of which most of them are being used to this day. + +![Famous Languages](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709058257292.png) + +- In 2012, we got Julia, Elm, Go, and TypeScript. +- In 2013, we got Dart +- In 2014, we got Swift, Hack, and Crystal +- And in 2015, we got Rust. + +Some of those languages are very relevant to this day, such as TypeScript and Rust, while others, such as Hack and Crystal, might be known to only a few in very niche sectors. + +Of course, it’s impossible to predict which programming language will become a standard. However, the skill that you need to hone is that of keeping an eye on the industry to spot new and emerging trends. + +### The importance of supporting frameworks + +Frameworks for a specific programming language do change a lot faster than the language itself, though. + +Frameworks are there to provide you with a simplified gateway into the functionalities that you’d normally need for common tasks. For example, in the context of backend web development, frameworks usually take care of: + +- **Parsing HTTP requests** and turning them into objects you can easily interact with (so you don’t have to learn how the HTTP protocol works). +- **Abstracting concepts,** such as a request or a response, into objects and functions that you can reason about at a higher level. This gives you an easier time thinking about how to solve a problem using these tools. +- **Accessing data becomes a lot easier when there are abstractions.** Some frameworks provide what is known as an ORM (Object Relational Mapping). Through ORM, you can interact with databases without having to think about writing SQL queries or even database schemas. +- And many more. + +Frameworks are an essential aspect of the work you’ll do as a backend developer, which is why you should not neglect them. Of course, learning and mastering every single framework out there is impossible. Instead, learn to keep an eye out in the industry and see what are the most common frameworks, and focus on one (or two) of them. + +## Software Design and Architecture + +Coding is not just about writing code. + +While that statement might be a bit confusing, the truth is there is a lot of architecture involved in software development (both in frontend and backend development). Sometimes, working on these aspects of a system is the job of a specific role called “architect.” However, for backend systems, it’s not uncommon for backend developers to also be involved in architecture conversations and decisions. You’re helping design the underlying backend infrastructure, after all. + +The following diagram shows an example of what a very simple system’s architecture might look like: + +![Simple System Architecture](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709057266440.png) + +While the example is oversimplified, it gives you an idea of what the practice of “architecting a system” is. + +Essentially, architecting a system means coming up with concepts that represent different aspects of the solution and then deciding how you want to make them interact with each other. + +Why is architecture so important here? Because it gives you properties such as code encapsulation, separation of concerns, reusability, and even scalability as a byproduct of the architecture itself. + +Let’s take a quick look at some of the most common architectures used for creating backend systems. + +### Most common backend architectures + +There are too many different architectural styles and patterns to cover them all inside a single article, but let's just look at some of the most common ones and how they can help you while working on your backend system. + +- **Monolithic architecture:** In a monolithic architecture, the entire application is built as a single, tightly coupled unit. All components (frontend, backend, database, etc) are part of the same codebase. This is a great first architecture because it feels very natural to develop under, and if your project is not going to grow out of proportion, then you will probably not hit any of the cons. +- **Microservice-based architecture:** The application is divided into small, independent services, each responsible for a specific business capability. These services communicate through APIs. +- **Service-Oriented Architecture:** Similar to microservices, a service-oriented architecture splits functionality into individual services. The main difference is that these services aren’t as granular as a microservice, so they might incorporate functionalities related to different business entities. +- **Event-driven architecture:** With this architecture, each system (or service) responds to events (e.g., user actions and messages from other services) by triggering actions or processes. All services communicate with each other indirectly through an event bus (also known as a “message bus”), so it removes the possibility of having two or more services coupled with each other (meaning that they can’t be treated individually). +- **Serverless Architecture:** Also known as Function as a Service (FaaS), serverless architecture allows you to focus on writing code without worrying about the server where they’ll run. Functions are executed in response to events without the need for provisioning or managing servers (this is done FOR you automatically). +- **Microkernel architecture:** This architecture lets you build the core, essential functionality into a small microkernel and have the rest of the features built as plugins that can be added, removed or exchanged easily. + +And if you want to know more about the patterns and principles mentioned here, please check out the [Software Design and Architecture roadmap](https://roadmap.sh/software-design-architecture). + +## Mastery of Database Management Systems + +As a backend developer, you will undoubtedly have to deal with database administration in your daily tasks. They are the industry standard for storing persistent data. + +Because of that, it’s important to understand that you should be aware of two main categories of databases: SQL databases and NoSQL databases. + +### SQL databases + +These are the standard structured databases (A.K.A relational databases) where you need to define the schema for your data (essentially the data structures you’re dealing with), and then you’ll use a language called [SQL (Structured Query Language)](https://roadmap.sh/sql) to interact with the data inside it. Most backend developers will interact with SQL databases at some point in their career, as this is the most common type of database. + +### NoSQL databases + +As the name implies, these are not your standard SQL databases; in fact, within this category, there are columnar databases, document-based ones (such as MongoDB), key-value-based ones (like Redis), and more. They don’t use predefined data structures, giving you more flexibility and control over what you can store and how you store it. Backend developers will deal with only a handful of these, as there are many different sub-types, and more are created every year. + +Some examples of these databases are: + +- MongoDB, a document-based database (see here a mongoDB roadmap if you’re interested). +- Redis, an in-memory key-value pair database. +- Neo4J, a graph database. +- ElasticSearch, a document-based search engine. + +In the end, the decision between SQL and NoSQL is about trade-offs and figuring out what works best for your particular use case. + +## API Development Capabilities + +Application Programming Interfaces (APIs) are everywhere. They power the backend of almost all major systems out there (according to a [study conducted by O’Reilly in 2020](https://www.oreilly.com/pub/pr/3307), 77% of companies were using microservices/APIs). + +That is to say, if you’re thinking about becoming a backend developer, you will be coding APIs/microservices. This is why understanding the basics of them is crucial to ensuring your relevance in the field. + +![System vs External System](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709057608824.png) + +The above diagram explains how APIs interact with whatever you might be building. + +Now, if you’re inside the “**The System**” box, then you need to understand how to interact with these APIs using the right tools. If you’re inside the “**External System**” box, then you need to understand the type of standards these APIs need to follow and how to implement them. + +Don’t worry though, for both situations, there are always frameworks and libraries you can use to simplify your task and ensure you’re following the proper industry standards. + +### What are the most common API types? + +The most common types of APIs used in the industry currently are REST and GraphQL. + +As a backend developer, it’s not mandatory that you master both of these types, but it’s definitely recommended that you have some practical experience with one of them. + +- **RESTful APIs.** These are APIs that work over HTTP and make extensive use of the HTTP Verbs to give meaning to each request. They’ve been the most popular type of API until recently, so there are still a lot of projects and teams that make use of it. +- **GraphQL.** GraphQL APIs operate over HTTP as well, leveraging the HTTP protocol and its verbs. In contrast to the conventional RESTful APIs, GraphQL has emerged as a powerful alternative, offering a flexible and efficient approach to data querying and manipulation. GraphQL allows clients to request only the data they need, providing a more tailored and efficient interaction between clients and servers. + +Is there one better than the other? There is no easy way to answer that question as both are capable of doing everything you’d need. It’s more about your particular requirements and the preferences of your dev team. + +## Version Control Savvy + +One mandatory skill that all backend developers should work on (actually, all developers, in general) is version control, or in other words, understanding and being familiar with version control systems. + +Essentially, you’ll want to know how to use the version control tool that everyone else is using. The industry standard at the moment of writing this is [Git](https://git-scm.com/), while there might be some teams using other (older) tools, as long as you understand the current one, you’ll be in good shape. + +### What is version control? + +Version control references the ability for you and other developers to share code with each other while you’re working on the same files. + +While Git is the industry standard at the moment, GitHub has created such a popular platform around Git, that it almost makes it mandatory to learn about. + +So go ahead and create an account, browse what others are doing, and upload your own personal projects. It’s definitely a great way to learn. + +### What should you learn about Git? + +If you’re picking up Git as your version control system of choice, there are two main areas you should be focusing on. + +- **The basics.** Clearly understanding how Git works and the basic commands to add, push and pull changes. You should aim to learn enough about them to feel comfortable using them on your day-to-day (because you will). +- **Branching strategies.** Sadly, using Git alone is not enough. While through Git you can already start versioning your code, when the project is complex enough and your team big enough, the tool alone will not be enough. You’ll have to come up with [branching strategies](https://learngitbranching.js.org/?locale=es_ES) to organize the whole team’s workflow. + +Keep in mind that Git and Git branching are not trivial topics, and they’ll take a while to master. So, while you should give yourself time to learn about them, also make sure you check with others (or use tools such as ChatGPT) to validate your commands before using them. Remember, a wrong Git command or a wrong workflow can cause major problems within a project, especially if there are many developers working on the same codebase. + +## Testing + +Understanding both what testing is and the importance of it within the backend development workflow is crucial for all developers, and one of the mandatory backend developer skills to focus on. + +Testing is the development process of making sure your code works in a way that doesn’t involve you manually testing every feature but rather using tools that allow you to test and reproduce any problems that can be found programmatically. + +This, of course, helps to remove potential human error from the equation when testing big systems and to increase the speed at which these tests can be done (think seconds vs hours of you doing it manually). + +Testing is a far more complex discipline than I can describe here. Just know that there are many different ways to test a system, and all backend developers should be aware of the following: + +- **Unit testing:** This is the most common way of doing code testing. You’ll write tests using a testing framework for every publicly available function/method in your code. This way you’re making sure every piece of code that can be used is tested and performs according to plan. Running these tests is usually quite fast, so you’ll be doing it before every commit (usually). +- **Integration testing:** If you’re building a system that consists of multiple independent systems working together (think, for instance, a microservice-based architecture), then testing each individual part is not enough. You also have to make sure systems that should interact with each other do so correctly. This is where integration tests come into play. +- **End-to-end testing (E2E):** These tests are similar to integration tests, but they also include the UI of the system. There are tools you can use to automate actions in your UI as if a real user were performing them and then checking the result. For example, clicking on a log-out button and checking if you’re later redirected to the log-in screen. This flow would involve the backend performing some actions that result in the user being logged out. +- **Load testing:** While not exactly the same process as with the previous test types, load testing is great for backend systems because it helps you determine if your backend is ready to deal with high amounts of traffic. + +You can think of the list in graphical format as the following diagram: + +![Testing types](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709057834295.png) + +If you’re just getting started with testing, I’d recommend focusing only on unit testing for the time being. Once you have a grasp on it, start moving out following the above diagram and slowly move into the other types as you progress. + +## CI/CD and DevOps Familiarity + +As a backend developer, your code will be constantly deployed, either into cloud environments or perhaps even into normal, on-premise servers. The point is that what you build will run through CI/CD (Continuous Integration and Continuous Deployment) processes. + +![ci-cd](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1709058122541.png) + +These processes will automatically test it (Continuous Integration) and automatically deploy it (if all tests go well). As a backend developer, you’re not usually expected to know and understand how to configure these processes; however, it’s important that you know about them. + +DevOps is yet another tangential area to that of a backend developer. When teams are small enough, backend devs might be “gently pushed” into tasks such as configuring CI/CD pipelines, setting up servers, and more. These tasks are usually performed by dedicated professionals with the role of DevOps. Their specialty is automation, making the deployment process efficient and ensuring that everything runs smoothly in the real-world server environment. They play a crucial role in maintaining the reliability and performance of applications and websites. + +So, while they’re not strictly the responsibilities of backend developers, they’re close enough to the role’s day-to-day that it would be a good idea to learn about them. If you’re interested in learning more about DevOps, check out [our DevOps roadmap](https://roadmap.sh/devops) containing all the key topics you should learn about if you want to become a DevOps engineer. + +## Soft Skills + +Finally, the last set of backend developer skills you should focus on are, actually, not technical skills, nor are they exclusively useful for backend developers. These are skills that every developer should work on during their career: soft skills. + +### Improving communication + +The ability to communicate with others, both technical and non-technical people, is crucial in any developer's career. + +For backend developers, it’s especially important because communicating their work and the effects of it is definitely harder than other roles, such as frontend developers who can actually showcase what they’re building. + +As a backend developer, you’ll be able to better explain problems or blockers to your colleagues, you’ll be able to perform requirement gathering much more effectively, and you’ll even improve your own problem-solving skills by being better at articulating the problems and potential solutions to yourself. + +### Critical thinking + +Honing your critical thinking as a backend developer will help your ability to analyze complex problems, identify patterns much faster, and come up with innovative solutions to the problems you’re facing. + +Pushing the limits of your critical thinking skills will also foster a more systematic and strategic approach to coding and architecting robust and efficient solutions. + +In other words, it’ll make you a better and more efficient coder. And who doesn’t want that? + +## Conclusion + +To summarize, if you expect to become a backend developer or to grow in the area of backend development: + +- Keep an eye on the industry to understand what’s the status quo and what’s new and hot. +- Understand the basics of software design and architecture. +- Look into relational databases and NoSQL databases as well; they’re both important. +- Learn how to build and use APIs; they’ll be part of almost every project you work on. +- Remember, testing might look like it’s not mandatory, but it’s definitely a standard practice when it comes to backend development. +- CI/CD and DevOps are practices you’ll be involved with, either directly or indirectly, so learn about them. +- Soft skills are just as important as technical skills if you expect to grow in your career. + +That said, do not take this list as the ultimate roadmap but rather as a starting point. If you’re willing to take your backend developer career to the next level, push yourself out of your comfort zone and pursue the skills listed here and the ones listed in this detailed [backend development roadmap](https://roadmap.sh/backend). + +Remember, constant learning is the only absolute truth in the software development world (this is true for backend developers, too). If you keep your skillset updated with the latest trends, you’ll remain adaptable and effective as a backend developer. diff --git a/src/data/guides/backend-languages.md b/src/data/guides/backend-languages.md index 3c57108e0..9f465fac8 100644 --- a/src/data/guides/backend-languages.md +++ b/src/data/guides/backend-languages.md @@ -6,7 +6,7 @@ excludedBySlug: '/backend/languages' seo: title: 'The 5 Best Backend Development Languages to Master (2024)' description: 'Discover the best backend development languages to learn right now for career development, with practical tips from an experienced developer.' -isNew: true +isNew: false type: 'textual' date: 2024-01-18 sitemap: diff --git a/src/data/roadmaps/backend/faqs.astro b/src/data/roadmaps/backend/faqs.astro index d853a9179..01efa2277 100644 --- a/src/data/roadmaps/backend/faqs.astro +++ b/src/data/roadmaps/backend/faqs.astro @@ -7,7 +7,7 @@ export const faqs: FAQType[] = [ answer: [ "If you are a complete beginner who is just getting started, you can start by learning a backend programming language such as Python, Ruby, Java, Go etc. Once you have got the basic to intermediate understanding of the language, learn about the package manager for that language and learn how to install and use external packages into your applications. Learn the basics of some relational database e.g. PostgreSQL and learn how to run simple CRUD operations. Optionally, you can pick up and learn a web framework for the language of your choice as well. Learn how to build a simple RESTful API and implement simple Authentication/Authorization into it. While you are learning all the items mentioned above, don't forget to learn about Git and GitHub as well.", 'After following all the instructions above, you can start applying for the entry level backend developer jobs. Also, look at the backend developer roadmap above to get an idea about the landscape and see what else you are missing. A degree in computer science or related field is not always necessary but networking, building a portfolio and actively seeking internships, junior developer positions or consulting can help to start and advance a career as a backend developer.', - "Note: remember to make a lot of projects while you are learning to solidify your understanding of the concepts. Also, it's important to have the attitude of continuous learning to improve your skills and be prepared for the fast-paced technology evolution in the industry.", + "Note: remember to make a lot of projects while you are learning to solidify your understanding of the concepts. Also, it's important to have the attitude of continuous learning to improve your [skills](https://roadmap.sh/backend/developer-skills) and be prepared for the fast-paced technology evolution in the industry.", ], }, { diff --git a/src/pages/backend/developer-skills.astro b/src/pages/backend/developer-skills.astro new file mode 100644 index 000000000..148a05762 --- /dev/null +++ b/src/pages/backend/developer-skills.astro @@ -0,0 +1,26 @@ +--- +import GuideHeader from '../../components/GuideHeader.astro'; +import MarkdownFile from '../../components/MarkdownFile.astro'; +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { getGuideById } from '../../lib/guide'; + +const guideId = 'backend-developer-skills'; +const guide = await getGuideById(guideId); + +const { frontmatter: guideData } = guide; +--- + + + + +
+ + + +
+
diff --git a/src/pages/backend/languages.astro b/src/pages/backend/languages.astro index 452a2c72f..95a4a5a20 100644 --- a/src/pages/backend/languages.astro +++ b/src/pages/backend/languages.astro @@ -13,7 +13,7 @@ const { frontmatter: guideData } = guide; From dcef07d7c674b375c1b01fc24e373a0d0d267db6 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Thu, 29 Feb 2024 11:39:09 +0000 Subject: [PATCH 05/91] Remove custom roadmaps from google indexing --- src/pages/r/index.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/r/index.astro b/src/pages/r/index.astro index fae5dde79..4d719a237 100644 --- a/src/pages/r/index.astro +++ b/src/pages/r/index.astro @@ -6,7 +6,7 @@ import Loader from '../../components/Loader.astro'; import ProgressHelpPopup from '../../components/ProgressHelpPopup.astro'; --- - +
From f6cd6419be88bcf32df61aaeea3207390fa5e148 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Thu, 29 Feb 2024 11:41:05 +0000 Subject: [PATCH 06/91] Fix typing error --- src/lib/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/http.ts b/src/lib/http.ts index 7f1692b27..91fc488cd 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -36,7 +36,7 @@ export async function httpCall< options?: HttpOptionsType, ): Promise> { try { - const fingerprintPromise = await fp.load({ monitoring: false }); + const fingerprintPromise = await fp.load(); const fingerprint = await fingerprintPromise.get(); const response = await fetch(url, { From d2f372fd6fe1660a829eb6124801d1cd91cecbcc Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Fri, 1 Mar 2024 17:21:06 +0000 Subject: [PATCH 07/91] Add community roadmap alert --- .../CustomRoadmap/CustomRoadmapAlert.tsx | 55 +++++++++++++++++++ .../CustomRoadmap/RoadmapHeader.tsx | 3 + src/components/NavigationDropdown.tsx | 1 - src/env.d.ts | 2 +- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/components/CustomRoadmap/CustomRoadmapAlert.tsx diff --git a/src/components/CustomRoadmap/CustomRoadmapAlert.tsx b/src/components/CustomRoadmap/CustomRoadmapAlert.tsx new file mode 100644 index 000000000..ead638713 --- /dev/null +++ b/src/components/CustomRoadmap/CustomRoadmapAlert.tsx @@ -0,0 +1,55 @@ +import { BadgeCheck, MessageCircleHeart, PencilRuler } from 'lucide-react'; +import { showLoginPopup } from '../../lib/popup.ts'; +import { isLoggedIn } from '../../lib/jwt.ts'; +import { useState } from 'react'; +import { CreateRoadmapModal } from './CreateRoadmap/CreateRoadmapModal.tsx'; + +export function CustomRoadmapAlert() { + const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); + + return ( + <> + {isCreatingRoadmap && ( + { + setIsCreatingRoadmap(false); + }} + /> + )} +
+

+ Community Roadmap +

+

+ This is a custom roadmap made by community and isn't verified by{' '} + roadmap.sh +

+
+ + + Visit Official Roadmaps + + · + +
+ + +
+ + ); +} diff --git a/src/components/CustomRoadmap/RoadmapHeader.tsx b/src/components/CustomRoadmap/RoadmapHeader.tsx index a59abb7e6..4c4c059fa 100644 --- a/src/components/CustomRoadmap/RoadmapHeader.tsx +++ b/src/components/CustomRoadmap/RoadmapHeader.tsx @@ -12,6 +12,7 @@ import { Lock, Shapes } from 'lucide-react'; import { Modal } from '../Modal'; import { ShareSuccess } from '../ShareOptions/ShareSuccess'; import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; +import { CustomRoadmapAlert } from './CustomRoadmapAlert.tsx'; type RoadmapHeaderProps = {}; @@ -89,6 +90,8 @@ export function RoadmapHeader(props: RoadmapHeaderProps) { return (
+ {!$canManageCurrentRoadmap && } + {creator?.name && (
interface ImportMetaEnv { GITHUB_SHA: string; From cfa8d2a9869220a6d7e889268b719a9d77418703 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Fri, 1 Mar 2024 17:46:54 +0000 Subject: [PATCH 08/91] Page sponsor banner fix --- src/components/PageSponsor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PageSponsor.tsx b/src/components/PageSponsor.tsx index fbdc0377d..45d149236 100644 --- a/src/components/PageSponsor.tsx +++ b/src/components/PageSponsor.tsx @@ -105,7 +105,7 @@ export function PageSponsor(props: PageSponsorProps) { Sponsor Banner From 7040b6637c0b940ef28243a25df0eb8cd8582efc Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Fri, 1 Mar 2024 18:10:21 +0000 Subject: [PATCH 09/91] Persist query parameters on ai and custom roadmap page --- src/components/AuthenticationFlow/GitHubButton.tsx | 2 +- src/components/AuthenticationFlow/GoogleButton.tsx | 2 +- src/components/AuthenticationFlow/LinkedInButton.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/AuthenticationFlow/GitHubButton.tsx b/src/components/AuthenticationFlow/GitHubButton.tsx index 3ebc3a628..3deb4c569 100644 --- a/src/components/AuthenticationFlow/GitHubButton.tsx +++ b/src/components/AuthenticationFlow/GitHubButton.tsx @@ -101,7 +101,7 @@ export function GitHubButton(props: GitHubButtonProps) { // For non authentication pages, we want to redirect back to the page // the user was on before they clicked the social login button if (!['/login', '/signup'].includes(window.location.pathname)) { - const pagePath = ['/respond-invite', '/befriend'].includes( + const pagePath = ['/respond-invite', '/befriend', '/r', '/ai'].includes( window.location.pathname, ) ? window.location.pathname + window.location.search diff --git a/src/components/AuthenticationFlow/GoogleButton.tsx b/src/components/AuthenticationFlow/GoogleButton.tsx index 3d92b1bde..60b0dcf84 100644 --- a/src/components/AuthenticationFlow/GoogleButton.tsx +++ b/src/components/AuthenticationFlow/GoogleButton.tsx @@ -97,7 +97,7 @@ export function GoogleButton(props: GoogleButtonProps) { // For non authentication pages, we want to redirect back to the page // the user was on before they clicked the social login button if (!['/login', '/signup'].includes(window.location.pathname)) { - const pagePath = ['/respond-invite', '/befriend'].includes( + const pagePath = ['/respond-invite', '/befriend', '/r', '/ai'].includes( window.location.pathname, ) ? window.location.pathname + window.location.search diff --git a/src/components/AuthenticationFlow/LinkedInButton.tsx b/src/components/AuthenticationFlow/LinkedInButton.tsx index 851b2a9d8..6f36c319b 100644 --- a/src/components/AuthenticationFlow/LinkedInButton.tsx +++ b/src/components/AuthenticationFlow/LinkedInButton.tsx @@ -97,7 +97,7 @@ export function LinkedInButton(props: LinkedInButtonProps) { // For non authentication pages, we want to redirect back to the page // the user was on before they clicked the social login button if (!['/login', '/signup'].includes(window.location.pathname)) { - const pagePath = ['/respond-invite', '/befriend'].includes( + const pagePath = ['/respond-invite', '/befriend', '/r', '/ai'].includes( window.location.pathname, ) ? window.location.pathname + window.location.search From 070d04334b2b9d739d2cdc47116cb97702d89658 Mon Sep 17 00:00:00 2001 From: Alex <42818181+alex-lvl@users.noreply.github.com> Date: Sun, 3 Mar 2024 11:38:37 -0500 Subject: [PATCH 10/91] fix: blockchain typo (#5282) Co-authored-by: Alex Marmolejo --- src/pages/get-started.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/get-started.astro b/src/pages/get-started.astro index 517a29656..bab443cbd 100644 --- a/src/pages/get-started.astro +++ b/src/pages/get-started.astro @@ -381,7 +381,7 @@ import { TipItem } from '../components/GetStarted/TipItem'; icon={PenSquare} title='Blockchain' link='/blockchain' - description='Learn all you need to know to become a Technical Writer.' + description='Learn all you need to know to become a Blockchain Developer.' /> Date: Tue, 5 Mar 2024 00:41:10 +1100 Subject: [PATCH 11/91] Fix formatting (#5247) --- .../content/100-typescript/102-install-configure/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/roadmaps/typescript/content/100-typescript/102-install-configure/index.md b/src/data/roadmaps/typescript/content/100-typescript/102-install-configure/index.md index 44a3e1394..de2102a21 100644 --- a/src/data/roadmaps/typescript/content/100-typescript/102-install-configure/index.md +++ b/src/data/roadmaps/typescript/content/100-typescript/102-install-configure/index.md @@ -35,7 +35,7 @@ npm install --save-dev typescript tsc ``` -Note: You can also compile individual TypeScript files by specifying the file name after the tsc command.For example: +Note: You can also compile individual TypeScript files by specifying the file name after the tsc command. For example: ```bash tsc index.ts From 44d372488067acf3881b2dd1f9075105450ea2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reyes=20Rond=C3=B3n?= <66537220+Reyes1921@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:46:22 -0400 Subject: [PATCH 12/91] fix: output length of string (#5227) --- .../101-typescript-types/115-type-assertions/101-as-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md b/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md index 85c2c5808..6a6767aa9 100644 --- a/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md +++ b/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md @@ -9,7 +9,7 @@ Here's a simple example: let someValue: any = "Hello, TypeScript!"; let strLength: number = (someValue as string).length; -console.log(strLength); // Outputs: 20 +console.log(strLength); // Outputs: 18 ``` In this example, someValue is initially of type any, and we use the as operator to assert that it is of type string before accessing its length property. It's important to note that type assertions do not change the underlying runtime representation; they are a compile-time construct used for static type checking in TypeScript. From d5fdc62343f78f5b40fc26445d6847ba14f816d9 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Mon, 4 Mar 2024 17:56:15 +0000 Subject: [PATCH 13/91] Adds AI roadmap generator (#5289) * feat: implement roadmap generator * feat: add roadmap stream * Update UI * fix: add fingerprint visitor id * feat: implement edit generated roadmap * feat: implement ai roadmap download * feat: add limit count * fix: add limit check * fix: download image button * feat: implement roadmap generator * feat: add roadmap stream * Update UI * fix: add fingerprint visitor id * feat: implement edit generated roadmap * feat: implement ai roadmap download * feat: add limit count * fix: add limit check * fix: download image button * UI Updates * Update UI for roadmap search * Update UI for roadmap limit * Update UI for roadmap * UI responsiveness on AI roadmap generator --------- Co-authored-by: Arik Chakma --- package.json | 4 + pnpm-lock.yaml | 90 +++-- public/images/icons8-wand.gif | Bin 0 -> 31555 bytes .../GenerateRoadmap/GenerateRoadmap.css | 58 +++ .../GenerateRoadmap/GenerateRoadmap.tsx | 361 ++++++++++++++++++ .../GenerateRoadmap/RoadmapSearch.tsx | 121 ++++++ src/helper/download-image.ts | 32 ++ src/helper/read-stream.ts | 43 +++ src/layouts/BaseLayout.astro | 2 +- src/pages/ai/index.astro | 10 + tsconfig.json | 5 +- 11 files changed, 688 insertions(+), 38 deletions(-) create mode 100644 public/images/icons8-wand.gif create mode 100644 src/components/GenerateRoadmap/GenerateRoadmap.css create mode 100644 src/components/GenerateRoadmap/GenerateRoadmap.tsx create mode 100644 src/components/GenerateRoadmap/RoadmapSearch.tsx create mode 100644 src/helper/read-stream.ts create mode 100644 src/pages/ai/index.astro diff --git a/package.json b/package.json index e2bc8eb11..d4e87dab9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "astro": "^4.4.0", "astro-compress": "^2.2.10", "clsx": "^2.1.0", + "dom-to-image": "^2.6.0", "dracula-prism": "^2.1.16", "jose": "^5.2.2", "js-cookie": "^3.0.5", @@ -46,15 +47,18 @@ "react-dom": "^18.2.0", "reactflow": "^11.10.4", "rehype-external-links": "^3.0.0", + "remark-parse": "^11.0.0", "roadmap-renderer": "^1.0.6", "slugify": "^1.6.6", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", + "unified": "^11.0.4", "zustand": "^4.5.1" }, "devDependencies": { "@playwright/test": "^1.41.2", "@tailwindcss/typography": "^0.5.10", + "@types/dom-to-image": "^2.6.7", "@types/js-cookie": "^3.0.6", "@types/prismjs": "^1.26.3", "csv-parser": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7831ce0e..40919589e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: dependencies: '@astrojs/react': specifier: ^3.0.10 - version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3) + version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3) '@astrojs/sitemap': specifier: ^3.0.5 version: 3.0.5 @@ -22,7 +22,7 @@ dependencies: version: 0.7.1(nanostores@0.9.5)(react@18.2.0) '@types/react': specifier: ^18.2.56 - version: 18.2.58 + version: 18.2.59 '@types/react-dom': specifier: ^18.2.19 version: 18.2.19 @@ -35,6 +35,9 @@ dependencies: clsx: specifier: ^2.1.0 version: 2.1.0 + dom-to-image: + specifier: ^2.6.0 + version: 2.6.0 dracula-prism: specifier: ^2.1.16 version: 2.1.16 @@ -73,10 +76,13 @@ dependencies: version: 18.2.0(react@18.2.0) reactflow: specifier: ^11.10.4 - version: 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + version: 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) rehype-external-links: specifier: ^3.0.0 version: 3.0.0 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 roadmap-renderer: specifier: ^1.0.6 version: 1.0.6 @@ -89,9 +95,12 @@ dependencies: tailwindcss: specifier: ^3.4.1 version: 3.4.1 + unified: + specifier: ^11.0.4 + version: 11.0.4 zustand: specifier: ^4.5.1 - version: 4.5.1(@types/react@18.2.58)(react@18.2.0) + version: 4.5.1(@types/react@18.2.59)(react@18.2.0) devDependencies: '@playwright/test': @@ -100,6 +109,9 @@ devDependencies: '@tailwindcss/typography': specifier: ^0.5.10 version: 0.5.10(tailwindcss@3.4.1) + '@types/dom-to-image': + specifier: ^2.6.7 + version: 2.6.7 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -185,7 +197,7 @@ packages: prismjs: 1.29.0 dev: false - /@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3): + /@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3): resolution: {integrity: sha512-uGRIwKMAn7tva2vxXMyoVIGxWFr0rjZ8ZWIlkTG/vIpnAjD2nM8Cz6B8j7yzj176jvl6gZ6xTbTVPm09aeK0Yw==} engines: {node: '>=18.14.1'} peerDependencies: @@ -194,7 +206,7 @@ packages: react: ^17.0.2 || ^18.0.0 react-dom: ^17.0.2 || ^18.0.0 dependencies: - '@types/react': 18.2.58 + '@types/react': 18.2.59 '@types/react-dom': 18.2.19 '@vitejs/plugin-react': 4.2.1(vite@5.1.3) react: 18.2.0 @@ -1102,39 +1114,39 @@ packages: config-chain: 1.1.13 dev: false - /@reactflow/background@11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/background@11.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/controls@11.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/core@11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==} peerDependencies: react: '>=17' @@ -1150,19 +1162,19 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/minimap@11.7.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 classcat: 5.0.4 @@ -1170,41 +1182,41 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-resizer@2.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-toolbar@1.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer @@ -1618,6 +1630,10 @@ packages: '@types/ms': 0.7.34 dev: false + /@types/dom-to-image@2.6.7: + resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==} + dev: true + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: false @@ -1700,11 +1716,11 @@ packages: /@types/react-dom@18.2.19: resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} dependencies: - '@types/react': 18.2.58 + '@types/react': 18.2.59 dev: false - /@types/react@18.2.58: - resolution: {integrity: sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==} + /@types/react@18.2.59: + resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==} dependencies: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 @@ -2697,6 +2713,10 @@ packages: entities: 4.5.0 dev: false + /dom-to-image@2.6.0: + resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==} + dev: false + /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: false @@ -5543,18 +5563,18 @@ packages: loose-envify: 1.4.0 dev: false - /reactflow@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): + /reactflow@11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/controls': 11.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/minimap': 11.7.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-resizer': 2.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -6929,7 +6949,7 @@ packages: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false - /zustand@4.5.1(@types/react@18.2.58)(react@18.2.0): + /zustand@4.5.1(@types/react@18.2.59)(react@18.2.0): resolution: {integrity: sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==} engines: {node: '>=12.7.0'} peerDependencies: @@ -6944,7 +6964,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.58 + '@types/react': 18.2.59 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false diff --git a/public/images/icons8-wand.gif b/public/images/icons8-wand.gif new file mode 100644 index 0000000000000000000000000000000000000000..621b405e3de6677c099a960d744884bb8625ad71 GIT binary patch literal 31555 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 g[data-type='topic'], +svg > g[data-type='subtopic'], +svg > g > g[data-type='link-item'], +svg > g[data-type='button'] { + cursor: pointer; +} + +svg > g[data-type='topic']:hover > rect { + fill: #d6d700; +} + +svg > g[data-type='subtopic']:hover > rect { + fill: #f3c950; +} +svg > g[data-type='button']:hover { + opacity: 0.8; +} + +svg .done rect { + fill: #cbcbcb !important; +} + +svg .done text, +svg .skipped text { + text-decoration: line-through; +} + +svg > g[data-type='topic'].learning > rect + text, +svg > g[data-type='topic'].done > rect + text { + fill: black; +} + +svg > g[data-type='subtipic'].done > rect + text, +svg > g[data-type='subtipic'].learning > rect + text { + fill: #cbcbcb; +} + +svg .learning rect { + fill: #dad1fd !important; +} +svg .learning text { + text-decoration: underline; +} + +svg .skipped rect { + fill: #496b69 !important; +} diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx new file mode 100644 index 000000000..7b1269fc9 --- /dev/null +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -0,0 +1,361 @@ +import { type FormEvent, useEffect, useRef, useState } from 'react'; +import './GenerateRoadmap.css'; +import { useToast } from '../../hooks/use-toast'; +import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator'; +import { renderFlowJSON } from '../../../editor/renderer/renderer'; +import { replaceChildren } from '../../lib/dom'; +import { readAIRoadmapStream } from '../../helper/read-stream'; +import { isLoggedIn, removeAuthToken } from '../../lib/jwt'; +import { RoadmapSearch } from './RoadmapSearch.tsx'; +import { Spinner } from '../ReactIcons/Spinner.tsx'; +import { Ban, Download, PenSquare, Wand } from 'lucide-react'; +import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; +import { httpGet, httpPost } from '../../lib/http.ts'; +import { pageProgressMessage } from '../../stores/page.ts'; +import { + deleteUrlParam, + getUrlParams, + setUrlParams, +} from '../../lib/browser.ts'; +import { downloadGeneratedRoadmapImage } from '../../helper/download-image.ts'; +import { showLoginPopup } from '../../lib/popup.ts'; +import { cn } from '../../lib/classname.ts'; + +const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@'); + +export function GenerateRoadmap() { + const roadmapContainerRef = useRef(null); + + const { id: roadmapId } = getUrlParams() as { id: string }; + const toast = useToast(); + + const [hasSubmitted, setHasSubmitted] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [roadmapTopic, setRoadmapTopic] = useState(''); + const [generatedRoadmap, setGeneratedRoadmap] = useState(''); + + const [roadmapLimit, setRoadmapLimit] = useState(0); + const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0); + + const renderRoadmap = async (roadmap: string) => { + const { nodes, edges } = generateAIRoadmapFromText(roadmap); + const svg = await renderFlowJSON({ nodes, edges }); + if (roadmapContainerRef?.current) { + replaceChildren(roadmapContainerRef?.current, svg); + } + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!roadmapTopic) { + return; + } + + setIsLoading(true); + setHasSubmitted(true); + + if (roadmapLimitUsed >= roadmapLimit) { + toast.error('You have reached your limit of generating roadmaps'); + setIsLoading(false); + return; + } + + deleteUrlParam('id'); + + const response = await fetch( + `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ topic: roadmapTopic }), + }, + ); + + if (!response.ok) { + const data = await response.json(); + + toast.error(data?.message || 'Something went wrong'); + setIsLoading(false); + + // Logout user if token is invalid + if (data.status === 401) { + removeAuthToken(); + window.location.reload(); + } + } + + const reader = response.body?.getReader(); + + if (!reader) { + setIsLoading(false); + toast.error('Something went wrong'); + return; + } + + await readAIRoadmapStream(reader, { + onStream: async (result) => { + if (result.includes('@ROADMAPID')) { + // @ROADMAPID: is a special token that we use to identify the roadmap + // @ROADMAPID:1234@ is the format, we will remove the token and the id + // and replace it with a empty string + const roadmapId = result.match(ROADMAP_ID_REGEX)?.[1] || ''; + setUrlParams({ id: roadmapId }); + result = result.replace(ROADMAP_ID_REGEX, ''); + } + + await renderRoadmap(result); + }, + onStreamEnd: async (result) => { + result = result.replace(ROADMAP_ID_REGEX, ''); + setGeneratedRoadmap(result); + loadAIRoadmapLimit().finally(() => {}); + }, + }); + + setIsLoading(false); + }; + + const editGeneratedRoadmap = async () => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + pageProgressMessage.set('Redirecting to Editor'); + + const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmap); + + const { response, error } = await httpPost<{ + roadmapId: string; + }>(`${import.meta.env.PUBLIC_API_URL}/v1-edit-ai-generated-roadmap`, { + title: roadmapTopic, + nodes: nodes.map((node) => ({ + ...node, + + // To reset the width and height of the node + // so that it can be calculated based on the content in the editor + width: undefined, + height: undefined, + style: { + ...node.style, + width: undefined, + height: undefined, + }, + })), + edges, + }); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + setIsLoading(false); + return; + } + + window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${response.roadmapId}`; + }; + + const downloadGeneratedRoadmap = async () => { + pageProgressMessage.set('Downloading Roadmap'); + + const node = document.getElementById('roadmap-container'); + if (!node) { + toast.error('Something went wrong'); + return; + } + + try { + await downloadGeneratedRoadmapImage(roadmapTopic, node); + pageProgressMessage.set(''); + } catch (error) { + console.error(error); + toast.error('Something went wrong'); + } + }; + + const loadAIRoadmapLimit = async () => { + const { response, error } = await httpGet<{ + limit: number; + used: number; + }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-limit`); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + return; + } + + const { limit, used } = response; + setRoadmapLimit(limit); + setRoadmapLimitUsed(used); + }; + + const loadAIRoadmap = async (roadmapId: string) => { + pageProgressMessage.set('Loading Roadmap'); + + const { response, error } = await httpGet<{ + topic: string; + data: string; + }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap/${roadmapId}`); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + setIsLoading(false); + return; + } + + const { topic, data } = response; + await renderRoadmap(data); + + setRoadmapTopic(topic); + setGeneratedRoadmap(data); + }; + + useEffect(() => { + loadAIRoadmapLimit().finally(() => {}); + }, []); + + useEffect(() => { + if (!roadmapId) { + return; + } + + setHasSubmitted(true); + loadAIRoadmap(roadmapId).finally(() => { + pageProgressMessage.set(''); + }); + }, [roadmapId]); + + if (!hasSubmitted) { + return ( + + ); + } + + const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`; + const canGenerateMore = roadmapLimitUsed < roadmapLimit; + + return ( +
+
+ {isLoading && ( + + + Generating roadmap .. + + )} + {!isLoading && ( +
+
+ + + {roadmapLimitUsed} of {roadmapLimit} + {' '} + roadmaps generated + {!isLoggedIn() && ( + <> + {' '} + + + )} + +
+
+ + setRoadmapTopic((e.target as HTMLInputElement).value) + } + /> + +
+
+
+ + {roadmapId && ( + + )} +
+ +
+
+ )} +
+
+
+ ); +} diff --git a/src/components/GenerateRoadmap/RoadmapSearch.tsx b/src/components/GenerateRoadmap/RoadmapSearch.tsx new file mode 100644 index 000000000..0b2ec2bac --- /dev/null +++ b/src/components/GenerateRoadmap/RoadmapSearch.tsx @@ -0,0 +1,121 @@ +import { Ban, Wand } from 'lucide-react'; +import type { FormEvent } from 'react'; +import { isLoggedIn } from '../../lib/jwt'; +import { showLoginPopup } from '../../lib/popup'; +import { cn } from '../../lib/classname.ts'; + +type RoadmapSearchProps = { + roadmapTopic: string; + setRoadmapTopic: (topic: string) => void; + handleSubmit: (e: FormEvent) => void; + limit: number; + limitUsed: number; +}; + +export function RoadmapSearch(props: RoadmapSearchProps) { + const { + roadmapTopic, + setRoadmapTopic, + handleSubmit, + limit = 0, + limitUsed = 0, + } = props; + + const canGenerateMore = limitUsed < limit; + + return ( +
+
+

+ Generate roadmaps with AI + AI Roadmap Generator +

+

+ + Enter a topic and let the AI generate a roadmap for you + + + Enter a topic to generate a roadmap + +

+
+
{ + if (limit > 0 && canGenerateMore) { + handleSubmit(e); + } else { + e.preventDefault(); + } + }} + className="my-3 flex w-full max-w-[600px] flex-col gap-2 sm:my-5 sm:flex-row" + > + setRoadmapTopic((e.target as HTMLInputElement).value)} + /> + +
+
+

+ Generated + You have generated + + {limitUsed} of {limit} + {' '} + roadmaps. + {!isLoggedIn && ( + <> + {' '} + + + )} +

+
+
+ ); +} diff --git a/src/helper/download-image.ts b/src/helper/download-image.ts index 193128cea..db598a49d 100644 --- a/src/helper/download-image.ts +++ b/src/helper/download-image.ts @@ -34,3 +34,35 @@ export async function downloadImage({ alert('Error downloading image'); } } + +export async function downloadGeneratedRoadmapImage( + name: string, + node: HTMLElement, +) { + // Append a watermark to the bottom right of the image + const watermark = document.createElement('div'); + watermark.className = 'flex justify-end absolute top-4 right-4 gap-2'; + watermark.innerHTML = ` + + roadmap.sh + + `; + node.insertAdjacentElement('afterbegin', watermark); + + const domtoimage = (await import('dom-to-image')).default; + if (!domtoimage) { + throw new Error('Unable to download image'); + } + + const dataUrl = await domtoimage.toJpeg(node, { + bgcolor: 'white', + quality: 1, + }); + node?.removeChild(watermark); + const link = document.createElement('a'); + link.download = `${name}-roadmap.jpg`; + link.href = dataUrl; + link.click(); +} diff --git a/src/helper/read-stream.ts b/src/helper/read-stream.ts new file mode 100644 index 000000000..422c89c85 --- /dev/null +++ b/src/helper/read-stream.ts @@ -0,0 +1,43 @@ +const NEW_LINE = '\n'.charCodeAt(0); + +export async function readAIRoadmapStream( + reader: ReadableStreamDefaultReader, + { + onStream, + onStreamEnd, + }: { + onStream?: (roadmap: string) => void; + onStreamEnd?: (roadmap: string) => void; + }, +) { + const decoder = new TextDecoder('utf-8'); + let result = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + // We will call the renderRoadmap callback whenever we encounter + // a new line with the result until the new line + // otherwise, we will keep appending the result to the previous result + if (value) { + let start = 0; + for (let i = 0; i < value.length; i++) { + if (value[i] === NEW_LINE) { + result += decoder.decode(value.slice(start, i + 1)); + onStream?.(result); + start = i + 1; + } + } + if (start < value.length) { + result += decoder.decode(value.slice(start)); + } + } + } + + onStream?.(result); + onStreamEnd?.(result); + reader.releaseLock(); +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 5baa80f82..ae3adfc96 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -149,7 +149,7 @@ const gaPageIdentifier = Astro.url.pathname ) } - + diff --git a/src/pages/ai/index.astro b/src/pages/ai/index.astro new file mode 100644 index 000000000..149e2064a --- /dev/null +++ b/src/pages/ai/index.astro @@ -0,0 +1,10 @@ +--- +import LoginPopup from '../../components/AuthenticationFlow/LoginPopup.astro'; +import { GenerateRoadmap } from '../../components/GenerateRoadmap/GenerateRoadmap'; +import AccountLayout from '../../layouts/AccountLayout.astro'; +--- + + + + + diff --git a/tsconfig.json b/tsconfig.json index c164c57da..0fb7885fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,6 @@ "moduleResolution": "node", "jsx": "react-jsx", "jsxImportSource": "react" - } -} \ No newline at end of file + }, + "exclude": ["node_modules", "dist"] +} From 09cb1ea827542155248648ca2a7528192aa6a192 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 5 Mar 2024 18:34:51 +0000 Subject: [PATCH 14/91] Update custom roadmap message --- src/components/CustomRoadmap/CustomRoadmapAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CustomRoadmap/CustomRoadmapAlert.tsx b/src/components/CustomRoadmap/CustomRoadmapAlert.tsx index ead638713..12c4119ae 100644 --- a/src/components/CustomRoadmap/CustomRoadmapAlert.tsx +++ b/src/components/CustomRoadmap/CustomRoadmapAlert.tsx @@ -21,7 +21,7 @@ export function CustomRoadmapAlert() { Community Roadmap

- This is a custom roadmap made by community and isn't verified by{' '} + This is a custom roadmap made by a community member and is not verified by{' '} roadmap.sh

From cd6232035fdf4019f57a6b62483ac51e41cd19f2 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Mon, 11 Mar 2024 11:14:32 +0600 Subject: [PATCH 15/91] Refactor AI roadmap generator (#5300) * fix: roadmap refetching * fix: remove current roadmap * feat: explore ai roadmaps * feat: generate roadmap content * fix: roadmap topic details * fix: make roadmap link * feat: add visit cookie * chore: update naming * Update UI for roadmap search * Update * Update * UI updates * fix: expire visit cookie in 1 hour * chore: limit roadmap topic content generation * Add alert on generate roadmap * UI for search * Refactor nodesg * Refactor * Load roadmap on click * Refactor UI for ai * Allow overriding with own API key * Allow overriding keys * Add configuration for open ai key * Add open ai saving * Fix responsiveness issues * Fix responsiveness issues --------- Co-authored-by: Kamran Ahmed --- .../ExploreAIRoadmap/ExploreAIRoadmap.tsx | 149 +++++ .../GenerateRoadmap/AIRoadmapAlert.tsx | 53 ++ .../GenerateRoadmap/GenerateRoadmap.tsx | 529 +++++++++++++----- .../GenerateRoadmap/OpenAISettings.tsx | 168 ++++++ .../GenerateRoadmap/RoadmapSearch.tsx | 199 ++++--- .../GenerateRoadmap/RoadmapTopicDetail.tsx | 241 ++++++++ src/helper/read-stream.ts | 30 + src/lib/date.ts | 4 + src/lib/jwt.ts | 36 ++ src/lib/markdown.ts | 21 + src/pages/ai/explore.astro | 10 + 11 files changed, 1224 insertions(+), 216 deletions(-) create mode 100644 src/components/ExploreAIRoadmap/ExploreAIRoadmap.tsx create mode 100644 src/components/GenerateRoadmap/AIRoadmapAlert.tsx create mode 100644 src/components/GenerateRoadmap/OpenAISettings.tsx create mode 100644 src/components/GenerateRoadmap/RoadmapTopicDetail.tsx create mode 100644 src/pages/ai/explore.astro diff --git a/src/components/ExploreAIRoadmap/ExploreAIRoadmap.tsx b/src/components/ExploreAIRoadmap/ExploreAIRoadmap.tsx new file mode 100644 index 000000000..151e7da72 --- /dev/null +++ b/src/components/ExploreAIRoadmap/ExploreAIRoadmap.tsx @@ -0,0 +1,149 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useToast } from '../../hooks/use-toast'; +import { httpGet } from '../../lib/http'; +import { getRelativeTimeString } from '../../lib/date'; +import { Eye, Loader2, RefreshCcw } from 'lucide-react'; +import { AIRoadmapAlert } from '../GenerateRoadmap/AIRoadmapAlert.tsx'; + +export interface AIRoadmapDocument { + _id?: string; + term: string; + title: string; + data: string; + viewCount: number; + createdAt: Date; + updatedAt: Date; +} + +type ExploreRoadmapsResponse = { + data: AIRoadmapDocument[]; + totalCount: number; + totalPages: number; + currPage: number; + perPage: number; +}; + +export function ExploreAIRoadmap() { + const toast = useToast(); + + const [isLoading, setIsLoading] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [roadmaps, setRoadmaps] = useState([]); + const [currPage, setCurrPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + const loadAIRoadmaps = useCallback( + async (currPage: number) => { + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`, + { + currPage, + }, + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + return; + } + + const newRoadmaps = [...roadmaps, ...response.data]; + if ( + JSON.stringify(roadmaps) === JSON.stringify(response.data) || + JSON.stringify(roadmaps) === JSON.stringify(newRoadmaps) + ) { + return; + } + + setRoadmaps(newRoadmaps); + setCurrPage(response.currPage); + setTotalPages(response.totalPages); + }, + [currPage, roadmaps], + ); + + useEffect(() => { + loadAIRoadmaps(currPage).finally(() => { + setIsLoading(false); + }); + }, []); + + const hasMorePages = currPage < totalPages; + + return ( +
+
+ +
+ + {isLoading ? ( +
    + {new Array(21).fill(0).map((_, index) => ( +
  • + ))} +
+ ) : ( +
+ {roadmaps?.length === 0 ? ( +
No roadmaps found
+ ) : ( + <> + + {hasMorePages && ( +
+ +
+ )} + + )} +
+ )} +
+ ); +} diff --git a/src/components/GenerateRoadmap/AIRoadmapAlert.tsx b/src/components/GenerateRoadmap/AIRoadmapAlert.tsx new file mode 100644 index 000000000..4a76c9d7d --- /dev/null +++ b/src/components/GenerateRoadmap/AIRoadmapAlert.tsx @@ -0,0 +1,53 @@ +import { BadgeCheck, Telescope, Wand } from 'lucide-react'; + +type AIRoadmapAlertProps = { + isListing?: boolean; +}; + +export function AIRoadmapAlert(props: AIRoadmapAlertProps) { + const { isListing = false } = props; + + return ( +
+

+ AI Generated Roadmap{isListing ? 's' : ''}{' '} + + Beta + +

+

+ {isListing + ? 'These are AI generated roadmaps and are not verified by' + : 'This is an AI generated roadmap and is not verified by'}{' '} + roadmap.sh. We are currently in + beta and working hard to improve the quality of the generated roadmaps. +

+

+ {isListing ? ( + + + Create your own Roadmap with AI + + ) : ( + + + Explore other AI Roadmaps + + )} + + + Visit Official Roadmaps + +

+
+ ); +} diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx index 7b1269fc9..1f273ead9 100644 --- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -1,14 +1,26 @@ -import { type FormEvent, useEffect, useRef, useState } from 'react'; +import { + type FormEvent, + type MouseEvent, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import './GenerateRoadmap.css'; import { useToast } from '../../hooks/use-toast'; import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator'; import { renderFlowJSON } from '../../../editor/renderer/renderer'; import { replaceChildren } from '../../lib/dom'; import { readAIRoadmapStream } from '../../helper/read-stream'; -import { isLoggedIn, removeAuthToken } from '../../lib/jwt'; +import { + getOpenAIKey, + isLoggedIn, + removeAuthToken, + visitAIRoadmap, +} from '../../lib/jwt'; import { RoadmapSearch } from './RoadmapSearch.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx'; -import { Ban, Download, PenSquare, Wand } from 'lucide-react'; +import { Ban, Cog, Download, PenSquare, Save, Wand } from 'lucide-react'; import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; import { httpGet, httpPost } from '../../lib/http.ts'; import { pageProgressMessage } from '../../stores/page.ts'; @@ -20,9 +32,55 @@ import { import { downloadGeneratedRoadmapImage } from '../../helper/download-image.ts'; import { showLoginPopup } from '../../lib/popup.ts'; import { cn } from '../../lib/classname.ts'; +import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; +import { AIRoadmapAlert } from './AIRoadmapAlert.tsx'; +import { OpenAISettings } from './OpenAISettings.tsx'; + +export type GetAIRoadmapLimitResponse = { + used: number; + limit: number; + topicUsed: number; + topicLimit: number; +}; const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@'); +export type RoadmapNodeDetails = { + nodeId: string; + nodeType: string; + targetGroup?: SVGElement; + nodeTitle?: string; + parentTitle?: string; +}; + +export function getNodeDetails( + svgElement: SVGElement, +): RoadmapNodeDetails | null { + const targetGroup = (svgElement?.closest('g') as SVGElement) || {}; + + const nodeId = targetGroup?.dataset?.nodeId; + const nodeType = targetGroup?.dataset?.type; + const nodeTitle = targetGroup?.dataset?.title; + const parentTitle = targetGroup?.dataset?.parentTitle; + if (!nodeId || !nodeType) return null; + + return { nodeId, nodeType, targetGroup, nodeTitle, parentTitle }; +} + +export const allowedClickableNodeTypes = [ + 'topic', + 'subtopic', + 'button', + 'link-item', +]; + +type GetAIRoadmapResponse = { + id: string; + term: string; + title: string; + data: string; +}; + export function GenerateRoadmap() { const roadmapContainerRef = useRef(null); @@ -31,11 +89,21 @@ export function GenerateRoadmap() { const [hasSubmitted, setHasSubmitted] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [roadmapTopic, setRoadmapTopic] = useState(''); - const [generatedRoadmap, setGeneratedRoadmap] = useState(''); + const [roadmapTerm, setRoadmapTerm] = useState(''); + const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState(''); + const [currentRoadmap, setCurrentRoadmap] = + useState(null); + const [selectedNode, setSelectedNode] = useState( + null, + ); const [roadmapLimit, setRoadmapLimit] = useState(0); const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0); + const [roadmapTopicLimit, setRoadmapTopicLimit] = useState(0); + const [roadmapTopicLimitUsed, setRoadmapTopicLimitUsed] = useState(0); + const [isConfiguring, setIsConfiguring] = useState(false); + + const openAPIKey = getOpenAIKey(); const renderRoadmap = async (roadmap: string) => { const { nodes, edges } = generateAIRoadmapFromText(roadmap); @@ -45,12 +113,7 @@ export function GenerateRoadmap() { } }; - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - if (!roadmapTopic) { - return; - } - + const loadTermRoadmap = async (term: string) => { setIsLoading(true); setHasSubmitted(true); @@ -61,6 +124,7 @@ export function GenerateRoadmap() { } deleteUrlParam('id'); + setCurrentRoadmap(null); const response = await fetch( `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`, @@ -70,7 +134,7 @@ export function GenerateRoadmap() { 'Content-Type': 'application/json', }, credentials: 'include', - body: JSON.stringify({ topic: roadmapTopic }), + body: JSON.stringify({ term }), }, ); @@ -104,13 +168,19 @@ export function GenerateRoadmap() { const roadmapId = result.match(ROADMAP_ID_REGEX)?.[1] || ''; setUrlParams({ id: roadmapId }); result = result.replace(ROADMAP_ID_REGEX, ''); + setCurrentRoadmap({ + id: roadmapId, + term: roadmapTerm, + title: term, + data: result, + }); } await renderRoadmap(result); }, onStreamEnd: async (result) => { result = result.replace(ROADMAP_ID_REGEX, ''); - setGeneratedRoadmap(result); + setGeneratedRoadmapContent(result); loadAIRoadmapLimit().finally(() => {}); }, }); @@ -118,7 +188,20 @@ export function GenerateRoadmap() { setIsLoading(false); }; - const editGeneratedRoadmap = async () => { + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!roadmapTerm) { + return; + } + + if (roadmapTerm === currentRoadmap?.topic) { + return; + } + + loadTermRoadmap(roadmapTerm).finally(() => null); + }; + + const saveAIRoadmap = async () => { if (!isLoggedIn()) { showLoginPopup(); return; @@ -126,38 +209,44 @@ export function GenerateRoadmap() { pageProgressMessage.set('Redirecting to Editor'); - const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmap); + const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmapContent); const { response, error } = await httpPost<{ roadmapId: string; - }>(`${import.meta.env.PUBLIC_API_URL}/v1-edit-ai-generated-roadmap`, { - title: roadmapTopic, - nodes: nodes.map((node) => ({ - ...node, - - // To reset the width and height of the node - // so that it can be calculated based on the content in the editor - width: undefined, - height: undefined, - style: { - ...node.style, + }>( + `${import.meta.env.PUBLIC_API_URL}/v1-save-ai-roadmap/${currentRoadmap?.id}`, + { + title: roadmapTerm, + nodes: nodes.map((node) => ({ + ...node, + + // To reset the width and height of the node + // so that it can be calculated based on the content in the editor width: undefined, height: undefined, - }, - })), - edges, - }); + style: { + ...node.style, + width: undefined, + height: undefined, + }, + })), + edges, + }, + ); if (error || !response) { toast.error(error?.message || 'Something went wrong'); + pageProgressMessage.set(''); setIsLoading(false); return; } - window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${response.roadmapId}`; + setIsLoading(false); + pageProgressMessage.set(''); + return response.roadmapId; }; - const downloadGeneratedRoadmap = async () => { + const downloadGeneratedRoadmapContent = async () => { pageProgressMessage.set('Downloading Roadmap'); const node = document.getElementById('roadmap-container'); @@ -167,7 +256,7 @@ export function GenerateRoadmap() { } try { - await downloadGeneratedRoadmapImage(roadmapTopic, node); + await downloadGeneratedRoadmapImage(roadmapTerm, node); pageProgressMessage.set(''); } catch (error) { console.error(error); @@ -176,19 +265,20 @@ export function GenerateRoadmap() { }; const loadAIRoadmapLimit = async () => { - const { response, error } = await httpGet<{ - limit: number; - used: number; - }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-limit`); + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-limit`, + ); if (error || !response) { toast.error(error?.message || 'Something went wrong'); return; } - const { limit, used } = response; + const { limit, used, topicLimit, topicUsed } = response; setRoadmapLimit(limit); setRoadmapLimitUsed(used); + setRoadmapTopicLimit(topicLimit); + setRoadmapTopicLimitUsed(topicUsed); }; const loadAIRoadmap = async (roadmapId: string) => { @@ -205,19 +295,65 @@ export function GenerateRoadmap() { return; } - const { topic, data } = response; + const { term, title, data } = response; await renderRoadmap(data); - setRoadmapTopic(topic); - setGeneratedRoadmap(data); + setCurrentRoadmap({ + id: roadmapId, + title: title, + term: term, + data, + }); + + setRoadmapTerm(title); + setGeneratedRoadmapContent(data); + visitAIRoadmap(roadmapId); }; + const handleNodeClick = useCallback( + (e: MouseEvent) => { + if (isLoading) { + return; + } + + const target = e.target as SVGElement; + const { nodeId, nodeType, targetGroup, nodeTitle, parentTitle } = + getNodeDetails(target) || {}; + if ( + !nodeId || + !nodeType || + !allowedClickableNodeTypes.includes(nodeType) || + !nodeTitle + ) + return; + + if (nodeType === 'button' || nodeType === 'link-item') { + const link = targetGroup?.dataset?.link || ''; + const isExternalLink = link.startsWith('http'); + if (isExternalLink) { + window.open(link, '_blank'); + } else { + window.location.href = link; + } + return; + } + + setSelectedNode({ + nodeId, + nodeType, + nodeTitle, + ...(nodeType === 'subtopic' && { parentTitle }), + }); + }, + [isLoading], + ); + useEffect(() => { loadAIRoadmapLimit().finally(() => {}); }, []); useEffect(() => { - if (!roadmapId) { + if (!roadmapId || roadmapId === currentRoadmap?.id) { return; } @@ -225,137 +361,230 @@ export function GenerateRoadmap() { loadAIRoadmap(roadmapId).finally(() => { pageProgressMessage.set(''); }); - }, [roadmapId]); + }, [roadmapId, currentRoadmap]); if (!hasSubmitted) { return ( { + setRoadmapTerm(term); + loadTermRoadmap(term).finally(() => {}); + }} /> ); } const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`; const canGenerateMore = roadmapLimitUsed < roadmapLimit; + const isLoggedInUser = isLoggedIn(); return ( -
-
- {isLoading && ( - - - Generating roadmap .. - - )} - {!isLoading && ( -
-
- - - {roadmapLimitUsed} of {roadmapLimit} - {' '} - roadmaps generated - {!isLoggedIn() && ( - <> - {' '} - - + <> + {isConfiguring && ( + { + setIsConfiguring(false); + loadAIRoadmapLimit().finally(() => null); + }} + /> + )} + + {selectedNode && currentRoadmap && !isLoading && ( + { + setSelectedNode(null); + setIsConfiguring(true); + }} + onClose={() => { + setSelectedNode(null); + loadAIRoadmapLimit().finally(() => {}); + }} + roadmapId={currentRoadmap?.id || ''} + topicLimit={roadmapTopicLimit} + topicLimitUsed={roadmapTopicLimitUsed} + onTopicContentGenerateComplete={async () => { + await loadAIRoadmapLimit(); + }} + /> + )} + +
+
+ {isLoading && ( + + + Generating roadmap .. + + )} + {!isLoading && ( +
+ +
+ + + {roadmapLimitUsed} of {roadmapLimit} + {' '} + roadmaps generated. + + {!isLoggedInUser && ( + )} - -
-
- - setRoadmapTopic((e.target as HTMLInputElement).value) - } - /> - )} - {roadmapLimit === 0 && Please wait..} - - {roadmapLimit > 0 && !canGenerateMore && ( - - - Limit reached - + {isLoggedInUser && openAPIKey && ( + )} - -
-
-
+
+
+ + setRoadmapTerm((e.target as HTMLInputElement).value) + } + /> - {roadmapId && ( - - )} + +
+
+ + {roadmapId && ( + + )} +
+ +
+ + + +
-
-
- )} -
-
-
+ )} +
+
+
+ ); } diff --git a/src/components/GenerateRoadmap/OpenAISettings.tsx b/src/components/GenerateRoadmap/OpenAISettings.tsx new file mode 100644 index 000000000..2b30e3f98 --- /dev/null +++ b/src/components/GenerateRoadmap/OpenAISettings.tsx @@ -0,0 +1,168 @@ +import { Modal } from '../Modal.tsx'; +import { useEffect, useState } from 'react'; +import { + deleteOpenAIKey, + getOpenAIKey, + saveOpenAIKey, +} from '../../lib/jwt.ts'; +import { cn } from '../../lib/classname.ts'; +import { CloseIcon } from '../ReactIcons/CloseIcon.tsx'; +import { useToast } from '../../hooks/use-toast.ts'; +import { httpPost } from '../../lib/http.ts'; + +type OpenAISettingsProps = { + onClose: () => void; +}; + +export function OpenAISettings(props: OpenAISettingsProps) { + const { onClose } = props; + + const [defaultOpenAIKey, setDefaultOpenAIKey] = useState(''); + + const [hasError, setHasError] = useState(false); + const [openaiApiKey, setOpenaiApiKey] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const toast = useToast(); + + useEffect(() => { + const apiKey = getOpenAIKey(); + setOpenaiApiKey(apiKey || ''); + setDefaultOpenAIKey(apiKey || ''); + }, []); + + return ( + +
+

OpenAI Settings

+
+

+ AI Roadmap generator uses OpenAI's GPT-4 model to generate roadmaps. +

+ +

+ + Create an account on OpenAI + {' '} + and enter your API key below to enable the AI Roadmap generator +

+ +
{ + e.preventDefault(); + setHasError(false); + + const normalizedKey = openaiApiKey.trim(); + if (!normalizedKey) { + deleteOpenAIKey(); + toast.success('OpenAI API key removed'); + onClose(); + return; + } + + if (!normalizedKey.startsWith('sk-')) { + setHasError(true); + return; + } + + setIsLoading(true); + const { response, error } = await httpPost( + `${import.meta.env.PUBLIC_API_URL}/v1-validate-openai-key`, + { + key: normalizedKey, + }, + ); + + if (error) { + setHasError(true); + setIsLoading(false); + return; + } + + // Save the API key to cookies + saveOpenAIKey(normalizedKey); + toast.success('OpenAI API key saved'); + onClose(); + }} + > +
+ { + setHasError(false); + setOpenaiApiKey((e.target as HTMLInputElement).value); + }} + /> + + {openaiApiKey && ( + + )} +
+ {hasError && ( +

+ Please enter a valid OpenAI API key +

+ )} + + {!defaultOpenAIKey && ( + + )} + {defaultOpenAIKey && ( + + )} +
+
+
+
+ ); +} diff --git a/src/components/GenerateRoadmap/RoadmapSearch.tsx b/src/components/GenerateRoadmap/RoadmapSearch.tsx index 0b2ec2bac..6e85b43ba 100644 --- a/src/components/GenerateRoadmap/RoadmapSearch.tsx +++ b/src/components/GenerateRoadmap/RoadmapSearch.tsx @@ -1,30 +1,55 @@ -import { Ban, Wand } from 'lucide-react'; +import { + ArrowUpRight, + Ban, + CircleFadingPlus, + Cog, + Telescope, + Wand, +} from 'lucide-react'; import type { FormEvent } from 'react'; -import { isLoggedIn } from '../../lib/jwt'; +import { getOpenAIKey, isLoggedIn } from '../../lib/jwt'; import { showLoginPopup } from '../../lib/popup'; import { cn } from '../../lib/classname.ts'; +import { useState } from 'react'; +import { OpenAISettings } from './OpenAISettings.tsx'; type RoadmapSearchProps = { - roadmapTopic: string; - setRoadmapTopic: (topic: string) => void; + roadmapTerm: string; + setRoadmapTerm: (topic: string) => void; handleSubmit: (e: FormEvent) => void; + loadAIRoadmapLimit: () => void; + onLoadTerm: (topic: string) => void; limit: number; limitUsed: number; }; export function RoadmapSearch(props: RoadmapSearchProps) { const { - roadmapTopic, - setRoadmapTopic, + roadmapTerm, + setRoadmapTerm, handleSubmit, limit = 0, limitUsed = 0, + onLoadTerm, + loadAIRoadmapLimit, } = props; const canGenerateMore = limitUsed < limit; + const [isConfiguring, setIsConfiguring] = useState(false); + const openAPIKey = getOpenAIKey(); + + const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC']; return (
+ {isConfiguring && ( + { + setIsConfiguring(false); + loadAIRoadmapLimit(); + }} + /> + )}

Generate roadmaps with AI @@ -39,61 +64,77 @@ export function RoadmapSearch(props: RoadmapSearchProps) {

-
{ - if (limit > 0 && canGenerateMore) { - handleSubmit(e); - } else { - e.preventDefault(); - } - }} - className="my-3 flex w-full max-w-[600px] flex-col gap-2 sm:my-5 sm:flex-row" - > - setRoadmapTopic((e.target as HTMLInputElement).value)} - /> - -
-
+ {limit > 0 && !canGenerateMore && ( + + + Limit reached + + )} + + +
+ {randomTerms.map((term) => ( + + ))} + + Explore AI Roadmaps + +
+
+

- Generated - You have generated + You have generated{' '} {' '} roadmaps. - {!isLoggedIn && ( - <> - {' '} - - +

+

+ {limit > 0 && !isLoggedIn() && ( + + )} +

+

+ {limit > 0 && isLoggedIn() && !openAPIKey && ( + + )} + + {limit > 0 && isLoggedIn() && openAPIKey && ( + )}

diff --git a/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx b/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx new file mode 100644 index 000000000..f3db8525c --- /dev/null +++ b/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx @@ -0,0 +1,241 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { useKeydown } from '../../hooks/use-keydown'; +import { useOutsideClick } from '../../hooks/use-outside-click'; +import { markdownToHtml } from '../../lib/markdown'; +import { Ban, Cog, FileText, X } from 'lucide-react'; +import { Spinner } from '../ReactIcons/Spinner'; +import type { RoadmapNodeDetails } from './GenerateRoadmap'; +import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt'; +import { readAIRoadmapContentStream } from '../../helper/read-stream'; +import { cn } from '../../lib/classname'; +import { showLoginPopup } from '../../lib/popup'; +import { OpenAISettings } from './OpenAISettings.tsx'; + +type RoadmapTopicDetailProps = RoadmapNodeDetails & { + onClose?: () => void; + roadmapId: string; + topicLimitUsed: number; + topicLimit: number; + onTopicContentGenerateComplete?: () => void; + onConfigureOpenAI?: () => void; +}; + +export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) { + const { + onClose, + roadmapId, + nodeTitle, + parentTitle, + topicLimit, + topicLimitUsed, + onTopicContentGenerateComplete, + onConfigureOpenAI, + } = props; + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [topicHtml, setTopicHtml] = useState(''); + + const topicRef = useRef(null); + + const abortController = useMemo(() => new AbortController(), []); + const generateAiRoadmapTopicContent = async () => { + setIsLoading(true); + setError(''); + // + // if (topicLimitUsed >= topicLimit) { + // setError('Maximum limit reached'); + // setIsLoading(false); + // return; + // } + + if (!roadmapId || !nodeTitle) { + setIsLoading(false); + setError('Invalid roadmap id or node title'); + return; + } + + const response = await fetch( + `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap-content/${roadmapId}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + nodeTitle, + parentTitle, + }), + signal: abortController.signal, + }, + ); + + if (!response.ok) { + const data = await response.json(); + + setError(data?.message || 'Something went wrong'); + setIsLoading(false); + + // Logout user if token is invalid + if (data.status === 401) { + removeAuthToken(); + window.location.reload(); + } + } + const reader = response.body?.getReader(); + + if (!reader) { + setIsLoading(false); + setError('Something went wrong'); + return; + } + + setIsLoading(false); + await readAIRoadmapContentStream(reader, { + onStream: async (result) => { + setTopicHtml(markdownToHtml(result, false)); + }, + }); + onTopicContentGenerateComplete?.(); + }; + + // Close the topic detail when user clicks outside the topic detail + useOutsideClick(topicRef, () => { + onClose?.(); + }); + + useKeydown('Escape', () => { + onClose?.(); + }); + + useEffect(() => { + if (!topicRef?.current) { + return; + } + + topicRef?.current?.focus(); + generateAiRoadmapTopicContent().finally(() => {}); + + return () => { + abortController.abort(); + }; + }, []); + + const hasContent = topicHtml?.length > 0; + const openAIKey = getOpenAIKey(); + + return ( +
+
+
+ + + {topicLimitUsed} of {topicLimit} + {' '} + topics generated + + {!isLoggedIn() && ( + + )} + {isLoggedIn() && !openAIKey && ( + + )} + {isLoggedIn() && openAIKey && ( + + )} +
+ + {isLoading && ( +
+ +
+ )} + + {!isLoading && !error && ( + <> +
+ +
+ + {hasContent ? ( +
+
+
+ ) : ( +
+ +

+ Empty Content +

+
+ )} + + )} + + {/* Error */} + {!isLoading && error && ( + <> + +
+ +

{error}

+
+ + )} +
+
+
+ ); +} diff --git a/src/helper/read-stream.ts b/src/helper/read-stream.ts index 422c89c85..2df821443 100644 --- a/src/helper/read-stream.ts +++ b/src/helper/read-stream.ts @@ -41,3 +41,33 @@ export async function readAIRoadmapStream( onStreamEnd?.(result); reader.releaseLock(); } + +export async function readAIRoadmapContentStream( + reader: ReadableStreamDefaultReader, + { + onStream, + onStreamEnd, + }: { + onStream?: (roadmap: string) => void; + onStreamEnd?: (roadmap: string) => void; + }, +) { + const decoder = new TextDecoder('utf-8'); + let result = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + if (value) { + result += decoder.decode(value); + onStream?.(result); + } + } + + onStream?.(result); + onStreamEnd?.(result); + reader.releaseLock(); +} diff --git a/src/lib/date.ts b/src/lib/date.ts index 07a0b4d40..4df386da3 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -26,5 +26,9 @@ export function getRelativeTimeString(date: string): string { relativeTime = rtf.format(-diffInDays, 'day'); } + if (relativeTime === 'this minute') { + return 'just now'; + } + return relativeTime; } diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts index 911c46312..d3206b083 100644 --- a/src/lib/jwt.ts +++ b/src/lib/jwt.ts @@ -48,3 +48,39 @@ export function removeAuthToken() { domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', }); } + +export function visitAIRoadmap(roadmapId: string) { + const isAlreadyVisited = Number(Cookies.get(`crv-${roadmapId}`) || 0) === 1; + if (isAlreadyVisited) { + return; + } + + Cookies.set(`crv-${roadmapId}`, '1', { + path: '/', + expires: 1 / 24, // 1 hour + sameSite: 'lax', + secure: !import.meta.env.DEV, + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} + +export function deleteOpenAIKey() { + Cookies.remove('oak', { + path: '/', + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} + +export function saveOpenAIKey(apiKey: string) { + Cookies.set('oak', apiKey, { + path: '/', + expires: 365, + sameSite: 'lax', + secure: true, + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} + +export function getOpenAIKey() { + return Cookies.get('oak'); +} diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 474c08174..b7114f012 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -8,6 +8,27 @@ export function markdownToHtml(markdown: string, isInline = true): string { linkify: true, }); + // Solution to open links in new tab in markdown + // otherwise default behaviour is to open in same tab + // + // SOURCE: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer + // + const defaultRender = + md.renderer.rules.link_open || + // @ts-ignore + function (tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + // @ts-ignore + md.renderer.rules.link_open = function (tokens, idx, options, env, self) { + // Add a new `target` attribute, or replace the value of the existing one. + tokens[idx].attrSet('target', '_blank'); + + // Pass the token to the default renderer. + return defaultRender(tokens, idx, options, env, self); + }; + if (isInline) { return md.renderInline(markdown); } else { diff --git a/src/pages/ai/explore.astro b/src/pages/ai/explore.astro new file mode 100644 index 000000000..8d8461f69 --- /dev/null +++ b/src/pages/ai/explore.astro @@ -0,0 +1,10 @@ +--- +import LoginPopup from '../../components/AuthenticationFlow/LoginPopup.astro'; +import { ExploreAIRoadmap } from '../../components/ExploreAIRoadmap/ExploreAIRoadmap'; +import AccountLayout from '../../layouts/AccountLayout.astro'; +--- + + + + + From da526fa684284edf7170ef7e3cdcd4a1d6413cd3 Mon Sep 17 00:00:00 2001 From: boc-the-git <3479092+boc-the-git@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:14:52 +1100 Subject: [PATCH 16/91] Remove duplication (#5311) --- .../101-typescript-types/115-type-assertions/101-as-type.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md b/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md index 6a6767aa9..e446798b7 100644 --- a/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md +++ b/src/data/roadmaps/typescript/content/101-typescript-types/115-type-assertions/101-as-type.md @@ -11,8 +11,8 @@ let strLength: number = (someValue as string).length; console.log(strLength); // Outputs: 18 ``` -In this example, someValue is initially of type any, and we use the as operator to assert that it is of type string before accessing its length property. It's important to note that type assertions do not change the underlying runtime representation; they are a compile-time construct used for static type checking in TypeScript. +In this example, someValue is initially of type any, and we use the as operator to assert that it is of type string before accessing its length property. -It's important to note that type assertions do not change the runtime type of a value, and do not cause any type of conversion. They simply provide a way for the programmer to override the type inference performed by the compiler. +It's important to note that type assertions do not change the runtime type of a value, and do not cause any type of conversion. They are a compile-time construct used for static type checking in TypeScript. - [Type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) From c9703c8589b2102a11ea86cce8e76bb4b6b66696 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Mon, 11 Mar 2024 12:12:06 +0000 Subject: [PATCH 17/91] Fix flicker on AI page --- src/components/GenerateRoadmap/RoadmapSearch.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/GenerateRoadmap/RoadmapSearch.tsx b/src/components/GenerateRoadmap/RoadmapSearch.tsx index 6e85b43ba..b9ddafe72 100644 --- a/src/components/GenerateRoadmap/RoadmapSearch.tsx +++ b/src/components/GenerateRoadmap/RoadmapSearch.tsx @@ -130,7 +130,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
-

+

You have generated{' '} {' '} roadmaps.

-

+

{limit > 0 && !isLoggedIn() && ( )}

-

+

{limit > 0 && isLoggedIn() && !openAPIKey && (

); diff --git a/src/components/HeroSection/HeroRoadmaps.tsx b/src/components/HeroSection/HeroRoadmaps.tsx index eb60fdaf1..8f92e4a76 100644 --- a/src/components/HeroSection/HeroRoadmaps.tsx +++ b/src/components/HeroSection/HeroRoadmaps.tsx @@ -7,7 +7,7 @@ import { MapIcon, Users2 } from 'lucide-react'; import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; import { type ReactNode, useState } from 'react'; -import { TeamAnnouncement } from '../TeamAnnouncement'; +import { AIAnnouncement } from '../AIAnnouncement.tsx'; type ProgressRoadmapProps = { url: string; @@ -97,7 +97,7 @@ export function HeroRoadmaps(props: ProgressListProps) { return (

- +

{isCreatingRoadmap && ( !!roadmap.resourceTitle + (roadmap) => !!roadmap.resourceTitle, ); const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!); diff --git a/src/components/HeroSection/HeroSection.astro b/src/components/HeroSection/HeroSection.astro index f677bcac9..52f5e4a4c 100644 --- a/src/components/HeroSection/HeroSection.astro +++ b/src/components/HeroSection/HeroSection.astro @@ -1,17 +1,17 @@ --- import { FavoriteRoadmaps } from './FavoriteRoadmaps'; -import {TeamAnnouncement} from "../TeamAnnouncement"; +import { AIAnnouncement } from "../AIAnnouncement"; ---
-

- +

+

Date: Tue, 12 Mar 2024 00:28:49 +0000 Subject: [PATCH 20/91] Change alert color --- src/components/AIAnnouncement.tsx | 2 +- src/pages/ai/explore.astro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AIAnnouncement.tsx b/src/components/AIAnnouncement.tsx index c5054b538..481e9cfcf 100644 --- a/src/components/AIAnnouncement.tsx +++ b/src/components/AIAnnouncement.tsx @@ -3,7 +3,7 @@ type AIAnnouncementProps = {}; export function AIAnnouncement(props: AIAnnouncementProps) { return ( diff --git a/src/pages/ai/explore.astro b/src/pages/ai/explore.astro index 8d8461f69..c501f613f 100644 --- a/src/pages/ai/explore.astro +++ b/src/pages/ai/explore.astro @@ -4,7 +4,7 @@ import { ExploreAIRoadmap } from '../../components/ExploreAIRoadmap/ExploreAIRoa import AccountLayout from '../../layouts/AccountLayout.astro'; --- - + From 64cfe503af6db85d155fde2455a7c9c23ec35b4b Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 12 Mar 2024 15:26:13 +0000 Subject: [PATCH 21/91] Block UI for guest users --- .../GenerateRoadmap/GenerateRoadmap.tsx | 134 +++++++----- .../GenerateRoadmap/OpenAISettings.tsx | 10 +- .../GenerateRoadmap/RoadmapSearch.tsx | 202 +++++++++++++----- src/lib/ai.ts | 1 + 4 files changed, 235 insertions(+), 112 deletions(-) create mode 100644 src/lib/ai.ts diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx index 174e8c3b5..e935e3a07 100644 --- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -35,6 +35,7 @@ import { cn } from '../../lib/classname.ts'; import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; import { AIRoadmapAlert } from './AIRoadmapAlert.tsx'; import { OpenAISettings } from './OpenAISettings.tsx'; +import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts'; export type GetAIRoadmapLimitResponse = { used: number; @@ -104,6 +105,7 @@ export function GenerateRoadmap() { const [isConfiguring, setIsConfiguring] = useState(false); const openAPIKey = getOpenAIKey(); + const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION; const renderRoadmap = async (roadmap: string) => { const { nodes, edges } = generateAIRoadmapFromText(roadmap); @@ -372,6 +374,7 @@ export function GenerateRoadmap() { limit={roadmapLimit} limitUsed={roadmapLimitUsed} loadAIRoadmapLimit={loadAIRoadmapLimit} + isKeyOnly={isKeyOnly} onLoadTerm={(term: string) => { setRoadmapTerm(term); loadTermRoadmap(term).finally(() => {}); @@ -427,63 +430,93 @@ export function GenerateRoadmap() { )} {!isLoading && ( -
+
-
- - - {roadmapLimitUsed} of {roadmapLimit} - {' '} - roadmaps generated. - - {!isLoggedInUser && ( - +

+ )} + {openAPIKey && ( +

+ You have added your own OpenAI API key.{' '} + +

+ )} +
+ )} + {!isKeyOnly && ( +
+ + + {roadmapLimitUsed} of {roadmapLimit} {' '} - or logging in - - )} - {isLoggedInUser && !openAPIKey && ( - - )} + roadmaps generated. + + {!isLoggedInUser && ( + + )} + {isLoggedInUser && !openAPIKey && ( + + )} - {isLoggedInUser && openAPIKey && ( - - )} -
+ {isLoggedInUser && openAPIKey && ( + + )} +
+ )}
= roadmapLimit || - roadmapTerm === currentRoadmap?.term + roadmapTerm === currentRoadmap?.term || + (isKeyOnly && !openAPIKey) } > {roadmapLimit > 0 && canGenerateMore && ( @@ -514,7 +548,7 @@ export function GenerateRoadmap() { {roadmapLimit === 0 && Please wait..} {roadmapLimit > 0 && !canGenerateMore && ( - + Limit reached diff --git a/src/components/GenerateRoadmap/OpenAISettings.tsx b/src/components/GenerateRoadmap/OpenAISettings.tsx index 2b30e3f98..29c8e0820 100644 --- a/src/components/GenerateRoadmap/OpenAISettings.tsx +++ b/src/components/GenerateRoadmap/OpenAISettings.tsx @@ -1,10 +1,6 @@ import { Modal } from '../Modal.tsx'; import { useEffect, useState } from 'react'; -import { - deleteOpenAIKey, - getOpenAIKey, - saveOpenAIKey, -} from '../../lib/jwt.ts'; +import { deleteOpenAIKey, getOpenAIKey, saveOpenAIKey } from '../../lib/jwt.ts'; import { cn } from '../../lib/classname.ts'; import { CloseIcon } from '../ReactIcons/CloseIcon.tsx'; import { useToast } from '../../hooks/use-toast.ts'; @@ -121,6 +117,10 @@ export function OpenAISettings(props: OpenAISettingsProps) { )}
+

+ We do not store your API key on our servers. +

+ {hasError && (

Please enter a valid OpenAI API key diff --git a/src/components/GenerateRoadmap/RoadmapSearch.tsx b/src/components/GenerateRoadmap/RoadmapSearch.tsx index 1797608c5..152b067cc 100644 --- a/src/components/GenerateRoadmap/RoadmapSearch.tsx +++ b/src/components/GenerateRoadmap/RoadmapSearch.tsx @@ -21,6 +21,7 @@ type RoadmapSearchProps = { onLoadTerm: (topic: string) => void; limit: number; limitUsed: number; + isKeyOnly: boolean; }; export function RoadmapSearch(props: RoadmapSearchProps) { @@ -32,16 +33,18 @@ export function RoadmapSearch(props: RoadmapSearchProps) { limitUsed = 0, onLoadTerm, loadAIRoadmapLimit, + isKeyOnly, } = props; const canGenerateMore = limitUsed < limit; const [isConfiguring, setIsConfiguring] = useState(false); const openAPIKey = getOpenAIKey(); + const isAuthenticatedUser = isLoggedIn(); const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC']; return ( -

+
{isConfiguring && ( { @@ -50,7 +53,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) { }} /> )} -
+

Generate roadmaps with AI AI Roadmap Generator @@ -90,30 +93,57 @@ export function RoadmapSearch(props: RoadmapSearchProps) { 'flex min-w-[154px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white', 'disabled:cursor-not-allowed disabled:opacity-50', )} - disabled={!limit || !roadmapTerm || limitUsed >= limit} + onClick={(e) => { + if (!isAuthenticatedUser) { + e.preventDefault(); + showLoginPopup(); + } + }} + disabled={ + isAuthenticatedUser && + (!limit || + !roadmapTerm || + limitUsed >= limit || + (isKeyOnly && !openAPIKey)) + } > - {(!limit || canGenerateMore) && ( + {!isAuthenticatedUser && ( <> Generate )} + {isAuthenticatedUser && ( + <> + {(!limit || canGenerateMore) && ( + <> + + Generate + + )} - {limit > 0 && !canGenerateMore && ( - - - Limit reached - + {limit > 0 && !canGenerateMore && ( + + + Limit reached + + )} + )} - -
-

- You have generated{' '} - - {limitUsed} of {limit} - {' '} - roadmaps. -

-

- {limit > 0 && !isLoggedIn() && ( + {!isAuthenticatedUser && ( +

+ )} + {isKeyOnly && isAuthenticatedUser && ( +
+ {!openAPIKey && ( + <> +

+ We have hit the limit for AI roadmap generation. Please try + again later or{' '} + +

+ + )} + {openAPIKey && ( +

+ You have added your own OpenAI API key.{' '} + +

)} - {limit > 0 && isLoggedIn() && openAPIKey && ( - + Explore AI Roadmaps + + + Visit Official Roadmaps + +

+
+ )} + {!isKeyOnly && limit > 0 && isAuthenticatedUser && ( +
+

+ You have generated{' '} + + {limitUsed} of {limit} + {' '} + roadmaps. +

+ {isAuthenticatedUser && ( +

+ {!openAPIKey && ( + + )} + + {openAPIKey && ( + + )} +

)} -

-
+
+ )}

); } diff --git a/src/lib/ai.ts b/src/lib/ai.ts new file mode 100644 index 000000000..6481f0f2c --- /dev/null +++ b/src/lib/ai.ts @@ -0,0 +1 @@ +export const IS_KEY_ONLY_ROADMAP_GENERATION = false; \ No newline at end of file From f838b5dac77aef79668cc7e72c3febf922db8ef6 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 12 Mar 2024 15:36:37 +0000 Subject: [PATCH 22/91] UI changes for logged out users --- .../GenerateRoadmap/GenerateRoadmap.tsx | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx index e935e3a07..fae9ea50f 100644 --- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -106,6 +106,7 @@ export function GenerateRoadmap() { const openAPIKey = getOpenAIKey(); const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION; + const isAuthenticatedUser = isLoggedIn(); const renderRoadmap = async (roadmap: string) => { const { nodes, edges } = generateAIRoadmapFromText(roadmap); @@ -249,6 +250,11 @@ export function GenerateRoadmap() { }; const downloadGeneratedRoadmapContent = async () => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + pageProgressMessage.set('Downloading Roadmap'); const node = document.getElementById('roadmap-container'); @@ -432,7 +438,7 @@ export function GenerateRoadmap() { {!isLoading && (
- {isKeyOnly && ( + {isKeyOnly && isAuthenticatedUser && (
{!openAPIKey && (

@@ -459,7 +465,7 @@ export function GenerateRoadmap() { )}

)} - {!isKeyOnly && ( + {!isKeyOnly && isAuthenticatedUser && (
{' '} roadmaps generated. - {!isLoggedInUser && ( - - )} - {isLoggedInUser && !openAPIKey && ( + {!openAPIKey && ( )} - {isLoggedInUser && openAPIKey && ( + {openAPIKey && (
)} + {!isAuthenticatedUser && ( + + )}
{ + if (!isAuthenticatedUser) { + e.preventDefault(); + showLoginPopup(); + } + }} disabled={ - !roadmapLimit || - !roadmapTerm || - roadmapLimitUsed >= roadmapLimit || - roadmapTerm === currentRoadmap?.term || - (isKeyOnly && !openAPIKey) + isAuthenticatedUser && + (!roadmapLimit || + !roadmapTerm || + roadmapLimitUsed >= roadmapLimit || + roadmapTerm === currentRoadmap?.term || + (isKeyOnly && !openAPIKey)) } > - {roadmapLimit > 0 && canGenerateMore && ( + {!isAuthenticatedUser && ( <> Generate )} - {roadmapLimit === 0 && Please wait..} + {isAuthenticatedUser && ( + <> + {roadmapLimit > 0 && canGenerateMore && ( + <> + + Generate + + )} - {roadmapLimit > 0 && !canGenerateMore && ( - - - Limit reached - + {roadmapLimit === 0 && Please wait..} + + {roadmapLimit > 0 && !canGenerateMore && ( + + + Limit reached + + )} + )} From c3a61e7f3464a71e689dfe9e42ecc45fddff36c5 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Fri, 15 Mar 2024 03:13:53 +0000 Subject: [PATCH 23/91] Add json ld for author pages --- src/data/authors/kamran.md | 3 ++ src/layouts/BaseLayout.astro | 5 ++- src/lib/author.ts | 4 +++ src/pages/authors/[authorId].astro | 57 ++++++++++++++++++++---------- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/data/authors/kamran.md b/src/data/authors/kamran.md index 8e5f7c9ee..5b80785c4 100644 --- a/src/data/authors/kamran.md +++ b/src/data/authors/kamran.md @@ -1,6 +1,9 @@ --- name: 'Kamran Ahmed' imageUrl: '/authors/kamran.jpeg' +employment: + title: 'Founder' + company: 'roadmap.sh' social: linkedin: 'https://www.linkedin.com/in/kamrify' twitter: 'https://twitter.com/kamrify' diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index ae3adfc96..03518e347 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -145,7 +145,10 @@ const gaPageIdentifier = Astro.url.pathname { jsonLd.length > 0 && ( -