├── .env.example ├── remix.env.d.ts ├── public ├── images │ ├── 4noobs-bg.png │ ├── question.png │ ├── staff │ │ ├── 7k.png │ │ ├── m7.png │ │ ├── julia.png │ │ ├── pride.png │ │ ├── yaya.png │ │ ├── daniel.png │ │ ├── grande.png │ │ ├── gustavo.png │ │ ├── nadachi.png │ │ ├── nextur.png │ │ └── novout.png │ ├── default-badge.png │ ├── discord-print.png │ ├── background-heart.png │ └── partner-picture.png ├── assets │ └── icons │ │ ├── trophy.png │ │ └── arrow-right.svg └── brands │ ├── heart.svg │ ├── 4noobs-vertical.svg │ └── he4rt-logo.svg ├── .prettierignore ├── .gitignore ├── .eslintrc.js ├── .prettierrc ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── prettier.xml ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── webResources.xml ├── he4rt-landing-ssr.iml └── dbnavigator.xml ├── app ├── components │ ├── Container.tsx │ ├── profile │ │ ├── ProfileAboutMe.tsx │ │ ├── ProfileSeasonBadge.tsx │ │ ├── ProfilePicture.tsx │ │ ├── ProfileProgressBar.tsx │ │ ├── ProfileHeader.tsx │ │ ├── ProfileBadge.tsx │ │ ├── ProfileStats.tsx │ │ ├── ProfileUsernameAndSocials.tsx │ │ └── ProfileBackground.tsx │ ├── He4rtSkills.tsx │ ├── ToggleTheme.tsx │ ├── UserCard.tsx │ ├── PartnerCard.tsx │ ├── ContactBanner.tsx │ ├── OurPartners.tsx │ ├── ForNoobs.tsx │ ├── Staff.tsx │ ├── About.tsx │ ├── He4rt.tsx │ ├── FindAtHe4rtSection.tsx │ ├── Projects.tsx │ ├── SectionRecords.tsx │ ├── Navbar.tsx │ └── Footer.tsx ├── entry.client.tsx ├── services │ ├── fetchContributors.ts │ ├── fetchMedals.ts │ └── fetchProfile.ts ├── types │ ├── contributors.ts │ └── profile.ts ├── routes │ ├── action │ │ └── set-theme.tsx │ ├── index.tsx │ └── p.$userId.tsx ├── utils │ ├── theme.server.ts │ ├── api.ts │ └── theme-provider.tsx ├── mocks │ ├── users.ts │ └── projects.ts ├── root.tsx └── entry.server.tsx ├── styles ├── styles.css └── tailwind.css ├── remix.config.js ├── tsconfig.json ├── package.json ├── tailwind.config.js └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL= 2 | API_KEY= 3 | GITHUB_CONTRIBUTORS_URL= 4 | SESSION_SECRET= -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/images/4noobs-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/4noobs-bg.png -------------------------------------------------------------------------------- /public/images/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/question.png -------------------------------------------------------------------------------- /public/images/staff/7k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/7k.png -------------------------------------------------------------------------------- /public/images/staff/m7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/m7.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | 8 | /app/tailwind.css 9 | -------------------------------------------------------------------------------- /public/images/staff/julia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/julia.png -------------------------------------------------------------------------------- /public/images/staff/pride.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/pride.png -------------------------------------------------------------------------------- /public/images/staff/yaya.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/yaya.png -------------------------------------------------------------------------------- /public/assets/icons/trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/assets/icons/trophy.png -------------------------------------------------------------------------------- /public/images/default-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/default-badge.png -------------------------------------------------------------------------------- /public/images/discord-print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/discord-print.png -------------------------------------------------------------------------------- /public/images/staff/daniel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/daniel.png -------------------------------------------------------------------------------- /public/images/staff/grande.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/grande.png -------------------------------------------------------------------------------- /public/images/staff/gustavo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/gustavo.png -------------------------------------------------------------------------------- /public/images/staff/nadachi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/nadachi.png -------------------------------------------------------------------------------- /public/images/staff/nextur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/nextur.png -------------------------------------------------------------------------------- /public/images/staff/novout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/staff/novout.png -------------------------------------------------------------------------------- /public/images/background-heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/background-heart.png -------------------------------------------------------------------------------- /public/images/partner-picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he4rt/he4rt-landing-ssr/HEAD/public/images/partner-picture.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | 8 | /app/tailwind.css 9 | /api/ 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'], 4 | }; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type ContainerProps = React.PropsWithChildren<{}>; 4 | export default function Container({ children }: ContainerProps) { 5 | return
{children}
; 6 | } 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # CodeStream ignored files 7 | /codestream.xml 8 | # Datasource local storage ignored files 9 | /dataSources/ 10 | /dataSources.local.xml 11 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /styles/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | --text-colour: #424141; 3 | --background-colour: #ffffff; 4 | 5 | color: var(--text-colour); 6 | background-color: var(--background-colour); 7 | scroll-behavior: smooth; 8 | } 9 | 10 | html.dark { 11 | --text-colour: #ffffff; 12 | --background-colour: #272727; 13 | } 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/he4rt-landing-ssr.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | // When running locally in development mode, we use the built in remix 4 | // server. This does not understand the vercel lambda module format, 5 | // so we default back to the standard build output. 6 | ignoredRouteFiles: ['**/.*'], 7 | // appDirectory: "app", 8 | // assetsBuildDirectory: "public/build", 9 | // serverBuildPath: "api/index.js", 10 | // publicPath: "/build/", 11 | serverDependenciesToBundle: ['axios'] 12 | }; 13 | -------------------------------------------------------------------------------- /public/assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/profile/ProfileAboutMe.tsx: -------------------------------------------------------------------------------- 1 | interface ProfileAboutMeProps { 2 | description?: string; 3 | } 4 | 5 | export default function ProfileAboutMe({ description }: ProfileAboutMeProps) { 6 | if (!description) { 7 | return null; 8 | } 9 | 10 | return ( 11 |
12 |

Sobre mim

13 |

14 | {description} 15 |

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from '@remix-run/react'; 2 | import { startTransition, StrictMode } from 'react'; 3 | import { hydrateRoot } from 'react-dom/client'; 4 | 5 | function hydrate() { 6 | startTransition(() => { 7 | hydrateRoot( 8 | document, 9 | 10 | 11 | , 12 | ); 13 | }); 14 | } 15 | 16 | if (window.requestIdleCallback) { 17 | window.requestIdleCallback(hydrate); 18 | } else { 19 | // Safari doesn't support requestIdleCallback 20 | // https://caniuse.com/requestidlecallback 21 | window.setTimeout(hydrate, 1); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | 19 | // Remix takes care of building everything in `remix build`. 20 | "noEmit": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/services/fetchContributors.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { APIContributors, APPContributors } from '~/types/contributors'; 3 | import { apiContributorsToAppContributors } from '~/utils/api'; 4 | 5 | const contributorsURL = process.env.GITHUB_CONTRIBUTORS_URL; 6 | 7 | if (!contributorsURL) { 8 | throw new Error('GITHUB_CONTRIBUTORS_URL env not found'); 9 | } 10 | 11 | export async function fetchContributors(): Promise { 12 | const response = await axios.get(contributorsURL!); 13 | 14 | const contributors = response.data.courses.map((course) => 15 | apiContributorsToAppContributors(course), 16 | ); 17 | 18 | return contributors; 19 | } 20 | -------------------------------------------------------------------------------- /app/components/He4rtSkills.tsx: -------------------------------------------------------------------------------- 1 | import { SlGraduation } from 'react-icons/sl'; 2 | 3 | export type SkillsProps = { 4 | title: string; 5 | description: string; 6 | }; 7 | 8 | export default function He4rtSkills({ title, description }: SkillsProps) { 9 | return ( 10 |
11 |
12 | 16 |

{title}

17 |
18 |

{description}

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/ToggleTheme.tsx: -------------------------------------------------------------------------------- 1 | import { BsSun, BsFillMoonFill } from 'react-icons/bs'; 2 | 3 | import { useTheme, Theme } from '~/utils/theme-provider'; 4 | 5 | export default function ToggleTheme() { 6 | const [theme, setTheme] = useTheme(); 7 | const toggleTheme = () => { 8 | setTheme((prevTheme) => 9 | prevTheme === Theme.LIGHT ? Theme.DARK : Theme.LIGHT, 10 | ); 11 | }; 12 | 13 | if (theme === Theme.DARK) { 14 | return ( 15 | 20 | ); 21 | } 22 | 23 | return ( 24 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/types/contributors.ts: -------------------------------------------------------------------------------- 1 | export interface Course { 2 | name: string; 3 | tags: string[]; 4 | description: string; 5 | category: string; 6 | author: { 7 | name: string; 8 | username: string; 9 | avatar_url: string; 10 | }; 11 | url: string; 12 | } 13 | 14 | export interface APIContributors { 15 | courses: Course[]; 16 | socials: { 17 | twitter: number; 18 | instagram: number; 19 | github: number; 20 | discord: number; 21 | }; 22 | categories: []; 23 | } 24 | 25 | export interface APPContributors { 26 | name: Course['author']['name']; 27 | username: Course['author']['username']; 28 | avatar: Course['author']['avatar_url']; 29 | github: Course['url']; 30 | projectTitle: Course['name']; 31 | projectCategory: Course['category']; 32 | projectDescription: Course['description']; 33 | } 34 | -------------------------------------------------------------------------------- /app/components/profile/ProfileSeasonBadge.tsx: -------------------------------------------------------------------------------- 1 | import type { APPProfile } from '~/types/profile'; 2 | 3 | type ProfileSeasonBadgeProps = Pick; 4 | 5 | export default function ProfileSeasonBadge({ 6 | seasonInfo, 7 | }: ProfileSeasonBadgeProps) { 8 | if (!seasonInfo || !seasonInfo.rankingPosition || !seasonInfo.seasonName) { 9 | return <>; 10 | } 11 | 12 | return ( 13 | 14 | 15 | 16 | S{seasonInfo.seasonName} 17 | 18 | 19 | {' | '} 20 | {seasonInfo?.rankingPosition}º Lugar 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/routes/action/set-theme.tsx: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/node'; 2 | import type { ActionFunction } from '@remix-run/node'; 3 | 4 | import { getThemeSession } from '~/utils/theme.server'; 5 | import { isTheme } from '~/utils/theme-provider'; 6 | 7 | export const action: ActionFunction = async ({ request }) => { 8 | const themeSession = await getThemeSession(request); 9 | const requestText = await request.text(); 10 | const form = new URLSearchParams(requestText); 11 | const theme = form.get('theme'); 12 | 13 | if (!isTheme(theme)) { 14 | return json({ 15 | success: false, 16 | message: `theme value of ${theme} is not a valid theme`, 17 | }); 18 | } 19 | 20 | themeSession.setTheme(theme); 21 | return json( 22 | { success: true }, 23 | { headers: { 'Set-Cookie': await themeSession.commit() } }, 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .slick-list, 7 | .slick-slider { 8 | @apply relative block; 9 | } 10 | 11 | .slick-list { 12 | @apply m-0 overflow-hidden p-0; 13 | } 14 | 15 | .slick-slide { 16 | @apply float-left block h-fit; 17 | } 18 | 19 | .slick-arrow { 20 | @apply absolute -bottom-8 z-10 block h-8 w-8 text-[0px] leading-[0px]; 21 | } 22 | 23 | .slick-prev { 24 | @apply left-1/3 rotate-180 transform lg:left-[50%]; 25 | } 26 | 27 | .slick-next { 28 | @apply right-1/3 left-[unset] lg:left-[60%] lg:right-[unset]; 29 | } 30 | 31 | .slick-arrow::before { 32 | @apply absolute left-1/2 top-1/2 block h-[10px] w-[6px] -translate-x-1/2 -translate-y-1/2 transform text-4xl text-black; 33 | content: ' '; 34 | background: url('/assets/icons/arrow-right.svg') center center no-repeat; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/services/fetchMedals.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { notFound, serverError } from 'remix-utils'; 3 | import type { BadgeInfo } from '~/types/profile'; 4 | 5 | const baseURL = process.env.MSR_MEDALS_URL; 6 | 7 | if (!baseURL) { 8 | throw new Error('MSR_MEDALS_URL env not found'); 9 | } 10 | 11 | export async function fetchMedals(userId: string): Promise { 12 | return axios 13 | .create({ 14 | baseURL, 15 | }) 16 | .get(`/user/medals/${userId}`) 17 | .then((data) => data.data) 18 | .catch((err) => { 19 | console.error(`Error while fetching ${userId}`); 20 | console.error(err); 21 | 22 | if (!axios.isAxiosError(err)) { 23 | throw serverError({}); 24 | } 25 | 26 | // Api returns a 422 when user doesnt exists on the DB 27 | if (err.status === 422) { 28 | throw notFound({}); 29 | } 30 | 31 | throw serverError({}); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /app/components/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineTwitter } from 'react-icons/ai'; 2 | import { UserCardProps } from '~/components/Staff'; 3 | 4 | const UserCard = ({ avatar, username, occupation, twitter }: UserCardProps) => ( 5 |
6 | {`Avatar 11 |
12 |
13 |

14 | {username} 15 |

16 | {occupation} 17 |
18 | 24 | 25 | 26 |
27 |
28 | ); 29 | 30 | export default UserCard; 31 | -------------------------------------------------------------------------------- /app/utils/theme.server.ts: -------------------------------------------------------------------------------- 1 | import { createCookieSessionStorage } from '@remix-run/node'; 2 | 3 | import type { Theme } from './theme-provider'; 4 | import { isTheme } from './theme-provider'; 5 | 6 | const sessionSecret = process.env.SESSION_SECRET; 7 | if (!sessionSecret) { 8 | throw new Error('SESSION_SECRET must be set'); 9 | } 10 | 11 | const themeStorage = createCookieSessionStorage({ 12 | cookie: { 13 | name: 'my_remix_theme', 14 | secure: true, 15 | secrets: [sessionSecret], 16 | sameSite: 'lax', 17 | path: '/', 18 | httpOnly: true, 19 | }, 20 | }); 21 | 22 | async function getThemeSession(request: Request) { 23 | const session = await themeStorage.getSession(request.headers.get('Cookie')); 24 | return { 25 | getTheme: () => { 26 | const themeValue = session.get('theme'); 27 | return isTheme(themeValue) ? themeValue : null; 28 | }, 29 | setTheme: (theme: Theme) => session.set('theme', theme), 30 | commit: () => themeStorage.commitSession(session), 31 | }; 32 | } 33 | 34 | export { getThemeSession }; 35 | -------------------------------------------------------------------------------- /app/components/profile/ProfilePicture.tsx: -------------------------------------------------------------------------------- 1 | import type { APPProfile } from '~/types/profile'; 2 | import ProfileProgressBar from './ProfileProgressBar'; 3 | import ProfileSeasonBadge from './ProfileSeasonBadge'; 4 | 5 | type ProfilePictureProps = Pick< 6 | APPProfile, 7 | 'nickname' | 'level' | 'profilePicture' | 'seasonInfo' | 'experience' 8 | >; 9 | 10 | export default function ProfilePicture({ 11 | profilePicture, 12 | nickname, 13 | level, 14 | experience, 15 | ...seasonInfo 16 | }: ProfilePictureProps) { 17 | return ( 18 |
19 |
20 | 25 | 26 |
27 | Lvl. {level} 28 |
29 |
30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/components/profile/ProfileProgressBar.tsx: -------------------------------------------------------------------------------- 1 | type ProfileProgressBarProps = { 2 | imageSrc: string; 3 | imageAlt: string; 4 | percentage: number; 5 | }; 6 | 7 | export default function ProfileProgressBar({ 8 | imageSrc, 9 | imageAlt, 10 | percentage, 11 | }: ProfileProgressBarProps) { 12 | return ( 13 |
14 |
15 | {imageAlt} 20 | 21 | 25 | 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /public/brands/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/PartnerCard.tsx: -------------------------------------------------------------------------------- 1 | export type PartnerCardProps = { 2 | name: string; 3 | specialty: string; 4 | description: string; 5 | contact: string; 6 | logo: string; 7 | }; 8 | 9 | export default function PartnerCard({ 10 | name, 11 | specialty, 12 | description, 13 | contact, 14 | logo, 15 | }: PartnerCardProps) { 16 | return ( 17 | <> 18 |
19 | partnerpicture 20 |
21 |
22 |

{name}

23 | {specialty} 24 |
25 |

26 | {description} 27 |

28 | 34 | Entrar em contato 35 | 36 |
37 |
38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/services/fetchProfile.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { APIProfile, APPProfile } from '~/types/profile'; 3 | import { apiProfileToAppProfile } from '~/utils/api'; 4 | import { notFound, serverError } from 'remix-utils'; 5 | 6 | const baseURL = process.env.BASE_URL; 7 | 8 | if (!baseURL) { 9 | throw new Error('BASE_URL env not found'); 10 | } 11 | const apikey = process.env.API_KEY; 12 | 13 | if (!apikey) { 14 | throw new Error('API_KEY env not found'); 15 | } 16 | 17 | export async function fetchProfile(userId: string): Promise { 18 | return axios 19 | .create({ 20 | baseURL, 21 | headers: { 22 | 'X-He4rt-Authorization': apikey, 23 | }, 24 | }) 25 | .get(`/api/users/profile/${userId}`) 26 | .then((data) => apiProfileToAppProfile(data.data)) 27 | .catch((err) => { 28 | console.error(`Error while fetching ${userId}`); 29 | console.error(err); 30 | 31 | if (!axios.isAxiosError(err)) { 32 | throw serverError({}); 33 | } 34 | 35 | // Api returns a 422 when user doesnt exists on the DB 36 | if (err.status === 422) { 37 | throw notFound({}); 38 | } 39 | 40 | throw serverError({}); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/ContactBanner.tsx: -------------------------------------------------------------------------------- 1 | import Container from './Container'; 2 | 3 | export default function ContactBanner() { 4 | return ( 5 |
6 | 7 |
8 |
9 |

10 | Novas perspectivas geram novas oportunidades 11 |

12 |

13 | Junte-se a nós e revolucione a maneira de aprender. Seja 14 | patrocinador de sonhos e ajude milhares de pessoas a alcançarem 15 | seus objetivos. 16 |

17 |
18 | 24 | Entrar em contato 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/components/profile/ProfileHeader.tsx: -------------------------------------------------------------------------------- 1 | import ProfileStats from './ProfileStats'; 2 | import ProfileUsernameAndSocials from './ProfileUsernameAndSocials'; 3 | import type { APPProfile } from '~/types/profile'; 4 | 5 | type ProfileHeaderProps = Pick< 6 | APPProfile, 7 | | 'git' 8 | | 'linkedin' 9 | | 'name' 10 | | 'nickname' 11 | | 'messagesCount' 12 | | 'badgesCount' 13 | | 'rankingPosition' 14 | >; 15 | 16 | export default function ProfileHeader(props: ProfileHeaderProps) { 17 | return ( 18 |
19 |
20 | 21 | 26 | 31 | 36 |
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/components/OurPartners.tsx: -------------------------------------------------------------------------------- 1 | import type { PartnerCardProps } from './PartnerCard'; 2 | import PartnerCard from './PartnerCard'; 3 | import Container from '~/components/Container'; 4 | 5 | const partners: PartnerCardProps[] = [ 6 | { 7 | name: 'Sinergicon', 8 | specialty: 'Consultoria Financeira', 9 | contact: 'https://www.sinergicon.com.br', 10 | description: 11 | 'Temos o propósito de trabalhar em Sinergia com nossos clientes, mais do que prestadores de serviços, a nossa missão é se tornar um parceiro de negócios e estar o mais próximo possível.', 12 | logo: '/images/partner-picture.png', 13 | }, 14 | ]; 15 | 16 | export default function OurPartners() { 17 | return ( 18 | 19 |
20 |

21 | Nossos parceiros 22 |

23 |

24 | Com o esforço de nossos parceiros estamos na missão de fazer o 25 | impossível. Juntos criamos soluções que mudará a maneira de aprender, 26 | garantindo oportunidade para todos. Faça parte dessa história, seja um 27 | parceiro. 28 |

29 |
30 |
31 | {partners.map((partner) => ( 32 | 33 | ))} 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/components/profile/ProfileBadge.tsx: -------------------------------------------------------------------------------- 1 | import type { BadgeInfo } from '~/types/profile'; 2 | 3 | export default function ProfileBadge({ badges }: { badges: BadgeInfo[] }) { 4 | return ( 5 |
6 |

🏆 Conquistas

7 |

8 | Seção de conquistas referentes à gameficação do servidor e eventos 9 | externos. 10 |

11 |
12 | {badges?.map(({ name, description, image_url, role_id }) => ( 13 |
14 | {`Imagem { 19 | currentTarget.onerror = null; 20 | currentTarget.src = '/images/default-badge.png'; 21 | }} 22 | /> 23 |
24 |

{name}

25 | 26 | {description} 27 | 28 |
29 |
30 | ))} 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { LinksFunction } from '@remix-run/node'; 2 | 3 | import About from '~/components/About'; 4 | import FindAtHe4rtSection from '~/components/FindAtHe4rtSection'; 5 | import Footer from '~/components/Footer'; 6 | import He4rt from '~/components/He4rt'; 7 | import OurPartners from '~/components/OurPartners'; 8 | import SectionRecords from '~/components/SectionRecords'; 9 | import Staff from '~/components/Staff'; 10 | import ForNoobs from '~/components/ForNoobs'; 11 | import ContactBanner from '~/components/ContactBanner'; 12 | import Navbar from '~/components/Navbar'; 13 | 14 | import { fetchContributors } from '~/services/fetchContributors'; 15 | import { useLoaderData } from '@remix-run/react'; 16 | 17 | export const loader = async () => { 18 | const contributors = await fetchContributors(); 19 | 20 | return contributors ?? []; 21 | }; 22 | 23 | type LoaderType = typeof loader; 24 | 25 | export const links: LinksFunction = () => { 26 | return [ 27 | { 28 | rel: 'prefetch', 29 | as: 'image', 30 | href: '/images/question.png', 31 | }, 32 | ]; 33 | }; 34 | 35 | export default function Index() { 36 | const contributorsData = useLoaderData(); 37 | 38 | return ( 39 | <> 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |