├── src ├── app │ ├── sitemap.xml │ ├── robots.txt │ ├── icon.png │ ├── favicon.ico │ ├── apple-icon.png │ ├── projects │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ ├── error.tsx │ │ └── page.tsx │ ├── loading.tsx │ ├── scroll.module.scss │ ├── error.tsx │ ├── page.tsx │ ├── global-error.tsx │ ├── layout.tsx │ └── globals.scss ├── utils │ └── cn.ts ├── components │ ├── common │ │ ├── WebVitals.tsx │ │ ├── SectionTitle.tsx │ │ ├── FilledButton.tsx │ │ ├── OutlinedButton.tsx │ │ ├── GalleryImageView.tsx │ │ ├── AppBar.tsx │ │ ├── ScrollToTop.tsx │ │ ├── HoverLayoutGrid.tsx │ │ ├── FlipWords.tsx │ │ └── TypewriterEffect.tsx │ ├── home │ │ ├── ui │ │ │ ├── ResumeButton.tsx │ │ │ ├── TalkButton.tsx │ │ │ ├── SocialButton.tsx │ │ │ ├── SkillItem.tsx │ │ │ ├── ProjectList.tsx │ │ │ ├── ExperienceItem.tsx │ │ │ ├── ProjectItem.tsx │ │ │ └── AnimatedServiceCard.tsx │ │ ├── Section5.tsx │ │ ├── Section4.tsx │ │ ├── Section2.tsx │ │ ├── Section3.tsx │ │ ├── Section6.tsx │ │ ├── Section1.tsx │ │ └── Section1 copy.tsx │ ├── core │ │ ├── PageBox.tsx │ │ ├── Row.tsx │ │ ├── Column.tsx │ │ ├── GridBox.tsx │ │ ├── ResponsiveBox.tsx │ │ ├── ConstraintedBox.tsx │ │ ├── Modal.tsx │ │ └── CardBox.tsx │ ├── projects │ │ ├── Section2.tsx │ │ ├── components │ │ │ └── ScreenshotGallery.tsx │ │ └── Section1.tsx │ └── navbar │ │ ├── Dropdown.tsx │ │ ├── FloatingNavbar.tsx │ │ ├── MenuItems.tsx │ │ └── NavBar.tsx ├── constants │ ├── config.ts │ └── strings.ts ├── hooks │ ├── useOnClickOutside.ts │ ├── useWindowDimensions.ts │ ├── useIsInViewport.ts │ ├── useMobileNav.ts │ ├── useScrolled.ts │ └── useVisibleSection.ts ├── data │ ├── socialLinks.ts │ ├── navMenus.ts │ ├── experiences.ts │ ├── services.ts │ ├── projects.ts │ └── skills.ts ├── types │ └── index.ts └── apis │ └── data.ts ├── .eslintrc.json ├── bun.lockb ├── public ├── skills │ ├── getx.png │ ├── docker.png │ ├── nextjs.png │ ├── ubuntu.png │ ├── Bryan_CV.pdf │ ├── socket-io.png │ ├── flutter.svg │ ├── html.svg │ ├── nextjs.svg │ ├── css.svg │ ├── dart.svg │ ├── firebase.svg │ ├── typescript.svg │ ├── git.svg │ ├── terraform.svg │ ├── javascript.svg │ ├── mongodb.svg │ ├── github.svg │ ├── golang-1.svg │ ├── redux.svg │ ├── nestjs.svg │ ├── nodejs.svg │ ├── docker.svg │ ├── sqlite.svg │ ├── jinja.svg │ ├── express.svg │ ├── redis.svg │ ├── aws.svg │ ├── postgresql.svg │ ├── sass.svg │ ├── mysql.svg │ ├── jenkins.svg │ ├── Flask.svg │ └── react.svg └── images │ ├── profile.webp │ ├── placeholder.png │ ├── collaboration.png │ ├── logical-thinking.png │ └── analytical-skills.png ├── jsconfig.json ├── sentry.edge.config.js ├── sentry.server.config.js ├── postcss.config.js ├── instrumentation.ts ├── sentry.client.config.js ├── .gitignore ├── next.config.js ├── tsconfig.json ├── package.json └── tailwind.config.ts /src/app/sitemap.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | Disallow: /private/ 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/src/app/icon.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/skills/getx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/skills/getx.png -------------------------------------------------------------------------------- /src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/src/app/apple-icon.png -------------------------------------------------------------------------------- /public/skills/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/skills/docker.png -------------------------------------------------------------------------------- /public/skills/nextjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/skills/nextjs.png -------------------------------------------------------------------------------- /public/skills/ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/skills/ubuntu.png -------------------------------------------------------------------------------- /public/images/profile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/images/profile.webp -------------------------------------------------------------------------------- /public/skills/Bryan_CV.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/skills/Bryan_CV.pdf -------------------------------------------------------------------------------- /public/skills/socket-io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/skills/socket-io.png -------------------------------------------------------------------------------- /public/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/images/placeholder.png -------------------------------------------------------------------------------- /public/images/collaboration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/images/collaboration.png -------------------------------------------------------------------------------- /public/images/logical-thinking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/images/logical-thinking.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": [ 5 | "./src/*" 6 | ] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /public/images/analytical-skills.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BryanJericho/BryanPortofolio/HEAD/public/images/analytical-skills.png -------------------------------------------------------------------------------- /sentry.edge.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | Sentry.init({ 4 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 5 | tracesSampleRate: 1.0, 6 | }); 7 | -------------------------------------------------------------------------------- /sentry.server.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | Sentry.init({ 4 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 5 | tracesSampleRate: 1.0, 6 | }); 7 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": "postcss-nesting", 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/projects/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | const ProjectsLayout = ({ children }: Readonly<{ children: ReactNode }>) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default ProjectsLayout; 8 | -------------------------------------------------------------------------------- /src/components/common/WebVitals.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useReportWebVitals } from "next/web-vitals"; 4 | 5 | export function WebVitals() { 6 | useReportWebVitals((metric) => { 7 | console.log(metric); 8 | }); 9 | 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === "nodejs") { 3 | await import("./sentry.server.config"); 4 | } 5 | 6 | if (process.env.NEXT_RUNTIME === "edge") { 7 | await import("./sentry.edge.config"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/common/SectionTitle.tsx: -------------------------------------------------------------------------------- 1 | const SectionTitle = ({ children }: Readonly<{ children: string }>) => { 2 | return ( 3 |

4 | {children} 5 |

6 | ); 7 | }; 8 | 9 | export default SectionTitle; 10 | -------------------------------------------------------------------------------- /sentry.client.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | Sentry.init({ 4 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 5 | integrations: [Sentry.replayIntegration()], 6 | 7 | tracesSampleRate: 1.0, 8 | replaysSessionSampleRate: 0.1, 9 | replaysOnErrorSampleRate: 1.0, 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading = () => { 2 | return ( 3 |
6 |

Loading, please wait...

7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/app/projects/loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading = () => { 2 | return ( 3 |
6 |

Loading, please wait...

7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /public/skills/flutter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/html.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/home/ui/ResumeButton.tsx: -------------------------------------------------------------------------------- 1 | import LocalConfig from "@/constants/config"; 2 | import Link from "next/link"; 3 | 4 | const ResumeButton = () => { 5 | return ( 6 | 11 | Download Resume 12 | 13 | ); 14 | }; 15 | 16 | export default ResumeButton; 17 | -------------------------------------------------------------------------------- /public/skills/nextjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/home/ui/TalkButton.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Strings from "@/constants/strings"; 3 | 4 | const TalkButton = () => { 5 | return ( 6 | 11 | Let's Talk 12 | 13 | ); 14 | }; 15 | 16 | export default TalkButton; 17 | -------------------------------------------------------------------------------- /public/skills/css.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/core/PageBox.tsx: -------------------------------------------------------------------------------- 1 | import type { CoreComponentsProps } from "@/types"; 2 | 3 | const PageBox = (props: Readonly) => { 4 | const { children, classNames } = props; 5 | 6 | return ( 7 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | export default PageBox; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # dependencies 3 | /node_modules 4 | /.pnp 5 | .pnp.js 6 | 7 | # testing 8 | /coverage 9 | 10 | # next.js 11 | /.next/ 12 | /out/ 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env*.local 28 | 29 | # vercel 30 | .vercel 31 | 32 | # typescript 33 | *.tsbuildinfo 34 | next-env.d.ts 35 | 36 | # lock 37 | package-lock.json 38 | pnpm-lock.yaml 39 | yarn.lock -------------------------------------------------------------------------------- /public/skills/dart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/firebase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/FilledButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ButtonComponentProps } from "@/types"; 4 | 5 | const AppFilledButton = (props: Readonly) => { 6 | const { label, onClick, classNames, name } = props; 7 | 8 | return ( 9 | 17 | ); 18 | }; 19 | 20 | export default AppFilledButton; 21 | -------------------------------------------------------------------------------- /src/components/core/Row.tsx: -------------------------------------------------------------------------------- 1 | import type { CoreComponentsProps } from "@/types"; 2 | 3 | const Row = (props: Readonly) => { 4 | const { children, classNames, onClick, id, elementRef } = props; 5 | 6 | return ( 7 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default Row; 19 | -------------------------------------------------------------------------------- /src/constants/config.ts: -------------------------------------------------------------------------------- 1 | const LocalConfig = { 2 | values: { 3 | TZ: process.env.TZ, 4 | NODE_ENV: process.env.NODE_ENV, 5 | AUTOPREFIXER_GRID: process.env.AUTOPREFIXER_GRID, 6 | NEXT_PUBLIC_GTAG_ID: process.env.NEXT_PUBLIC_GTAG_ID, 7 | NEXT_PUBLIC_GITHUB_TOKEN: process.env.NEXT_PUBLIC_GITHUB_TOKEN, 8 | NEXT_PUBLIC_VERCEL_TOKEN: process.env.NEXT_PUBLIC_VERCEL_TOKEN, 9 | NEXT_PUBLIC_RESUME_LINK: process.env.NEXT_PUBLIC_RESUME_LINK || "/skills/Bryan_CV.pdf", 10 | }, 11 | }; 12 | 13 | export default LocalConfig; 14 | -------------------------------------------------------------------------------- /src/components/common/OutlinedButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ButtonComponentProps } from "@/types"; 4 | 5 | const AppOutlinedButton = (props: Readonly) => { 6 | const { label, onClick, classNames, name } = props; 7 | 8 | return ( 9 | 17 | ); 18 | }; 19 | 20 | export default AppOutlinedButton; 21 | -------------------------------------------------------------------------------- /src/components/core/Column.tsx: -------------------------------------------------------------------------------- 1 | import type { CoreComponentsProps } from "@/types"; 2 | 3 | const Column = (props: Readonly) => { 4 | const { children, classNames, onClick, id, elementRef } = props; 5 | 6 | return ( 7 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default Column; 19 | -------------------------------------------------------------------------------- /src/components/core/GridBox.tsx: -------------------------------------------------------------------------------- 1 | import type { CoreComponentsProps } from "@/types"; 2 | 3 | const GridBox = (props: Readonly) => { 4 | const { children, classNames, onClick, id, elementRef } = props; 5 | 6 | return ( 7 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default GridBox; 19 | -------------------------------------------------------------------------------- /public/skills/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/core/ResponsiveBox.tsx: -------------------------------------------------------------------------------- 1 | import type { CoreComponentsProps } from "@/types"; 2 | 3 | const ResponsiveBox = (props: Readonly) => { 4 | const { children, classNames, id, elementRef, onClick } = props; 5 | 6 | return ( 7 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default ResponsiveBox; 19 | -------------------------------------------------------------------------------- /src/components/common/GalleryImageView.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | const GalleryImageView = ({ 4 | src, 5 | alt, 6 | }: Readonly<{ src: string; alt?: string }>) => { 7 | return ( 8 | {alt 20 | ); 21 | }; 22 | 23 | export default GalleryImageView; 24 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const path = require("path"); 3 | const { withSentryConfig } = require("@sentry/nextjs"); 4 | 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | swcMinify: true, 8 | images: { 9 | remotePatterns: [ 10 | { protocol: "https", hostname: "**.githubusercontent.com" }, 11 | { protocol: "https", hostname: "**.github.com" }, 12 | ], 13 | }, 14 | }; 15 | 16 | module.exports = withSentryConfig(nextConfig, { 17 | org: "nixlab-technologies", 18 | project: "portfolio-nextjs", 19 | authToken: process.env.SENTRY_AUTH_TOKEN, 20 | silent: false, 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/core/ConstraintedBox.tsx: -------------------------------------------------------------------------------- 1 | import type { CoreComponentsProps } from "@/types"; 2 | 3 | const ConstraintedBox = (props: Readonly) => { 4 | const { children, classNames, onClick, id, elementRef } = props; 5 | 6 | return ( 7 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default ConstraintedBox; 19 | -------------------------------------------------------------------------------- /public/skills/git.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/terraform.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/projects/Section2.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 5 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 6 | import { getProjectDetails } from "@/data/projects"; 7 | 8 | const ProjectsSection2 = ({ id }: Readonly<{ id?: string }>) => { 9 | const searchParams = useSearchParams(); 10 | const description = getProjectDetails(searchParams.get("id")!)?.description; 11 | 12 | return ( 13 | 14 | 15 |

{description}

16 |
17 |
18 | ); 19 | }; 20 | 21 | export default ProjectsSection2; 22 | -------------------------------------------------------------------------------- /public/skills/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { RefObject, useEffect } from "react"; 4 | 5 | function useOnClickOutside( 6 | ref: RefObject, 7 | handler: CallableFunction 8 | ) { 9 | useEffect(() => { 10 | if (!ref) return; 11 | 12 | const listener = (event: any) => { 13 | if (ref.current && !ref.current.contains(event.target)) { 14 | handler(event); 15 | } 16 | }; 17 | 18 | document.addEventListener("mousedown", listener); 19 | document.addEventListener("touchstart", listener); 20 | 21 | return () => { 22 | document.removeEventListener("mousedown", listener); 23 | document.removeEventListener("touchstart", listener); 24 | }; 25 | }, [ref, handler]); 26 | } 27 | 28 | export default useOnClickOutside; 29 | -------------------------------------------------------------------------------- /src/components/home/Section5.tsx: -------------------------------------------------------------------------------- 1 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 2 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 3 | import SectionTitle from "@/components/common/SectionTitle"; 4 | import ProjectList from "./ui/ProjectList"; 5 | import projects from "@/data/projects"; 6 | 7 | const HomeSection5 = ({ id }: { id: string }) => { 8 | return ( 9 | 13 | 14 | Recent Works 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default HomeSection5; 23 | -------------------------------------------------------------------------------- /src/components/home/ui/SocialButton.tsx: -------------------------------------------------------------------------------- 1 | import type { ISocialLinkItem } from "@/types"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import Link from "next/link"; 4 | 5 | const SocialButton = ({ text, icon, url }: ISocialLinkItem) => { 6 | return ( 7 | 12 | 13 | 14 | 15 | 16 |

{text}

17 | 18 | ); 19 | }; 20 | 21 | export default SocialButton; 22 | -------------------------------------------------------------------------------- /src/hooks/useWindowDimensions.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | function getWindowDimensions() { 6 | const { innerWidth: width, innerHeight: height } = window; 7 | 8 | return { width, height }; 9 | } 10 | 11 | export default function useWindowDimensions() { 12 | const [wDimensions, setWindowDimensions] = useState(getWindowDimensions()); 13 | 14 | useEffect(() => { 15 | function handleResize() { 16 | setWindowDimensions(getWindowDimensions()); 17 | } 18 | 19 | if (document.readyState === "complete") 20 | setWindowDimensions(getWindowDimensions()); 21 | 22 | window.addEventListener("resize", handleResize); 23 | 24 | return () => window.removeEventListener("resize", handleResize); 25 | }, []); 26 | 27 | return wDimensions; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "incremental": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | } 29 | }, 30 | "include": [ 31 | "next-env.d.ts", 32 | ".next/types/**/*.ts", 33 | "**/*.ts", 34 | "**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /public/skills/mongodb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import { isValidElement, ReactNode } from "react"; 2 | import Link from "next/link"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; 5 | import Row from "@/components/core/Row"; 6 | 7 | const AppBar = ({ children }: Readonly<{ children?: string | ReactNode }>) => { 8 | return ( 9 | 10 | 11 | 15 | 16 | 17 | {children ? ( 18 | isValidElement(children) ? ( 19 | children 20 | ) : ( 21 |

{children}

22 | ) 23 | ) : null} 24 |
25 | ); 26 | }; 27 | 28 | export default AppBar; 29 | -------------------------------------------------------------------------------- /src/components/navbar/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import MenuItems from "@/components/navbar/MenuItems"; 2 | import { DropdownMenuProps } from "@/types"; 3 | 4 | const Dropdown = (props: DropdownMenuProps) => { 5 | let { 6 | submenus, 7 | dropdown, 8 | depthLevel, 9 | mobileNav, 10 | handleCloseMobileMenu, 11 | ...others 12 | } = props; 13 | 14 | depthLevel = depthLevel + 1; 15 | const dropdownClass = depthLevel > 1 ? " dropdown-submenu" : ""; 16 | return ( 17 |
    21 | {submenus.map((submenu, index) => ( 22 | 29 | ))} 30 |
31 | ); 32 | }; 33 | 34 | export default Dropdown; 35 | -------------------------------------------------------------------------------- /src/app/projects/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | const Error = ({ 4 | error, 5 | reset, 6 | }: { 7 | error: Error & { digest?: string }; 8 | reset: () => void; 9 | }) => { 10 | return ( 11 |
14 |

15 | Something went wrong...!!! 16 |

17 | 18 |
19 |

{error.message}

20 |
21 | 22 | 31 |
32 | ); 33 | }; 34 | 35 | export default Error; 36 | -------------------------------------------------------------------------------- /public/skills/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/projects/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import PageBox from "@/components/core/PageBox"; 3 | import ProjectsSection1 from "@/components/projects/Section1"; 4 | import ProjectsSection2 from "@/components/projects/Section2"; 5 | import Modal from "@/components/core/Modal"; 6 | import GalleryImageView from "@/components/common/GalleryImageView"; 7 | import React from "react"; 8 | 9 | const ProjectDetails = ({ 10 | params, 11 | searchParams, 12 | }: Readonly<{ params: any; searchParams: any }>) => { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {!!searchParams.imgSrc ? ( 23 | 24 | 25 | 26 | 27 | 28 | ) : null} 29 | 30 | ); 31 | }; 32 | 33 | export default ProjectDetails; 34 | -------------------------------------------------------------------------------- /src/hooks/useIsInViewport.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ViewportProps } from "@/types"; 4 | import { useEffect, useState, useMemo, RefObject } from "react"; 5 | 6 | const useInViewport = ( 7 | ref: RefObject, 8 | options?: ViewportProps 9 | ) => { 10 | const [isVisible, setIsVisible] = useState(false); 11 | 12 | const vOptions = useMemo( 13 | () => ({ 14 | root: options?.root, 15 | rootMargin: options?.rootMargin || "20px", 16 | threshold: options?.threshold || 0.3, 17 | }), 18 | [options] 19 | ); 20 | 21 | const observer = useMemo( 22 | () => 23 | new IntersectionObserver(([entry]) => { 24 | setIsVisible(entry.isIntersecting); 25 | }, vOptions), 26 | [vOptions] 27 | ); 28 | 29 | useEffect(() => { 30 | if (!ref || !ref.current) return; 31 | 32 | observer.observe(ref.current); 33 | 34 | return () => observer.disconnect(); 35 | }, [ref, observer]); 36 | 37 | return isVisible; 38 | }; 39 | 40 | export default useInViewport; 41 | -------------------------------------------------------------------------------- /src/app/scroll.module.scss: -------------------------------------------------------------------------------- 1 | .topToBottom { 2 | position: fixed; 3 | bottom: 1rem; 4 | right: 1rem; 5 | z-index: 20; 6 | } 7 | 8 | .btnStyle { 9 | background-color: var(--bgColor); 10 | border: 2px solid var(--whiteColor); 11 | border-radius: 50%; 12 | height: 3rem; 13 | width: 3rem; 14 | color: var(--whiteColor); 15 | cursor: pointer; 16 | animation: upDown 3s ease-in-out infinite; 17 | transition: all 0.25s ease-in-out; 18 | 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .btnStyle:hover { 25 | animation: none; 26 | background-color: var(--whiteColor); 27 | color: var(--bgColor); 28 | } 29 | 30 | .iconStyle { 31 | width: 1.5rem; 32 | height: 1.5rem; 33 | } 34 | 35 | @keyframes upDown { 36 | 0% { 37 | transform: translateY(0px); 38 | } 39 | 25% { 40 | transform: translateY(8px); 41 | } 42 | 50% { 43 | transform: translateY(0px); 44 | } 45 | 75% { 46 | transform: translateY(-8px); 47 | } 48 | 100% { 49 | transform: translateY(0px); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/home/Section4.tsx: -------------------------------------------------------------------------------- 1 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 2 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 3 | import GridBox from "@/components/core/GridBox"; 4 | import SectionTitle from "@/components/common/SectionTitle"; 5 | import SkillItem from "./ui/SkillItem"; 6 | import skills from "@/data/skills"; 7 | 8 | const HomeSection4 = ({ id }: { id: string }) => { 9 | return ( 10 | 14 | 15 | Skills 16 | 17 | 18 | {skills.map((skill, index) => { 19 | return ; 20 | })} 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default HomeSection4; 28 | -------------------------------------------------------------------------------- /src/constants/strings.ts: -------------------------------------------------------------------------------- 1 | class Strings { 2 | static telegram = "Telegram"; 3 | static telegramLink = "https://telegram.me/bryanjericho"; 4 | static telegramUsername = "bryanjericho"; 5 | 6 | static github = "GitHub"; 7 | static githubLink = "https://www.github.com/bryanjericho"; 8 | static githubUsername = "bryanjericho"; 9 | 10 | static linkedIn = "LinkedIn"; 11 | static linkedInLink = "https://www.linkedin.com/in/bryanjericho"; 12 | static linkedInUsername = "Bryan Jericho"; 13 | 14 | static instagram = "Instagram"; 15 | static instagramLink = "https://www.instagram.com/bryanjerichoo"; 16 | static instagramUsername = "bryanjerichoo"; 17 | 18 | static twitter = "Twitter"; 19 | static twitterLink = "https://www.twitter.com/bryanjerichoo1"; 20 | static twitterUsername = "@bryanjericho"; 21 | 22 | static email = "Email"; 23 | static primaryEmailLink = "mailto:bryanpanggalo@gmail.com"; 24 | static primaryEmail = "bryanpanggalo@gmail.com"; 25 | 26 | static fullName = "Bryan Jericho"; 27 | static shortName = "bryanjericho"; 28 | } 29 | 30 | export default Strings; 31 | -------------------------------------------------------------------------------- /src/components/home/Section2.tsx: -------------------------------------------------------------------------------- 1 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 2 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 3 | import SectionTitle from "@/components/common/SectionTitle"; 4 | import { HoverLayoutGrid } from "@/components/common/HoverLayoutGrid"; 5 | import services from "@/data/services"; 6 | 7 | const HomeSection2 = ({ id }: { id: string }) => { 8 | return ( 9 | 13 | {/*
*/} 14 | 15 | 16 | Services 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default HomeSection2; 24 | -------------------------------------------------------------------------------- /src/hooks/useMobileNav.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | const useMobileNav = () => { 6 | const [mobileNav, setMobileNav] = useState(false); 7 | const [showMobileMenu, setShowMobileMenu] = useState(false); 8 | 9 | useEffect(() => { 10 | const handleResize = () => { 11 | const innerWidth = window.innerWidth; 12 | if (innerWidth > 900) { 13 | setMobileNav(false); 14 | if (showMobileMenu) { 15 | setShowMobileMenu(false); 16 | } 17 | } else { 18 | setMobileNav(true); 19 | } 20 | }; 21 | 22 | if (document.readyState === "complete") handleResize(); 23 | 24 | window.addEventListener("load", handleResize); 25 | window.addEventListener("resize", handleResize); 26 | 27 | return () => { 28 | window.removeEventListener("load", handleResize); 29 | window.removeEventListener("resize", handleResize); 30 | }; 31 | }, [showMobileMenu, mobileNav]); 32 | 33 | return { mobileNav, setMobileNav, showMobileMenu, setShowMobileMenu }; 34 | }; 35 | 36 | export default useMobileNav; 37 | -------------------------------------------------------------------------------- /src/components/home/Section3.tsx: -------------------------------------------------------------------------------- 1 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 2 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 3 | import GridBox from "@/components/core/GridBox"; 4 | import SectionTitle from "@/components/common/SectionTitle"; 5 | import ExperienceItem from "./ui/ExperienceItem"; 6 | import experiences from "@/data/experiences"; 7 | 8 | const HomeSection3 = ({ id }: { id: string }) => { 9 | return ( 10 | 14 | 15 | Experiences 16 | 17 | 18 | {experiences.map((experience, index) => { 19 | return ( 20 | 21 | ); 22 | })} 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default HomeSection3; 30 | -------------------------------------------------------------------------------- /src/hooks/useScrolled.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | const useScrolled = (offset?: number) => { 6 | const [scrolled, setScrolled] = useState(false); 7 | 8 | useEffect(() => { 9 | let tempOffset = 40; 10 | if ( 11 | !offset || 12 | offset < 0 || 13 | offset > window.innerHeight || 14 | typeof offset !== "number" || 15 | isNaN(offset) || 16 | !isFinite(offset) || 17 | offset === Infinity || 18 | offset === -Infinity || 19 | offset === 0 || 20 | offset === null || 21 | offset === undefined 22 | ) { 23 | tempOffset = 40; 24 | } 25 | 26 | const handleScroll = () => { 27 | if (window.scrollY > tempOffset) { 28 | setScrolled(true); 29 | } else { 30 | setScrolled(false); 31 | } 32 | }; 33 | 34 | if (document.readyState === "complete") handleScroll(); 35 | 36 | window.addEventListener("scroll", handleScroll); 37 | 38 | return () => window.removeEventListener("scroll", handleScroll); 39 | }, [offset]); 40 | 41 | return scrolled; 42 | }; 43 | 44 | export default useScrolled; 45 | -------------------------------------------------------------------------------- /public/skills/golang-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import { useEffect } from "react"; 5 | 6 | const Error = ({ 7 | error, 8 | reset, 9 | }: { 10 | error: Error & { digest?: string }; 11 | reset: () => void; 12 | }) => { 13 | useEffect(() => { 14 | // Log the error to Sentry 15 | Sentry.captureException(error); 16 | }, [error]); 17 | 18 | return ( 19 |
22 |

23 | Something went wrong...!!! 24 |

25 | 26 |
27 |

{error.message}

28 |
29 | 30 | 39 |
40 | ); 41 | }; 42 | 43 | export default Error; 44 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import PageBox from "@/components/core/PageBox"; 3 | import { menuItems } from "@/data/navMenus"; 4 | import HomeSection1 from "@/components/home/Section1"; 5 | 6 | const HomeSection2 = dynamic(() => import("@/components/home/Section2")); 7 | const HomeSection3 = dynamic(() => import("@/components/home/Section3")); 8 | const HomeSection4 = dynamic(() => import("@/components/home/Section4")); 9 | const HomeSection5 = dynamic(() => import("@/components/home/Section5")); 10 | const HomeSection6 = dynamic(() => import("@/components/home/Section6")); 11 | 12 | const FloatingNavbar = dynamic(() => 13 | import("@/components/navbar/FloatingNavbar") 14 | ); 15 | const ScrollToTop = dynamic(() => import("@/components/common/ScrollToTop")); 16 | 17 | const Home = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Home; 33 | -------------------------------------------------------------------------------- /src/components/common/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faArrowUpLong } from "@fortawesome/free-solid-svg-icons"; 6 | import styles from "@/app/scroll.module.scss"; 7 | 8 | const ScrollToTop = () => { 9 | const [showTopBtn, setShowTopBtn] = useState(false); 10 | 11 | useEffect(() => { 12 | const handleShowBtn = () => { 13 | if (window.scrollY > 400) { 14 | setShowTopBtn(true); 15 | } else { 16 | setShowTopBtn(false); 17 | } 18 | }; 19 | 20 | window.addEventListener("scroll", handleShowBtn); 21 | 22 | return () => window.removeEventListener("scroll", handleShowBtn); 23 | }, []); 24 | 25 | const goToTop = () => { 26 | window.scrollTo({ 27 | top: 0, 28 | behavior: "smooth", 29 | }); 30 | }; 31 | 32 | return ( 33 |
34 | {showTopBtn ? ( 35 | 38 | ) : null} 39 |
40 | ); 41 | }; 42 | export default ScrollToTop; 43 | -------------------------------------------------------------------------------- /src/data/socialLinks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | faGithub, 3 | faLinkedin, 4 | faTelegram, 5 | faInstagram, 6 | faXTwitter, 7 | } from "@fortawesome/free-brands-svg-icons"; 8 | import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; 9 | import Strings from "@/constants/strings"; 10 | import type { ISocialLinkItem } from "@/types"; 11 | 12 | const socialLinks: ISocialLinkItem[] = [ 13 | { 14 | name: Strings.github, 15 | url: Strings.githubLink, 16 | icon: faGithub, 17 | text: Strings.githubUsername, 18 | }, 19 | { 20 | name: Strings.linkedIn, 21 | url: Strings.linkedInLink, 22 | icon: faLinkedin, 23 | text: Strings.linkedInUsername, 24 | }, 25 | { 26 | name: Strings.telegram, 27 | url: Strings.telegramLink, 28 | icon: faTelegram, 29 | text: Strings.telegramUsername, 30 | }, 31 | { 32 | name: Strings.instagram, 33 | url: Strings.instagramLink, 34 | icon: faInstagram, 35 | text: Strings.instagramUsername, 36 | }, 37 | { 38 | name: Strings.twitter, 39 | url: Strings.twitterLink, 40 | icon: faXTwitter, 41 | text: Strings.twitterUsername, 42 | }, 43 | { 44 | name: Strings.email, 45 | url: Strings.primaryEmailLink, 46 | icon: faEnvelope, 47 | text: Strings.primaryEmail, 48 | }, 49 | ]; 50 | 51 | export default socialLinks; 52 | -------------------------------------------------------------------------------- /src/components/home/Section6.tsx: -------------------------------------------------------------------------------- 1 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 2 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 3 | import GridBox from "@/components/core/GridBox"; 4 | import Column from "@/components/core/Column"; 5 | import SectionTitle from "@/components/common/SectionTitle"; 6 | import SocialButton from "./ui/SocialButton"; 7 | import socialLinks from "@/data/socialLinks"; 8 | 9 | const HomeSection6 = ({ id }: { id: string }) => { 10 | return ( 11 | 15 | 16 | Get in Touch with me 17 | 18 | 19 | 20 | {socialLinks.map((link, index) => { 21 | return ( 22 | 28 | ); 29 | })} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default HomeSection6; 42 | -------------------------------------------------------------------------------- /public/skills/redux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import NextError from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | const Error = ({ 8 | error, 9 | reset, 10 | }: { 11 | error: Error & { digest?: string }; 12 | reset: () => void; 13 | }) => { 14 | useEffect(() => { 15 | Sentry.captureException(error); 16 | }, [error]); 17 | 18 | return ( 19 | 20 | 21 |
24 |

25 | Something went wrong...!!! 26 |

27 | 28 |
29 |

30 | {error.message} 31 |

32 |
33 | 34 | 43 |
44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default Error; 52 | -------------------------------------------------------------------------------- /src/components/core/Modal.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { ReactNode } from "react"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faXmark } from "@fortawesome/free-solid-svg-icons"; 5 | import Column from "@/components/core/Column"; 6 | 7 | const Modal = ({ 8 | children, 9 | closeHref, 10 | }: Readonly<{ children: ReactNode; closeHref: any }>) => { 11 | return ( 12 |
13 |
14 | 19 | 23 | 24 | 25 | 26 | {children} 27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default Modal; 34 | -------------------------------------------------------------------------------- /public/skills/nestjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/nodejs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/home/ui/SkillItem.tsx: -------------------------------------------------------------------------------- 1 | import type { ISkillListItem } from "@/types"; 2 | import Image from "next/image"; 3 | import CardBox from "@/components/core/CardBox"; 4 | import Row from "@/components/core/Row"; 5 | import Column from "@/components/core/Column"; 6 | 7 | const SkillItem = ({ data }: { data: ISkillListItem }) => { 8 | return ( 9 | 10 |

{data.title}

11 | 12 | {data.items.length > 0 ? ( 13 | 14 | {data.items.map((skill, index) => { 15 | return ( 16 | 20 | {skill.icon ? ( 21 | {`logo-${skill.title}`} 32 | ) : null} 33 | 34 |

{skill.title}

35 |
36 | ); 37 | })} 38 |
39 | ) : null} 40 |
41 | ); 42 | }; 43 | 44 | export default SkillItem; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio-nextjs", 3 | "version": "1.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "push": "git add -A && git commit -m \"Update Changes & Bug Fixes\" && git push origin master" 11 | }, 12 | "dependencies": { 13 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 14 | "@fortawesome/free-brands-svg-icons": "^6.5.2", 15 | "@fortawesome/free-regular-svg-icons": "^6.5.2", 16 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 17 | "@fortawesome/react-fontawesome": "^0.2.2", 18 | "@sentry/nextjs": "^8.12.0", 19 | "@tsparticles/engine": "^3.4.0", 20 | "@tsparticles/react": "^3.0.0", 21 | "@tsparticles/slim": "^3.4.0", 22 | "animejs": "^3.2.2", 23 | "clsx": "^2.1.1", 24 | "framer-motion": "^11.2.12", 25 | "lodash": "^4.17.21", 26 | "mini-svg-data-uri": "^1.4.4", 27 | "next": "^14.2.4", 28 | "react": "^18.3.1", 29 | "react-dom": "^18.3.1", 30 | "react-wrap-balancer": "^1.1.1", 31 | "tailwind-merge": "^2.3.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.24.7", 35 | "@tailwindcss/typography": "^0.5.13", 36 | "@types/animejs": "^3.1.12", 37 | "@types/lodash": "^4.17.5", 38 | "@types/node": "^20.14.8", 39 | "@types/react": "^18.3.3", 40 | "autoprefixer": "^10.4.19", 41 | "eslint": "^9.5.0", 42 | "eslint-config-next": "^14.2.4", 43 | "postcss": "^8.4.38", 44 | "postcss-nesting": "^12.1.5", 45 | "sass": "^1.77.6", 46 | "tailwindcss": "^3.4.4", 47 | "tailwindcss-debug-screens": "^2.2.1", 48 | "typescript": "^5.5.2" 49 | } 50 | } -------------------------------------------------------------------------------- /src/data/navMenus.ts: -------------------------------------------------------------------------------- 1 | import { INavItem, INavMenuItem } from "@/types"; 2 | import { 3 | faHome, 4 | faUser, 5 | faBriefcase, 6 | faTimeline, 7 | faAward, 8 | faLaptopCode, 9 | faEnvelope, 10 | } from "@fortawesome/free-solid-svg-icons"; 11 | 12 | const navMenus: INavMenuItem[] = [ 13 | { 14 | id: "about", 15 | title: "About", 16 | path: "/#about", 17 | section: "about", 18 | }, 19 | { 20 | id: "services", 21 | title: "Services", 22 | path: "/#services", 23 | section: "services", 24 | }, 25 | { 26 | id: "experiences", 27 | title: "Experiences", 28 | path: "/#experiences", 29 | section: "experiences", 30 | }, 31 | { 32 | id: "skills", 33 | title: "Skills", 34 | path: "/#skills", 35 | section: "skills", 36 | }, 37 | { 38 | id: "projects", 39 | title: "Projects", 40 | path: "/#projects", 41 | section: "projects", 42 | }, 43 | { 44 | id: "contact", 45 | title: "Contact", 46 | path: "/#contact", 47 | section: "contact", 48 | }, 49 | ]; 50 | 51 | export default navMenus; 52 | 53 | export const menuItems: INavItem[] = [ 54 | { 55 | name: "About", 56 | link: "/#about", 57 | icon: faUser, 58 | }, 59 | { 60 | name: "Services", 61 | link: "/#services", 62 | icon: faBriefcase, 63 | }, 64 | { 65 | name: "Experiences", 66 | link: "/#experiences", 67 | icon: faTimeline, 68 | }, 69 | { 70 | name: "Skills", 71 | link: "/#skills", 72 | icon: faAward, 73 | }, 74 | { 75 | name: "Projects", 76 | link: "/#projects", 77 | icon: faLaptopCode, 78 | }, 79 | { 80 | name: "Contact", 81 | link: "/#contact", 82 | icon: faEnvelope, 83 | }, 84 | ]; 85 | -------------------------------------------------------------------------------- /src/components/common/HoverLayoutGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { cn } from "@/utils/cn"; 6 | import { AnimatedServiceCard } from "../home/ui/AnimatedServiceCard"; 7 | 8 | export const HoverLayoutGrid = ({ 9 | cards, 10 | className, 11 | }: { 12 | cards: any[]; 13 | className?: string; 14 | }) => { 15 | const [hoveredIndex, setHoveredIndex] = useState(null); 16 | 17 | return ( 18 |
24 | {cards.map((item, idx) => ( 25 |
setHoveredIndex(idx)} 29 | onMouseLeave={() => setHoveredIndex(null)} 30 | > 31 | 32 | {hoveredIndex === idx && ( 33 | 46 | )} 47 | 48 | 49 |
50 | ))} 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/projects/components/ScreenshotGallery.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useSearchParams } from "next/navigation"; 4 | import Link from "next/link"; 5 | import Image from "next/image"; 6 | import Row from "@/components/core/Row"; 7 | import Column from "@/components/core/Column"; 8 | 9 | const ScreenshotGallery = ({ 10 | imageList, 11 | }: Readonly<{ imageList: string[] }>) => { 12 | const pathname = usePathname(); 13 | const searchParams = useSearchParams(); 14 | 15 | return ( 16 | 17 | 18 | {imageList.map((img, index) => { 19 | return ( 20 | 29 | {`screenshot-${index}`} 40 | 41 | ); 42 | })} 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default ScreenshotGallery; 49 | -------------------------------------------------------------------------------- /src/data/experiences.ts: -------------------------------------------------------------------------------- 1 | import { IExperienceItem } from "@/types"; 2 | 3 | const experiences: IExperienceItem[] = [ 4 | { 5 | designation: "Full Stack Developer Project Based Intern", 6 | company: "BTPN Syariah", 7 | startDate: "Dec 2023", 8 | endDate: "Jan 2024", 9 | isCurrentJob: false, 10 | location: "Remote", 11 | shortDescription: 12 | "During this Project Based Internship, I learned about the role of a Full Stack Developer at Bank BTPN Syariah", 13 | description: 14 | "I was expected to master skills and tools used by Full Stack Developers, such as JavaScript, MySQL, Git, and others. I contributed to developing IT services tailored to user needs at Bank BTPN.", 15 | }, 16 | { 17 | designation: "Junior Software Engineer", 18 | company: "Production Team Gereja Isa Almasih", 19 | startDate: "Jul 2024", 20 | endDate: "PRESENT", 21 | isCurrentJob: false, 22 | location: "Remote", 23 | shortDescription: 24 | "contributed to the development and maintenance of the website, ensuring its functionality and performance.", 25 | description: 26 | "During my time here, I learned how to create a landing page for a team, utilizing skills such as HTML, CSS, and Python, and working with backend technologies.", 27 | }, 28 | { 29 | designation: "Rocket Academy Graduated", 30 | company: "Rocket Academy", 31 | startDate: "Sept 2023", 32 | endDate: "Nov 2023", 33 | isCurrentJob: false, 34 | location: "Remote", 35 | shortDescription: 36 | "Studied the fundamentals of JavaScript and its application in web development.", 37 | description: 38 | "The course expanded my network by connecting me with individuals in Singapore and enhanced my English-speaking skills", 39 | }, 40 | ]; 41 | 42 | export default experiences; 43 | -------------------------------------------------------------------------------- /public/skills/docker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/core/CardBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { MouseEvent } from "react"; 4 | import type { CoreComponentsProps } from "@/types"; 5 | import { motion, useMotionTemplate, useSpring } from "framer-motion"; 6 | 7 | const CardBox = (props: Readonly) => { 8 | const { children, classNames, onClick, id, elementRef } = props; 9 | 10 | const mouseX = useSpring(0, { stiffness: 500, damping: 100 }); 11 | const mouseY = useSpring(0, { stiffness: 500, damping: 100 }); 12 | 13 | function onMouseMove(e: MouseEvent) { 14 | if (!e.currentTarget) return; 15 | const { left, top } = e.currentTarget.getBoundingClientRect(); 16 | mouseX.set(e.clientX - left); 17 | mouseY.set(e.clientY - top); 18 | } 19 | let maskImage = useMotionTemplate`radial-gradient(240px at ${mouseX}px ${mouseY}px, white, transparent)`; 20 | let style = { maskImage, WebkitMaskImage: maskImage }; 21 | 22 | return ( 23 |
30 |
31 |
32 | 36 | 40 |
41 | {children} 42 |
43 | ); 44 | }; 45 | 46 | export default CardBox; 47 | -------------------------------------------------------------------------------- /src/components/home/ui/ProjectList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createRef } from "react"; 4 | import { IProjectItem } from "@/types"; 5 | import Row from "@/components/core/Row"; 6 | import ProjectItem from "./ProjectItem"; 7 | import Column from "@/components/core/Column"; 8 | 9 | const ProjectList = ({ projects }: Readonly<{ projects: IProjectItem[] }>) => { 10 | const carouselRef = createRef(); 11 | 12 | const _handleOnClickPrev = () => { 13 | if (!carouselRef || carouselRef.current === null) return; 14 | 15 | let offset = 400; 16 | if (window.innerWidth < 480) offset = 280; 17 | 18 | carouselRef.current.scrollLeft -= offset; 19 | }; 20 | 21 | const _handleOnClickNext = () => { 22 | if (!carouselRef || carouselRef.current === null) return; 23 | 24 | let offset = 400; 25 | if (window.innerWidth < 480) offset = 280; 26 | 27 | carouselRef.current.scrollLeft += offset; 28 | }; 29 | 30 | return ( 31 | 32 | 36 | {projects.map((item, index) => { 37 | return ; 38 | })} 39 | 40 | 41 | 42 | 49 | 50 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default ProjectList; 64 | -------------------------------------------------------------------------------- /src/data/services.ts: -------------------------------------------------------------------------------- 1 | import { IServiceItem } from "@/types"; 2 | 3 | const services: IServiceItem[] = [ 4 | { 5 | id: `1`, 6 | title: "Web Development", 7 | icons: [ 8 | "/skills/redux.svg", 9 | "/skills/react.svg", 10 | "/skills/nextjs.png", 11 | "/skills/html.svg", 12 | "/skills/css.svg", 13 | ], 14 | shortDescription: "I build visually stunning and user-friendly websites.", 15 | description: 16 | "I deliver stunning, user-friendly websites to establish your online presence. From simple sites to complex e-commerce platforms, I provide tailored solutions using the latest frameworks and technologies for a seamless, responsive, and SEO-friendly browsing experience. Enhance your online identity with quality.", 17 | }, 18 | { 19 | id: 2, 20 | title: "Backend Development", 21 | icons: [ 22 | "/skills/socket-io.png", 23 | "/skills/docker.png", 24 | "/skills/nodejs.svg", 25 | "/skills/express.svg", 26 | "/skills/aws.svg", 27 | ], 28 | shortDescription: "I create robust and scalable backend infrastructures.", 29 | description: 30 | "I enhance digital applications with robust, scalable backend infrastructures. I develop efficient database structures, APIs, and configure servers for optimal performance, security, and scalability, ensuring your applications handle high traffic and complex data management seamlessly. Rely on strong backend solutions.", 31 | }, 32 | { 33 | id: 3, 34 | title: "Database Management", 35 | icons: [ 36 | "/skills/mysql.svg", 37 | "/skills/postgresql.svg", 38 | "/skills/mongodb.svg", 39 | "/skills/redis.svg", 40 | "/skills/sqlite.svg", 41 | ], 42 | shortDescription: "I manage and optimize your database systems.", 43 | description: 44 | "I manage and optimize your database systems for performance, reliability, and scalability. With expertise in SQL and NoSQL databases, I design schemas, write complex queries, and implement best practices for data integrity and security. Ensure your data is managed effectively and efficiently.", 45 | }, 46 | ]; 47 | 48 | export default services; 49 | -------------------------------------------------------------------------------- /src/components/common/FlipWords.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useEffect, useState } from "react"; 4 | import { AnimatePresence, motion } from "framer-motion"; 5 | import { cn } from "@/utils/cn"; 6 | 7 | export const FlipWords = ({ 8 | words, 9 | duration = 3000, 10 | className, 11 | }: { 12 | words: string[]; 13 | duration?: number; 14 | className?: string; 15 | }) => { 16 | const [currentWord, setCurrentWord] = useState(words[0]); 17 | const [isAnimating, setIsAnimating] = useState(false); 18 | 19 | const startAnimation = useCallback(() => { 20 | const word = words[words.indexOf(currentWord) + 1] || words[0]; 21 | setCurrentWord(word); 22 | setIsAnimating(true); 23 | }, [currentWord, words]); 24 | 25 | useEffect(() => { 26 | if (!isAnimating) 27 | setTimeout(() => { 28 | startAnimation(); 29 | }, duration); 30 | }, [isAnimating, duration, startAnimation]); 31 | 32 | return ( 33 | { 35 | setIsAnimating(false); 36 | }} 37 | > 38 | 68 | {currentWord.split(/(?<=\s)/).map((letter, index) => ( 69 | 79 | {letter} 80 | 81 | ))} 82 | 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/hooks/useVisibleSection.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from "react"; 2 | import { throttle } from "lodash"; 3 | import navMenus from "@/data/navMenus"; 4 | 5 | function useVisibleSection(sections = navMenus) { 6 | const [visibleSectionId, setVisibleSectionId] = useState( 7 | sections[0].id 8 | ); 9 | 10 | const isSectionVisible = (elementId: string) => { 11 | const section = document.getElementById(elementId); 12 | 13 | if (!section) return false; 14 | 15 | const sectionPosition = section.getBoundingClientRect(); 16 | const vHeight = window.innerHeight || document.documentElement.clientHeight; 17 | 18 | // Calculate the threshold for more than 50% visibility 19 | var threshold = vHeight * 0.5; 20 | 21 | // Check if more than 50% of the element is visible from the start or end in the viewport 22 | return ( 23 | (sectionPosition.top <= threshold && 24 | sectionPosition.bottom >= threshold) || // More than 50% from the start 25 | (sectionPosition.bottom >= vHeight - threshold && 26 | sectionPosition.top <= vHeight - threshold) // More than 50% from the end 27 | ); 28 | }; 29 | 30 | const checkVisibility = useCallback(() => { 31 | if (!sections || sections.length < 1) return; 32 | 33 | sections.forEach(({ id }) => { 34 | const isVisible = isSectionVisible(id); 35 | 36 | if (isVisible) { 37 | setVisibleSectionId(id); 38 | } 39 | }); 40 | }, [sections]); 41 | 42 | useEffect(() => { 43 | const handler = throttle(checkVisibility, 300); 44 | 45 | if (document.readyState === "complete") handler(); 46 | 47 | window.addEventListener("DOMContentLoaded", handler); 48 | window.addEventListener("load", handler); 49 | window.addEventListener("scroll", handler); 50 | window.addEventListener("resize", handler); 51 | 52 | return () => { 53 | window.removeEventListener("DOMContentLoaded", handler); 54 | window.removeEventListener("load", handler); 55 | window.removeEventListener("scroll", handler); 56 | window.removeEventListener("resize", handler); 57 | }; 58 | }, [checkVisibility]); 59 | 60 | return visibleSectionId; 61 | } 62 | 63 | export default useVisibleSection; 64 | -------------------------------------------------------------------------------- /src/components/home/ui/ExperienceItem.tsx: -------------------------------------------------------------------------------- 1 | import type { IExperienceItem } from "@/types"; 2 | import { Balancer } from "react-wrap-balancer"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faTrophy } from "@fortawesome/free-solid-svg-icons"; 5 | import CardBox from "@/components/core/CardBox"; 6 | import Column from "@/components/core/Column"; 7 | 8 | const ExperienceItem = ({ data }: { data: IExperienceItem }) => { 9 | return ( 10 | 11 | 12 | 13 | 17 | 18 | 19 |

{data.designation}

20 | 21 |

22 | {data.company} 23 |

24 |
25 | 26 |
30 |

{data.startDate}

31 | 32 | - 33 | 34 |

35 | {data.isCurrentJob ? "Present" : data.endDate} 36 |

37 |
38 | 39 |

40 | {data.shortDescription} 41 |

42 |
43 | 44 |
45 |

46 | {data.description} 47 |

48 |
49 |
50 | ); 51 | }; 52 | 53 | export default ExperienceItem; 54 | -------------------------------------------------------------------------------- /public/skills/sqlite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import defaultTheme from "tailwindcss/defaultTheme"; 2 | 3 | const svgToDataUri = require("mini-svg-data-uri"); 4 | 5 | const colors = require("tailwindcss/colors"); 6 | const { 7 | default: flattenColorPalette, 8 | } = require("tailwindcss/lib/util/flattenColorPalette"); 9 | 10 | /** @type {import('tailwindcss').Config} */ 11 | module.exports = { 12 | content: [ 13 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 14 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 15 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 16 | ], 17 | theme: { 18 | extend: { 19 | animation: { 20 | move: "move 5s linear infinite", 21 | }, 22 | keyframes: { 23 | move: { 24 | "0%": { transform: "translateX(-200px)" }, 25 | "100%": { transform: "translateX(200px)" }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | plugins: [ 31 | require("@tailwindcss/typography"), 32 | require("tailwindcss-debug-screens"), 33 | addVariablesForColors, 34 | function ({ matchUtilities, theme }: any) { 35 | matchUtilities( 36 | { 37 | "bg-grid": (value: any) => ({ 38 | backgroundImage: `url("${svgToDataUri( 39 | `` 40 | )}")`, 41 | }), 42 | "bg-grid-small": (value: any) => ({ 43 | backgroundImage: `url("${svgToDataUri( 44 | `` 45 | )}")`, 46 | }), 47 | "bg-dot": (value: any) => ({ 48 | backgroundImage: `url("${svgToDataUri( 49 | `` 50 | )}")`, 51 | }), 52 | }, 53 | { values: flattenColorPalette(theme("backgroundColor")), type: "color" } 54 | ); 55 | }, 56 | ], 57 | }; 58 | 59 | function addVariablesForColors({ addBase, theme }: any) { 60 | let allColors = flattenColorPalette(theme("colors")); 61 | let newVars = Object.fromEntries( 62 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]) 63 | ); 64 | 65 | addBase({ 66 | ":root": newVars, 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.scss"; 2 | import { Poppins } from "next/font/google"; 3 | import { ReactNode } from "react"; 4 | import { Metadata } from "next"; 5 | import Script from "next/script"; 6 | import LocalConfig from "@/constants/config"; 7 | import { WebVitals } from "@/components/common/WebVitals"; 8 | 9 | const poppins = Poppins({ 10 | weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], 11 | style: ["normal", "italic"], 12 | subsets: ["latin", "latin-ext"], 13 | display: "swap", 14 | preload: true, 15 | fallback: [ 16 | "system-ui", 17 | "arial", 18 | "BlinkMacSystemFont", 19 | "Segoe UI", 20 | "Roboto", 21 | "Oxygen", 22 | "Ubuntu", 23 | "Fira Sans", 24 | "Droid Sans", 25 | ], 26 | }); 27 | 28 | export const metadata: Metadata = { 29 | title: "Bryan Jericho", 30 | description: 31 | "I'm Bryan, currently leveraging knowledge at Hasanuddin University. I'm really passionate about becoming a software engineer and exploring the world of technology.", 32 | robots: { 33 | index: true, 34 | follow: true, 35 | googleBot: { 36 | index: true, 37 | follow: true, 38 | "max-video-preview": -1, 39 | "max-image-preview": "large", 40 | "max-snippet": -1, 41 | }, 42 | }, 43 | icons: [ 44 | { 45 | url: "/favicon.ico", 46 | rel: "icon", 47 | sizes: "any", 48 | type: "image/svg+xml", 49 | }, 50 | ], 51 | keywords: [ 52 | "Bryan Jericho", 53 | ], 54 | }; 55 | 56 | const RootLayout = ({ children }: Readonly<{ children: ReactNode }>) => { 57 | return ( 58 | 59 | 60 | 75 | 76 | 77 | 82 | {process.env.NODE_ENV === "development" ? : null} 83 |
{children}
84 | 85 | 86 | ); 87 | }; 88 | 89 | export default RootLayout; 90 | -------------------------------------------------------------------------------- /src/components/projects/Section1.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import Image from "next/image"; 5 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 6 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 7 | import Row from "@/components/core/Row"; 8 | import Column from "@/components/core/Column"; 9 | import AppBar from "@/components/common/AppBar"; 10 | import ScreenshotGallery from "./components/ScreenshotGallery"; 11 | import { getProjectDetails } from "@/data/projects"; 12 | import { ProjectType } from "@/types"; 13 | 14 | const ProjectsSection1 = ({ id }: Readonly<{ id?: string }>) => { 15 | const searchParams = useSearchParams(); 16 | const project = getProjectDetails(searchParams.get("id")!); 17 | 18 | const renderProjectType = (type?: ProjectType) => { 19 | switch (type) { 20 | case ProjectType.Personal: 21 | return "Personal Project"; 22 | 23 | case ProjectType.JobWork: 24 | return "Job Work"; 25 | 26 | case ProjectType.Freelance: 27 | return "Freelance Project"; 28 | 29 | default: 30 | return null; 31 | } 32 | }; 33 | 34 | return ( 35 | 36 | 37 | 38 | {project ? ( 39 | 40 | 41 | {`project-${project.title}`} 52 | 53 | 54 | 55 |

{project?.title}

56 |

57 | {renderProjectType(project.projectType)} 58 |

59 |
60 |
61 | ) : null} 62 |
63 | 64 | {project && project.sceenshots && project.sceenshots.length > 0 ? ( 65 | 66 | ) : null} 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default ProjectsSection1; 73 | -------------------------------------------------------------------------------- /public/skills/jinja.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { IconDefinition, IconProp } from "@fortawesome/fontawesome-svg-core"; 2 | import type { MouseEventHandler, ReactNode, RefObject } from "react"; 3 | 4 | export interface INavMenuItem { 5 | id: string; 6 | title: string; 7 | path: string; 8 | section: string; 9 | submenu?: INavMenuItem[]; 10 | } 11 | 12 | export interface INavItem { 13 | name: string; 14 | link: string; 15 | icon: IconProp; 16 | } 17 | 18 | export interface IExperienceItem { 19 | designation: string; 20 | company: string; 21 | startDate: string; 22 | endDate: string; 23 | isCurrentJob: boolean; 24 | location: string; 25 | shortDescription: string; 26 | description: string; 27 | } 28 | 29 | export enum RepoType { 30 | Public, 31 | Private, 32 | } 33 | 34 | export enum ProjectType { 35 | Personal, 36 | JobWork, 37 | Freelance, 38 | } 39 | 40 | export interface IProjectItem { 41 | id: string; 42 | title: string; 43 | description: string; 44 | icon: string; 45 | repoType: RepoType; 46 | projectType?: ProjectType; 47 | githubUrl?: string; 48 | url?: string; 49 | tags?: string[]; 50 | sceenshots?: string[]; 51 | about?: string; 52 | } 53 | 54 | export type IServiceItem = { 55 | id: number | string; 56 | title: string; 57 | icon?: IconDefinition; 58 | shortDescription: string; 59 | description: string; 60 | icons: string[]; 61 | }; 62 | 63 | export interface ISkillListItem { 64 | title: string; 65 | items: ISkillItem[]; 66 | } 67 | 68 | export enum SkillLevel { 69 | Expert, 70 | Intermediate, 71 | Begginer, 72 | } 73 | 74 | export interface ISkillItem { 75 | title: string; 76 | level?: SkillLevel; 77 | icon?: string; 78 | } 79 | 80 | export interface ISocialLinkItem { 81 | url: string; 82 | icon: IconDefinition; 83 | text: string; 84 | name?: string; 85 | } 86 | 87 | export interface MenutItemProps { 88 | items: INavMenuItem; 89 | depthLevel: number; 90 | mobileNav: boolean; 91 | handleCloseMobileMenu: () => void; 92 | current?: string; 93 | } 94 | 95 | export interface DropdownMenuProps 96 | extends Omit { 97 | submenus: INavMenuItem[]; 98 | dropdown: boolean; 99 | } 100 | 101 | export interface ButtonComponentProps { 102 | label: string; 103 | onClick: MouseEventHandler; 104 | classNames?: string; 105 | name?: string; 106 | } 107 | 108 | export interface CoreComponentsProps { 109 | children: ReactNode; 110 | classNames?: string; 111 | onClick?: MouseEventHandler; 112 | id?: string; 113 | elementRef?: RefObject; 114 | } 115 | 116 | export interface ViewportProps { 117 | root?: null | undefined; 118 | rootMargin?: string | undefined; 119 | threshold?: number | undefined; 120 | } 121 | 122 | export interface ShootingStarProps { 123 | vw: number; 124 | vh: number; 125 | } 126 | -------------------------------------------------------------------------------- /public/skills/express.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/navbar/FloatingNavbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, AnimatePresence } from "framer-motion"; 4 | import { cn } from "@/utils/cn"; 5 | import Link from "next/link"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faHandHoldingDollar } from "@fortawesome/free-solid-svg-icons"; 8 | import { INavItem } from "@/types"; 9 | 10 | const FloatingNavbar = ({ 11 | navItems, 12 | className, 13 | }: { 14 | navItems: INavItem[]; 15 | className?: string; 16 | }) => { 17 | return ( 18 | 19 | 36 | {navItems.map((navItem: INavItem, idx: number) => ( 37 | 44 | 45 | 50 | 51 | 52 | {navItem.name} 53 | 54 | 55 | ))} 56 | 61 | 62 | 63 | 64 | Support Me 65 | 66 | 67 | 68 | 69 | ); 70 | 71 | 72 | }; 73 | 74 | export default FloatingNavbar; 75 | -------------------------------------------------------------------------------- /src/components/home/Section1.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import Column from "@/components/core/Column"; 4 | import ConstraintedBox from "@/components/core/ConstraintedBox"; 5 | import ResponsiveBox from "@/components/core/ResponsiveBox"; 6 | import Row from "@/components/core/Row"; 7 | import socialLinks from "@/data/socialLinks"; 8 | import ResumeButton from "./ui/ResumeButton"; 9 | import TalkButton from "./ui/TalkButton"; 10 | import { FlipWords } from "../common/FlipWords"; 11 | 12 | const HomeSection1 = ({ id }: Readonly<{ id: string }>) => { 13 | return ( 14 | 18 | {} 19 | 20 | 21 | 22 |
23 |

24 | Hi there, I am 25 |

26 | 30 | 31 |
32 |

33 | Software Engineer & CTF Rookie 34 |

35 | 36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 |

Follow me here

44 | 45 | 46 | {socialLinks.slice(0, 5).map((link, index) => { 47 | return ( 48 | 55 | 56 | 57 | 58 | 59 | ); 60 | })} 61 | 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default HomeSection1; 69 | -------------------------------------------------------------------------------- /src/data/projects.ts: -------------------------------------------------------------------------------- 1 | import { IProjectItem, ProjectType, RepoType } from "@/types"; 2 | 3 | const projects: IProjectItem[] = [ 4 | { 5 | id: "social-media-app-flutter", 6 | title: "Simple notes app", 7 | description: 8 | "A minimalist notes app built with Next.js, TypeScript, and Tailwind CSS, featuring a rich text editor and dark mode with a navy blue and light blue color palette.", 9 | icon: "/skills/typescript.svg", 10 | repoType: RepoType.Public, 11 | projectType: ProjectType.Personal, 12 | githubUrl: "https://github.com/BryanJericho/simpleNotes-app", 13 | url: "https://simple-notes-app-lime.vercel.app/", 14 | tags: ["Typescript", "Next JS","Tailwind"], 15 | }, 16 | { 17 | id: "e-commerce-app-mern", 18 | title: "Mindparents", 19 | description: 20 | " Developed a web-based information system using AI to assist parents.", 21 | icon: "/skills/nextjs.svg", 22 | repoType: RepoType.Public, 23 | projectType: ProjectType.Personal, 24 | githubUrl: "https://github.com/naddiyh/mindparents-fe", 25 | url: "https://mindparents.vercel.app/", 26 | tags: ["Next.js", "Firebase", "Typescript", "Firebase"], 27 | }, 28 | { 29 | id: "social-media-app-flutter", 30 | title: "Website of Production Team", 31 | description: 32 | "I created this website with some collaboration and also enchanting my team works skill (Still in progress)", 33 | icon: "/skills/github.svg", 34 | repoType: RepoType.Public, 35 | projectType: ProjectType.Personal, 36 | githubUrl: "https://github.com/BryanJericho/ProductionTeam", 37 | url: "https://github.com/BryanJericho/ProductionTeam", 38 | tags: ["Html", "CSS", "Firebase", "Tailwind"], 39 | }, 40 | { 41 | id: "video-calling-app-flutter", 42 | title: "Sistem Akademik", 43 | description: 44 | "A very simple academic system to organize their students and lecturers", 45 | icon: "/skills/firebase.svg", 46 | repoType: RepoType.Public, 47 | projectType: ProjectType.Personal, 48 | githubUrl: "https://github.com/BryanJericho/siakad23", 49 | url: "https://siakad-pi.vercel.app/", 50 | tags: ["Flask", "Python", "Firebase", "Jinja"], 51 | }, 52 | { 53 | id: "social-media-api-nodejs", 54 | title: "Book Management API", 55 | description: 56 | "Implementing simple API for CRUD using Golang", 57 | icon: "/skills/golang-1.svg", 58 | repoType: RepoType.Public, 59 | projectType: ProjectType.Personal, 60 | githubUrl: "https://github.com/BryanJericho/Book-Management-API", 61 | url: "https://github.com/BryanJericho/Book-Management-API", 62 | tags: ["Golang"], 63 | }, 64 | ]; 65 | 66 | export default projects; 67 | 68 | export function getProjectName(id: string) { 69 | const item = projects.find((e) => e.id === id); 70 | 71 | if (!item) return null; 72 | 73 | return item.title; 74 | } 75 | 76 | export function getProjectDetails(id: string): IProjectItem | null { 77 | const item = projects.find((e) => e.id === id); 78 | 79 | if (!item) return null; 80 | 81 | return item; 82 | } 83 | -------------------------------------------------------------------------------- /src/components/navbar/MenuItems.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef } from "react"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faCaretDown } from "@fortawesome/free-solid-svg-icons"; 6 | import Dropdown from "@/components/navbar/Dropdown"; 7 | import { MenutItemProps } from "@/types"; 8 | 9 | const MenuItems = (props: MenutItemProps) => { 10 | const { items, depthLevel, mobileNav, handleCloseMobileMenu, current } = 11 | props; 12 | 13 | const [dropdown, setDropdown] = useState(false); 14 | 15 | let ref = useRef(null); 16 | 17 | const handleScroll = (id: string) => { 18 | if (!id) return; 19 | const element = document.getElementById(id); 20 | if (!element) return; 21 | const elementPosition = element.offsetTop; 22 | const offsetPosition = elementPosition; 23 | 24 | window.scroll({ 25 | top: offsetPosition, 26 | behavior: "smooth", 27 | }); 28 | }; 29 | 30 | useEffect(() => { 31 | const handler = (event: MouseEvent | TouchEvent) => { 32 | if ( 33 | dropdown && 34 | ref.current && 35 | !ref.current.contains(event.target as Node) 36 | ) { 37 | setDropdown(false); 38 | } 39 | }; 40 | 41 | document.addEventListener("mousedown", handler); 42 | document.addEventListener("touchstart", handler); 43 | 44 | return () => { 45 | document.removeEventListener("mousedown", handler); 46 | document.removeEventListener("touchstart", handler); 47 | }; 48 | }, [dropdown]); 49 | 50 | const onMouseEnter = () => { 51 | window.innerWidth > 900 && setDropdown(true); 52 | }; 53 | 54 | const onMouseLeave = () => { 55 | window.innerWidth > 900 && setDropdown(false); 56 | }; 57 | 58 | return ( 59 |
  • 65 | {items.submenu ? ( 66 | <> 67 | 78 | 85 | 86 | ) : ( 87 | 97 | )}{" "} 98 |
  • 99 | ); 100 | }; 101 | 102 | export default MenuItems; 103 | -------------------------------------------------------------------------------- /src/components/home/Section1 copy.tsx: -------------------------------------------------------------------------------- 1 | // import Image from "next/image"; 2 | // import { Balancer } from "react-wrap-balancer"; 3 | // import Column from "@/components/core/Column"; 4 | // import ConstraintedBox from "@/components/core/ConstraintedBox"; 5 | // import GridBox from "@/components/core/GridBox"; 6 | // import ResponsiveBox from "@/components/core/ResponsiveBox"; 7 | // import Row from "@/components/core/Row"; 8 | 9 | // const HomeSection2 = ({ id }: Readonly<{ id: string }>) => { 10 | // return ( 11 | // 15 | // 16 | // 17 | // 18 | // 19 | //

    Hi 👋 I'm

    20 | 21 | //

    22 | // Nikhil Rajput 23 | //

    24 | 25 | //

    26 | // Software Enginner & Full Stack Developer 27 | //

    28 | 29 | //

    30 | // 31 | // Welcome to my portfolio! I am a goal-oriented and 32 | // results-driven Full Stack Developer from India with 2+ years 33 | // of experience in developing dynamic web applications and 34 | // robust backend APIs. I am skilled in front-end and back-end 35 | // development using modern technologies such as Node.js, 36 | // React.js, Next.js, Express.js, Flutter, GetX and Redux 37 | // Toolkit. I demonstrate a quick learning ability and a passion 38 | // for keeping up with industry advancements. I am a strong 39 | // problem-solver who excels in collaborative team settings. 40 | // 41 | //

    42 | //
    43 | //
    44 | 45 | // 46 | // 47 | // profile 63 | // 64 | // 65 | //
    66 | //
    67 | //
    68 | // ); 69 | // }; 70 | 71 | // export default HomeSection2; 72 | -------------------------------------------------------------------------------- /public/skills/redis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/skills.ts: -------------------------------------------------------------------------------- 1 | import { ISkillListItem, SkillLevel } from "@/types"; 2 | 3 | const skills: ISkillListItem[] = [ 4 | { 5 | title: "Programming Languages", 6 | items: [ 7 | { 8 | title: "JavaScript", 9 | level: SkillLevel.Expert, 10 | icon: "/skills/javascript.svg", 11 | }, 12 | { 13 | title: "TypeScript", 14 | level: SkillLevel.Intermediate, 15 | icon: "/skills/typescript.svg", 16 | }, 17 | { 18 | title: "Golang", 19 | level: SkillLevel.Intermediate, 20 | icon: "/skills/golang-1.svg", 21 | } 22 | ], 23 | }, 24 | { 25 | title: "Frontend Development", 26 | items: [ 27 | { 28 | title: "Next.js", 29 | level: SkillLevel.Expert, 30 | icon: "/skills/nextjs.png", 31 | }, 32 | { 33 | title: "React.js", 34 | level: SkillLevel.Expert, 35 | icon: "/skills/react.svg", 36 | }, 37 | { 38 | title: "HTML", 39 | level: SkillLevel.Expert, 40 | icon: "/skills/html.svg", 41 | }, 42 | { 43 | title: "CSS", 44 | level: SkillLevel.Intermediate, 45 | icon: "/skills/css.svg", 46 | }, 47 | { 48 | title: "SASS", 49 | level: SkillLevel.Intermediate, 50 | icon: "/skills/sass.svg", 51 | } 52 | ], 53 | }, 54 | { 55 | title: "Backend Development", 56 | items: [ 57 | { 58 | title: "Node.js", 59 | level: SkillLevel.Expert, 60 | icon: "/skills/nodejs.svg", 61 | }, 62 | { 63 | title: "Express.js", 64 | level: SkillLevel.Expert, 65 | icon: "/skills/express.svg", 66 | }, 67 | { 68 | title: "Flask", 69 | level: SkillLevel.Expert, 70 | icon: "/skills/flask.svg", 71 | }, 72 | { 73 | title: "Jinja", 74 | level: SkillLevel.Expert, 75 | icon: "/skills/jinja.svg", 76 | } 77 | ], 78 | }, 79 | { 80 | title: "Database Management", 81 | items: [ 82 | { 83 | title: "MongoDB", 84 | level: SkillLevel.Intermediate, 85 | icon: "/skills/mongodb.svg", 86 | }, 87 | { 88 | title: "Firebase", 89 | level: SkillLevel.Intermediate, 90 | icon: "/skills/firebase.svg", 91 | }, 92 | { 93 | title: "MySQL", 94 | level: SkillLevel.Begginer, 95 | icon: "/skills/mysql.svg", 96 | }, 97 | ], 98 | }, 99 | { 100 | title: "DevOps/VCS", 101 | items: [ 102 | { 103 | title: "Docker", 104 | level: SkillLevel.Begginer, 105 | icon: "/skills/docker.png", 106 | }, 107 | { 108 | title: "AWS", 109 | level: SkillLevel.Intermediate, 110 | icon: "/skills/aws.svg", 111 | }, 112 | { 113 | title: "Git", 114 | level: SkillLevel.Expert, 115 | icon: "/skills/git.svg", 116 | }, 117 | { 118 | title: "GitHub", 119 | level: SkillLevel.Expert, 120 | icon: "/skills/github.svg", 121 | }, 122 | ], 123 | }, 124 | { 125 | title: "Miscellaneous", 126 | items: [ 127 | { 128 | title: "Ubuntu", 129 | level: SkillLevel.Intermediate, 130 | icon: "/skills/ubuntu.png", 131 | }, 132 | ], 133 | }, 134 | { 135 | title: "Nontechnical Skills", 136 | items: [ 137 | { 138 | title: "Problem Solving", 139 | level: SkillLevel.Expert, 140 | icon: "/images/logical-thinking.png", 141 | }, 142 | { 143 | title: "Collaboration", 144 | level: SkillLevel.Expert, 145 | icon: "/images/collaboration.png", 146 | }, 147 | { 148 | title: "Analytical Skills", 149 | level: SkillLevel.Expert, 150 | icon: "/images/analytical-skills.png", 151 | }, 152 | ], 153 | }, 154 | ]; 155 | 156 | export default skills; 157 | -------------------------------------------------------------------------------- /public/skills/aws.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/postgresql.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/sass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/home/ui/ProjectItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { RepoType, type IProjectItem } from "@/types"; 5 | import { Balancer } from "react-wrap-balancer"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faGithub } from "@fortawesome/free-brands-svg-icons"; 8 | import { faEye } from "@fortawesome/free-solid-svg-icons"; 9 | import Image from "next/image"; 10 | import Link from "next/link"; 11 | import Column from "@/components/core/Column"; 12 | import Row from "@/components/core/Row"; 13 | import CardBox from "@/components/core/CardBox"; 14 | 15 | const ProjectItem = ({ project }: { project: IProjectItem }) => { 16 | const router = useRouter(); 17 | 18 | const _handleNavigateToPage = (id: string) => { 19 | if (!id || id.length < 1) return; 20 | 21 | router.push(`/projects?id=${id}`); 22 | }; 23 | 24 | return ( 25 | _handleNavigateToPage(project.id)} 28 | > 29 | 30 | 31 | {`project-${project.title}`} 42 | 43 | 44 |

    {project.title}

    45 | 46 |
    53 |

    54 | {project.repoType === RepoType.Private ? "Private" : "Public"} 55 |

    56 |
    57 | 58 | 59 | {project.githubUrl ? ( 60 | 66 | 70 | 71 | ) : null} 72 | 73 | {project.url ? ( 74 | 80 | 84 | 85 | ) : null} 86 | 87 |
    88 | 89 | 90 |

    91 | {project.description} 92 |

    93 | 94 | {project.tags && project.tags.length > 0 ? ( 95 | 96 | {project.tags.map((tag, i) => { 97 | return ( 98 |

    102 | {tag} 103 |

    104 | ); 105 | })} 106 |
    107 | ) : null} 108 |
    109 |
    110 | ); 111 | }; 112 | 113 | export default ProjectItem; 114 | -------------------------------------------------------------------------------- /src/components/common/TypewriterEffect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { cn } from "@/utils/cn"; 5 | import { motion, stagger, useAnimate, useInView } from "framer-motion"; 6 | 7 | export const TypewriterEffect = ({ 8 | words, 9 | className, 10 | delay, 11 | duration, 12 | cursorClassName, 13 | }: { 14 | words: { 15 | text: string; 16 | className?: string; 17 | }[]; 18 | className?: string; 19 | delay?: number; 20 | duration?: number; 21 | cursorClassName?: string; 22 | }) => { 23 | // split text inside of words into array of characters 24 | const wordsArray = words.map((word) => { 25 | return { 26 | ...word, 27 | text: word.text.split(""), 28 | }; 29 | }); 30 | 31 | const [scope, animate] = useAnimate(); 32 | const isInView = useInView(scope); 33 | 34 | useEffect(() => { 35 | if (isInView) { 36 | animate( 37 | "span", 38 | { 39 | display: "inline-block", 40 | opacity: 1, 41 | width: "fit-content", 42 | }, 43 | { 44 | duration: 0.5, 45 | delay: stagger(0.1), 46 | ease: "easeInOut", 47 | } 48 | ); 49 | } 50 | }, [isInView]); 51 | 52 | const renderWords = () => { 53 | return ( 54 | 55 | {wordsArray.map((word, idx) => { 56 | return ( 57 | 58 | {word.text.map((char, index) => ( 59 | 67 | {char} 68 | 69 | ))} 70 |   71 | 72 | ); 73 | })} 74 | 75 | ); 76 | }; 77 | return ( 78 | 85 | {renderWords()} 86 | 103 | 104 | ); 105 | }; 106 | 107 | export const TypewriterEffectSmooth = ({ 108 | words, 109 | className, 110 | delay, 111 | duration, 112 | }: { 113 | words: { 114 | text: string; 115 | className?: string; 116 | }[]; 117 | className?: string; 118 | delay?: number; 119 | duration?: number; 120 | }) => { 121 | // split text inside of words into array of characters 122 | const wordsArray = words.map((word) => { 123 | return { 124 | ...word, 125 | text: word.text.split(""), 126 | }; 127 | }); 128 | 129 | const renderWords = () => { 130 | return ( 131 | 132 | {wordsArray.map((word, idx) => { 133 | return ( 134 | 135 | {word.text.map((char, index) => ( 136 | 143 | {char} 144 | 145 | ))} 146 |   147 | 148 | ); 149 | })} 150 | 151 | ); 152 | }; 153 | 154 | return ( 155 | 165 | 169 | {renderWords()} 170 | 171 | 172 | ); 173 | }; 174 | -------------------------------------------------------------------------------- /public/skills/mysql.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/skills/jenkins.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/navbar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef, useState } from "react"; 4 | import Link from "next/link"; 5 | import Image from "next/image"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faBars, faXmark } from "@fortawesome/free-solid-svg-icons"; 8 | import useOnClickOutside from "@/hooks/useOnClickOutside"; 9 | import navMenus from "@/data/navMenus"; 10 | import MenuItems from "@/components/navbar/MenuItems"; 11 | import useVisibleSection from "@/hooks/useVisibleSection"; 12 | import useScrolled from "@/hooks/useScrolled"; 13 | 14 | const NavBar = () => { 15 | const mobileMenuRef = useRef(null); 16 | const navRef = useRef(null); 17 | 18 | const visibleSection = useVisibleSection(); 19 | const isScrolled = useScrolled(); 20 | 21 | const [mobileMenuVisible, setMobileMenuVisible] = useState(false); 22 | 23 | const toggleMenu = () => { 24 | if (!mobileMenuRef.current) return; 25 | const cList = mobileMenuRef.current.classList; 26 | if ( 27 | cList.contains("flex") && 28 | cList.contains("opacity-1") && 29 | cList.contains("z-[1000]") && 30 | cList.contains("visible") 31 | ) { 32 | hideMobileMenu(); 33 | } else { 34 | showMobileMenu(); 35 | } 36 | }; 37 | 38 | const showMobileMenu = () => { 39 | if (!mobileMenuRef.current) return; 40 | const cList = mobileMenuRef.current.classList; 41 | 42 | if ( 43 | cList.contains("hidden") && 44 | cList.contains("opacity-0") && 45 | cList.contains("z-0") && 46 | cList.contains("invisible") 47 | ) { 48 | cList.remove("hidden"); 49 | cList.remove("opacity-0"); 50 | cList.remove("z-0"); 51 | cList.remove("invisible"); 52 | } 53 | 54 | cList.add("flex"); 55 | cList.add("opacity-1"); 56 | cList.add("z-[1000]"); 57 | cList.add("visible"); 58 | setMobileMenuVisible(true); 59 | }; 60 | 61 | const hideMobileMenu = () => { 62 | if (!mobileMenuRef.current) return; 63 | const cList = mobileMenuRef.current.classList; 64 | 65 | if ( 66 | cList.contains("flex") && 67 | cList.contains("opacity-1") && 68 | cList.contains("z-[1000]") && 69 | cList.contains("visible") 70 | ) { 71 | cList.remove("flex"); 72 | cList.remove("opacity-1"); 73 | cList.remove("z-[1000]"); 74 | cList.remove("visible"); 75 | } 76 | 77 | cList.add("hidden"); 78 | cList.add("opacity-0"); 79 | cList.add("z-0"); 80 | cList.add("invisible"); 81 | setMobileMenuVisible(false); 82 | }; 83 | 84 | useOnClickOutside(navRef, hideMobileMenu); 85 | 86 | return ( 87 |
    95 |
    96 |
    97 | 101 | profile 112 | 113 | 114 |
    115 | 134 | 135 | {/* Menu Items */} 136 | 137 |
    141 | 153 | 154 |
    155 | {navMenus.map((menu, index) => { 156 | const depthLevel = 0; 157 | return ( 158 | 166 | ); 167 | })} 168 |
    169 |
    170 |
    171 |
    172 |
    173 |
    174 | ); 175 | }; 176 | 177 | export default NavBar; 178 | -------------------------------------------------------------------------------- /src/components/home/ui/AnimatedServiceCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { animate, motion } from "framer-motion"; 4 | import { useEffect } from "react"; 5 | import { cn } from "@/utils/cn"; 6 | import { IServiceItem } from "@/types"; 7 | import Image from "next/image"; 8 | 9 | export function AnimatedServiceCard({ 10 | item, 11 | }: Readonly<{ item: IServiceItem }>) { 12 | return ( 13 | 14 | 15 | 16 | 17 | {item.title} 18 | {item.description} 19 | 20 | ); 21 | } 22 | 23 | const IconSkeleton = ({ item }: Readonly<{ item: IServiceItem }>) => { 24 | const scale = [1, 1.1, 1]; 25 | const transform = ["translateY(0px)", "translateY(-4px)", "translateY(0px)"]; 26 | const sequence = [ 27 | [ 28 | ".circle-1", 29 | { 30 | scale, 31 | transform, 32 | }, 33 | { duration: 0.8 }, 34 | ], 35 | [ 36 | ".circle-2", 37 | { 38 | scale, 39 | transform, 40 | }, 41 | { duration: 0.8 }, 42 | ], 43 | [ 44 | ".circle-3", 45 | { 46 | scale, 47 | transform, 48 | }, 49 | { duration: 0.8 }, 50 | ], 51 | [ 52 | ".circle-4", 53 | { 54 | scale, 55 | transform, 56 | }, 57 | { duration: 0.8 }, 58 | ], 59 | [ 60 | ".circle-5", 61 | { 62 | scale, 63 | transform, 64 | }, 65 | { duration: 0.8 }, 66 | ], 67 | ]; 68 | 69 | useEffect(() => { 70 | // @ts-ignore 71 | animate(sequence, { 72 | repeat: Infinity, 73 | repeatDelay: 1, 74 | }); 75 | }, []); 76 | 77 | return ( 78 |
    79 |
    80 | 81 | {`icon-1`} 92 | 93 | 94 | {`icon-2`} 105 | 106 | 107 | {`icon-3`} 118 | 119 | 120 | {`icon-4`} 131 | 132 | 133 | {`icon-5`} 144 | 145 |
    146 | 147 |
    148 |
    149 | 150 |
    151 |
    152 |
    153 | ); 154 | }; 155 | 156 | const Sparkles = () => { 157 | const randomMove = () => Math.random() * 2 - 1; 158 | const randomOpacity = () => Math.random(); 159 | const random = () => Math.random(); 160 | return ( 161 |
    162 | {[...Array(12)].map((_, i) => ( 163 | 187 | ))} 188 |
    189 | ); 190 | }; 191 | 192 | export const Card = ({ 193 | className, 194 | children, 195 | }: { 196 | className?: string; 197 | children: React.ReactNode; 198 | }) => { 199 | return ( 200 |
    206 |
    207 |
    {children}
    208 |
    209 |
    210 | ); 211 | }; 212 | 213 | export const CardTitle = ({ 214 | children, 215 | className, 216 | }: { 217 | children: React.ReactNode; 218 | className?: string; 219 | }) => { 220 | return ( 221 |

    227 | {children} 228 |

    229 | ); 230 | }; 231 | 232 | export const CardDescription = ({ 233 | children, 234 | className, 235 | }: { 236 | children: React.ReactNode; 237 | className?: string; 238 | }) => { 239 | return ( 240 |

    246 | {children} 247 |

    248 | ); 249 | }; 250 | 251 | export const CardSkeletonContainer = ({ 252 | className, 253 | children, 254 | showGradient = true, 255 | }: { 256 | className?: string; 257 | children: React.ReactNode; 258 | showGradient?: boolean; 259 | }) => { 260 | return ( 261 |
    269 | {children} 270 |
    271 | ); 272 | }; 273 | 274 | const IconContainer = ({ 275 | className, 276 | children, 277 | }: { 278 | className?: string; 279 | children: React.ReactNode; 280 | }) => { 281 | return ( 282 |
    290 | {children} 291 |
    292 | ); 293 | }; 294 | -------------------------------------------------------------------------------- /public/skills/Flask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/apis/data.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | const revalidate = 60; 4 | const MINUTES_5 = 60 * 5; 5 | const HOURS_1 = 60 * 60; 6 | const HOURS_12 = 60 * 60 * 12; 7 | 8 | export async function getUser(username: string) { 9 | console.log("Fetching user data for", username); 10 | console.time("getUser"); 11 | const res = await fetch("https://api.github.com/users/" + username, { 12 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 13 | next: { revalidate: revalidate }, 14 | }); 15 | console.timeEnd("getUser"); 16 | return res.json(); 17 | } 18 | 19 | export async function getRepos(username: string) { 20 | console.log("Fetching repos for", username); 21 | console.time("getRepos"); 22 | const res = await fetch( 23 | "https://api.github.com/users/" + username + "/repos", 24 | { 25 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 26 | next: { revalidate: revalidate }, 27 | } 28 | ); 29 | console.timeEnd("getRepos"); 30 | return res.json(); 31 | } 32 | 33 | export async function getSocialAccounts(username: string) { 34 | console.log("Fetching social accounts for", username); 35 | console.time("getSocialAccounts"); 36 | const res = await fetch( 37 | "https://api.github.com/users/" + username + "/social_accounts", 38 | { 39 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 40 | next: { revalidate: MINUTES_5 }, 41 | } 42 | ); 43 | console.timeEnd("getSocialAccounts"); 44 | return res.json(); 45 | } 46 | 47 | export const getPinnedRepos = cache(async (username: string) => { 48 | console.log("Fetching pinned repos for", username); 49 | console.time("getPinnedRepos"); 50 | const res = await fetch("https://api.github.com/graphql", { 51 | method: "POST", 52 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 53 | body: JSON.stringify({ 54 | query: `{user(login: "${username}") {pinnedItems(first: 6, types: REPOSITORY) {nodes {... on Repository {name}}}}}`, 55 | }), 56 | }); 57 | const pinned = await res.json(); 58 | console.timeEnd("getPinnedRepos"); 59 | const names = pinned.data.user.pinnedItems.nodes.map( 60 | (node: any) => node.name 61 | ); 62 | return names; 63 | }); 64 | 65 | export const getUserOrganizations = async (username: string) => { 66 | console.log("Fetching organizations for", username); 67 | console.time("getUserOrganizations"); 68 | const res = await fetch("https://api.github.com/graphql", { 69 | next: { revalidate: MINUTES_5 }, 70 | method: "POST", 71 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 72 | body: JSON.stringify({ 73 | query: `{user(login: "${username}") {organizations(first: 6) {nodes {name,websiteUrl,url,avatarUrl,description}}}}`, 74 | }), 75 | }); 76 | console.timeEnd("getUserOrganizations"); 77 | return res.json(); 78 | }; 79 | 80 | export const getVercelProjects = async () => { 81 | if (!process.env.VC_TOKEN) { 82 | console.log("No Vercel token found - no projects will be shown."); 83 | return { projects: [] }; 84 | } 85 | console.log("Fetching Vercel projects"); 86 | console.time("getVercelProjects"); 87 | const res = await fetch("https://api.vercel.com/v9/projects", { 88 | headers: { Authorization: `Bearer ${process.env.VC_TOKEN}` }, 89 | }); 90 | console.timeEnd("getVercelProjects"); 91 | // eg. expired token. 92 | if (!res.ok) { 93 | console.error("Vercel API returned an error.", res.status, res.statusText); 94 | return { projects: [] }; 95 | } 96 | return res.json(); 97 | }; 98 | 99 | /** Cache revalidated every 12 hours. */ 100 | export const getNextjsLatestRelease = cache(async () => { 101 | const res = await fetch("https://api.github.com/graphql", { 102 | method: "POST", 103 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 104 | body: JSON.stringify({ 105 | query: `{ 106 | repository(name: "next.js", owner: "vercel") { 107 | latestRelease { 108 | tagName 109 | updatedAt 110 | } 111 | } 112 | }`, 113 | }), 114 | next: { revalidate: HOURS_12 }, 115 | }); 116 | if (!res.ok) { 117 | console.error("GitHub API returned an error.", res.status, res.statusText); 118 | return {}; 119 | } 120 | const nextjsLatest = await res.json(); 121 | 122 | const result = { 123 | tagName: nextjsLatest.data.repository.latestRelease.tagName.replace( 124 | "v", 125 | "" 126 | ), 127 | updatedAt: nextjsLatest.data.repository.latestRelease.updatedAt, 128 | }; 129 | return result; 130 | }); 131 | 132 | export const getRepositoryPackageJson = cache( 133 | async (username: string, reponame: string) => { 134 | const res = await fetch("https://api.github.com/graphql", { 135 | method: "POST", 136 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 137 | body: JSON.stringify({ 138 | query: `{ 139 | repository(name: "${reponame}", owner: "${username}") { 140 | object(expression: "HEAD:package.json") { 141 | ... on Blob { 142 | text 143 | } 144 | } 145 | } 146 | }`, 147 | }), 148 | next: { revalidate: HOURS_12 }, 149 | }); 150 | const response = await res.json(); 151 | try { 152 | const packageJson = JSON.parse(response.data.repository.object.text); 153 | return packageJson; 154 | } catch (error) { 155 | console.error("Error parsing package.json", error); 156 | return {}; 157 | } 158 | } 159 | ); 160 | 161 | export const getRecentUserActivity = cache(async (username: string) => { 162 | console.log("Fetching recent activity for", username); 163 | console.time("getRecentUserActivity"); 164 | const res = await fetch( 165 | "https://api.github.com/users/" + username + "/events", 166 | { 167 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 168 | next: { revalidate: HOURS_12 }, 169 | } 170 | ); 171 | const response = await res.json(); 172 | console.timeEnd("getRecentUserActivity"); 173 | return response; 174 | }); 175 | 176 | export const getTrafficPageViews = async ( 177 | username: string, 178 | reponame: string 179 | ) => { 180 | const res = await fetch( 181 | "https://api.github.com/repos/" + 182 | username + 183 | "/" + 184 | reponame + 185 | "/traffic/views", 186 | { 187 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 188 | next: { revalidate: HOURS_1 }, 189 | } 190 | ); 191 | const response = await res.json(); 192 | 193 | const sumUniques = response.uniques || 0; 194 | 195 | // Today date in format YYYY-MM-DD. 196 | const today = new Date().toISOString().slice(0, 10); 197 | // Last day with at least one view. 198 | const todayUniques = 199 | response.views?.find((day: any) => day.timestamp.startsWith(today)) 200 | ?.uniques || 0; 201 | 202 | return { sumUniques, todayUniques }; 203 | }; 204 | 205 | export const getDependabotAlerts = cache( 206 | async (username: string, reponame: string) => { 207 | const res = await fetch( 208 | "https://api.github.com/repos/" + 209 | username + 210 | "/" + 211 | reponame + 212 | "/dependabot/alerts", 213 | { 214 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 215 | next: { revalidate: HOURS_12 }, 216 | } 217 | ); 218 | 219 | const response = await res.json(); 220 | 221 | // Id dependabot is not enabled, the response will be an object, not an array. 222 | if (response.length === undefined) { 223 | return []; 224 | } 225 | const openAlertsBySeverity = response.reduce((acc: any, alert: any) => { 226 | if (alert.state === "open") { 227 | acc[alert.security_advisory.severity] = acc[ 228 | alert.security_advisory.severity 229 | ] 230 | ? acc[alert.security_advisory.severity] + 1 231 | : 1; 232 | } 233 | return acc; 234 | }, {}); 235 | 236 | return openAlertsBySeverity; 237 | } 238 | ); 239 | 240 | /** 241 | * Determines if a repository is using Next.js App Router or legacy pages/_app.jsx. Or both. 242 | * @param {*} repoOwner GitHub username 243 | * @param {string} repoName repository name 244 | * @returns Object with two booleans: isRouterPages and isRouterApp 245 | */ 246 | export async function checkAppJsxTsxExistence( 247 | repoOwner: string, 248 | repoName: string 249 | ) { 250 | const urlPagesAppJSX = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/pages/_app.jsx`; 251 | 252 | const urlAppLayoutJSX = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/app/layout.jsx`; 253 | 254 | const urlPagesAppTSX = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/pages/_app.tsx`; 255 | const urlAppLayoutTSX = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/app/layout.tsx`; 256 | 257 | const res = { 258 | isRouterPagesJSX: false, 259 | isRouterAppJSX: false, 260 | isRouterPagesTSX: false, 261 | isRouterAppTSX: false, 262 | }; 263 | 264 | try { 265 | const [isPagesResJSX, isAppLayoutResJSX, isPagesResTSX, isAppLayoutResTSX] = 266 | await Promise.all([ 267 | fetch(urlPagesAppJSX, { 268 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 269 | next: { revalidate: HOURS_12 }, 270 | }), 271 | fetch(urlAppLayoutJSX, { 272 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 273 | next: { revalidate: HOURS_12 }, 274 | }), 275 | fetch(urlPagesAppTSX, { 276 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 277 | next: { revalidate: HOURS_12 }, 278 | }), 279 | fetch(urlAppLayoutTSX, { 280 | headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` }, 281 | next: { revalidate: HOURS_12 }, 282 | }), 283 | ]); 284 | 285 | if (isPagesResJSX.status === 200) { 286 | res.isRouterPagesJSX = true; 287 | } 288 | 289 | if (isAppLayoutResJSX.status === 200) { 290 | res.isRouterAppJSX = true; 291 | } 292 | 293 | if (isPagesResTSX.status === 200) { 294 | res.isRouterPagesJSX = true; 295 | } 296 | 297 | if (isAppLayoutResTSX.status === 200) { 298 | res.isRouterAppJSX = true; 299 | } 300 | } catch (error: any) { 301 | console.error( 302 | `Error checking _app.jsx(tsx) existence in ${repoName}: ${error.message}` 303 | ); 304 | } 305 | 306 | return res; 307 | } 308 | -------------------------------------------------------------------------------- /public/skills/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/globals.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | .text-edge-outline { 7 | -webkit-text-stroke: 1px rgba(255, 255, 255, 0.3); 8 | } 9 | } 10 | 11 | @layer utilities { 12 | 13 | /* Hide scrollbar for Chrome, Safari and Opera */ 14 | .no-scrollbar::-webkit-scrollbar { 15 | display: none; 16 | } 17 | 18 | /* Hide scrollbar for IE, Edge and Firefox */ 19 | .no-scrollbar { 20 | -ms-overflow-style: none; 21 | /* IE and Edge */ 22 | scrollbar-width: none; 23 | /* Firefox */ 24 | } 25 | } 26 | 27 | :root { 28 | --maxWidth: 1200px; 29 | --borderRadius: 1.5rem; 30 | --defaultRadius: 1.5rem; 31 | 32 | --dialogColor10: rgba(18, 22, 27, 0.1); 33 | --dialogColor20: rgba(18, 22, 27, 0.2); 34 | --dialogColor30: rgba(18, 22, 27, 0.3); 35 | --dialogColor40: rgba(18, 22, 27, 0.4); 36 | --dialogColor50: rgba(18, 22, 27, 0.5); 37 | --dialogColor60: rgba(18, 22, 27, 0.6); 38 | --dialogColor70: rgba(18, 22, 27, 0.7); 39 | --dialogColor80: rgba(18, 22, 27, 0.8); 40 | --dialogColor90: rgba(18, 22, 27, 0.9); 41 | --dialogColor100: rgba(18, 22, 27, 1); 42 | --dialogColor: rgb(18, 22, 27); 43 | 44 | --bgColor10: rgba(10, 13, 15, 0.1); 45 | --bgColor20: rgba(10, 13, 15, 0.2); 46 | --bgColor30: rgba(10, 13, 15, 0.3); 47 | --bgColor40: rgba(10, 13, 15, 0.4); 48 | --bgColor50: rgba(10, 13, 15, 0.5); 49 | --bgColor60: rgba(10, 13, 15, 0.6); 50 | --bgColor70: rgba(10, 13, 15, 0.7); 51 | --bgColor80: rgba(10, 13, 15, 0.8); 52 | --bgColor90: rgba(10, 13, 15, 0.9); 53 | --bgColor: rgba(10, 13, 15, 1); 54 | 55 | --lightGrayColor: #ebebeb; 56 | --lighterGrayColor: #e1e1e1; 57 | 58 | --borderColor: rgba(220, 220, 220, 1); 59 | --darkGrayColor: rgb(160, 160, 160); 60 | 61 | --linkColor10: rgba(0, 113, 255, 0.1); 62 | --linkColor20: rgba(0, 113, 255, 0.2); 63 | --linkColor30: rgba(0, 113, 255, 0.3); 64 | --linkColor40: rgba(0, 113, 255, 0.4); 65 | --linkColor50: rgba(0, 113, 255, 0.5); 66 | --linkColor60: rgba(0, 113, 255, 0.6); 67 | --linkColor70: rgba(0, 113, 255, 0.7); 68 | --linkColor80: rgba(0, 113, 255, 0.8); 69 | --linkColor90: rgba(0, 113, 255, 0.9); 70 | --linkColor100: rgba(0, 113, 255, 1); 71 | --linkColor: rgba(0, 113, 255, 1); 72 | 73 | --primaryColor10: rgba(0, 177, 113, 0.1); 74 | --primaryColor20: rgba(0, 177, 113, 0.2); 75 | --primaryColor30: rgba(0, 177, 113, 0.3); 76 | --primaryColor40: rgba(0, 177, 113, 0.4); 77 | --primaryColor50: rgba(0, 177, 113, 0.5); 78 | --primaryColor60: rgba(0, 177, 113, 0.6); 79 | --primaryColor70: rgba(0, 177, 113, 0.7); 80 | --primaryColor80: rgba(0, 177, 113, 0.8); 81 | --primaryColor90: rgba(0, 177, 113, 0.9); 82 | --primaryColor100: rgba(0, 177, 113, 1); 83 | --primaryColor: rgba(0, 177, 113, 1); 84 | 85 | --secondaryColor10: rgba(0, 237, 138, 0.1); 86 | --secondaryColor20: rgba(0, 237, 138, 0.2); 87 | --secondaryColor30: rgba(0, 237, 138, 0.3); 88 | --secondaryColor40: rgba(0, 237, 138, 0.4); 89 | --secondaryColor50: rgba(0, 237, 138, 0.5); 90 | --secondaryColor60: rgba(0, 237, 138, 0.6); 91 | --secondaryColor70: rgba(0, 237, 138, 0.7); 92 | --secondaryColor80: rgba(0, 237, 138, 0.8); 93 | --secondaryColor90: rgba(0, 237, 138, 0.9); 94 | --secondaryColor100: rgba(0, 237, 138, 1); 95 | --secondaryColor: rgba(35, 230, 134, 1); 96 | 97 | --successColor10: rgba(30, 180, 80, 0.1); 98 | --successColor20: rgba(30, 180, 80, 0.2); 99 | --successColor30: rgba(30, 180, 80, 0.3); 100 | --successColor40: rgba(30, 180, 80, 0.4); 101 | --successColor50: rgba(30, 180, 80, 0.5); 102 | --successColor60: rgba(30, 180, 80, 0.6); 103 | --successColor70: rgba(30, 180, 80, 0.7); 104 | --successColor80: rgba(30, 180, 80, 0.8); 105 | --successColor90: rgba(30, 180, 80, 0.9); 106 | --successColor100: rgba(30, 180, 80, 1); 107 | --successColor: rgba(30, 180, 80, 1); 108 | 109 | --errorColor10: rgba(244, 67, 54, 0.1); 110 | --errorColor20: rgba(244, 67, 54, 0.2); 111 | --errorColor30: rgba(244, 67, 54, 0.3); 112 | --errorColor40: rgba(244, 67, 54, 0.4); 113 | --errorColor50: rgba(244, 67, 54, 0.5); 114 | --errorColor60: rgba(244, 67, 54, 0.6); 115 | --errorColor70: rgba(244, 67, 54, 0.7); 116 | --errorColor80: rgba(244, 67, 54, 0.8); 117 | --errorColor90: rgba(244, 67, 54, 0.9); 118 | --errorColor100: rgba(244, 67, 54, 1); 119 | --errorColor: rgba(244, 67, 54, 1); 120 | 121 | --warningColor10: rgba(240, 156, 0, 0.1); 122 | --warningColor20: rgba(240, 156, 0, 0.2); 123 | --warningColor30: rgba(240, 156, 0, 0.3); 124 | --warningColor40: rgba(240, 156, 0, 0.4); 125 | --warningColor50: rgba(240, 156, 0, 0.5); 126 | --warningColor60: rgba(240, 156, 0, 0.6); 127 | --warningColor70: rgba(240, 156, 0, 0.7); 128 | --warningColor80: rgba(240, 156, 0, 0.8); 129 | --warningColor90: rgba(240, 156, 0, 0.9); 130 | --warningColor100: rgba(240, 156, 0, 1); 131 | --warningColor: rgba(240, 156, 0, 1); 132 | 133 | --textColor10: rgba(248, 248, 248, 0.1); 134 | --textColor20: rgba(248, 248, 248, 0.2); 135 | --textColor30: rgba(248, 248, 248, 0.3); 136 | --textColor40: rgba(248, 248, 248, 0.4); 137 | --textColor50: rgba(248, 248, 248, 0.5); 138 | --textColor60: rgba(248, 248, 248, 0.6); 139 | --textColor70: rgba(248, 248, 248, 0.7); 140 | --textColor80: rgba(248, 248, 248, 0.8); 141 | --textColor90: rgba(248, 248, 248, 0.9); 142 | --textColor100: rgba(248, 248, 248, 1); 143 | --textColor: rgba(248, 248, 248, 1); 144 | 145 | --textColorLight10: rgba(180, 180, 180, 0.1); 146 | --textColorLight20: rgba(180, 180, 180, 0.2); 147 | --textColorLight30: rgba(180, 180, 180, 0.3); 148 | --textColorLight40: rgba(180, 180, 180, 0.4); 149 | --textColorLight50: rgba(180, 180, 180, 0.5); 150 | --textColorLight60: rgba(180, 180, 180, 0.6); 151 | --textColorLight70: rgba(180, 180, 180, 0.7); 152 | --textColorLight80: rgba(180, 180, 180, 0.8); 153 | --textColorLight90: rgba(180, 180, 180, 0.9); 154 | --textColorLight100: rgba(180, 180, 180, 1); 155 | --textColorLight: rgba(180, 180, 180, 1); 156 | 157 | --blackColor10: rgba(0, 0, 0, 0.1); 158 | --blackColor20: rgba(0, 0, 0, 0.2); 159 | --blackColor30: rgba(0, 0, 0, 0.3); 160 | --blackColor40: rgba(0, 0, 0, 0.4); 161 | --blackColor50: rgba(0, 0, 0, 0.5); 162 | --blackColor60: rgba(0, 0, 0, 0.6); 163 | --blackColor70: rgba(0, 0, 0, 0.7); 164 | --blackColor80: rgba(0, 0, 0, 0.8); 165 | --blackColor90: rgba(0, 0, 0, 0.9); 166 | --blackColor100: rgba(0, 0, 0, 1); 167 | --blackColor: rgba(0, 0, 0, 1); 168 | 169 | --whiteColor10: rgba(255, 255, 255, 0.1); 170 | --whiteColor20: rgba(255, 255, 255, 0.2); 171 | --whiteColor30: rgba(255, 255, 255, 0.3); 172 | --whiteColor40: rgba(255, 255, 255, 0.4); 173 | --whiteColor50: rgba(255, 255, 255, 0.5); 174 | --whiteColor60: rgba(255, 255, 255, 0.6); 175 | --whiteColor70: rgba(255, 255, 255, 0.7); 176 | --whiteColor80: rgba(255, 255, 255, 0.8); 177 | --whiteColor90: rgba(255, 255, 255, 0.9); 178 | --whiteColor100: rgba(255, 255, 255, 1); 179 | --whiteColor: rgba(255, 255, 255, 1); 180 | 181 | --primaryGrad: linear-gradient(118.18deg, 182 | var(--primaryColor) 0.99%, 183 | var(--secondaryColor) 100%); 184 | 185 | --boxShadow: 0px 4px 12px -6px rgba(0, 0, 0, 0.1), 186 | 0px -4px 12px -6px rgba(0, 0, 0, 0.1), -4px 0px 12px -6px rgba(0, 0, 0, 0.1), 187 | 4px 0px 12px -6px rgba(0, 0, 0, 0.1); 188 | 189 | --navBarShadow: 0 0 10px 0 var(--blackColor10); 190 | } 191 | 192 | * { 193 | box-sizing: border-box; 194 | padding: 0; 195 | margin: 0; 196 | scroll-behavior: smooth; 197 | } 198 | 199 | html, 200 | body { 201 | max-width: 100vw; 202 | overflow-x: hidden; 203 | } 204 | 205 | body { 206 | background-color: var(--bgColor); 207 | font-family: Poppins, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, 208 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 209 | sans-serif; 210 | font-size: 1rem; 211 | font-weight: 400; 212 | line-height: 1.5; 213 | transition: all 0.5s ease-in-out; 214 | color: var(--textColor); 215 | overflow-x: hidden; 216 | scroll-behavior: smooth; 217 | padding: 0 !important; 218 | } 219 | 220 | ::-webkit-scrollbar { 221 | width: 0.25rem; 222 | height: 0.25rem; 223 | } 224 | 225 | /* Track */ 226 | ::-webkit-scrollbar-track { 227 | background: var(--bgColor); 228 | } 229 | 230 | /* Handle */ 231 | ::-webkit-scrollbar-thumb { 232 | background: var(--textColor70); 233 | } 234 | 235 | /* Handle on hover */ 236 | ::-webkit-scrollbar-thumb:hover { 237 | background: var(--textColor80); 238 | } 239 | 240 | /* Chrome, Safari, Edge, Opera */ 241 | input::-webkit-outer-spin-button, 242 | input::-webkit-inner-spin-button { 243 | -webkit-appearance: none; 244 | margin: 0; 245 | } 246 | 247 | a { 248 | color: var(--linkColor); 249 | text-decoration: none; 250 | font-family: inherit; 251 | } 252 | 253 | button { 254 | background-color: transparent; 255 | outline: none; 256 | border: none; 257 | width: fit-content; 258 | height: fit-content; 259 | text-decoration: none; 260 | cursor: pointer; 261 | } 262 | 263 | small { 264 | line-height: 1.5; 265 | font-weight: 300; 266 | font-family: inherit; 267 | transition: all 0.5s ease-in-out; 268 | } 269 | 270 | p { 271 | line-height: 1.5; 272 | font-weight: 400; 273 | font-family: inherit; 274 | transition: all 0.5s ease-in-out; 275 | } 276 | 277 | h1 { 278 | line-height: 1.5; 279 | font-family: inherit; 280 | transition: all 0.5s ease-in-out; 281 | } 282 | 283 | h2 { 284 | line-height: 1.5; 285 | font-family: inherit; 286 | transition: all 0.5s ease-in-out; 287 | } 288 | 289 | h3 { 290 | line-height: 1.5; 291 | font-family: inherit; 292 | transition: all 0.5s ease-in-out; 293 | } 294 | 295 | h4 { 296 | line-height: 1.5; 297 | font-family: inherit; 298 | transition: all 0.5s ease-in-out; 299 | } 300 | 301 | h5 { 302 | line-height: 1.5; 303 | font-family: inherit; 304 | transition: all 0.5s ease-in-out; 305 | } 306 | 307 | h6 { 308 | line-height: 1.5; 309 | font-family: inherit; 310 | transition: all 0.5s ease-in-out; 311 | } 312 | 313 | .app__outlined_btn { 314 | position: relative; 315 | display: flex; 316 | flex-direction: row; 317 | align-items: center; 318 | justify-content: center; 319 | 320 | flex: none; 321 | order: 0; 322 | flex-grow: 0; 323 | 324 | width: fit-content; 325 | outline: none; 326 | background-color: transparent; 327 | border: 1px solid var(--primaryColor); 328 | border-radius: var(--borderRadius); 329 | padding: 0.75rem 1.5rem; 330 | 331 | color: var(--primaryColor); 332 | text-decoration: none; 333 | font-size: 1rem; 334 | font-family: inherit; 335 | text-transform: capitalize; 336 | font-weight: 600; 337 | line-height: 1.5rem; 338 | 339 | overflow: hidden; 340 | z-index: 1; 341 | 342 | cursor: pointer; 343 | transition: all 0.5s ease-in-out; 344 | 345 | &::before { 346 | content: ""; 347 | width: 100%; 348 | height: 100%; 349 | margin: auto; 350 | position: absolute; 351 | top: 0; 352 | left: -100%; 353 | background: var(--primaryColor); 354 | transition: all 0.5s ease; 355 | z-index: -1; 356 | } 357 | 358 | &:hover:before { 359 | top: 0; 360 | left: 0; 361 | border: 0px; 362 | transform: translateX(0); 363 | } 364 | 365 | &:hover { 366 | color: var(--whiteColor); 367 | } 368 | 369 | &:disabled { 370 | &:hover:before { 371 | top: 0; 372 | left: 0; 373 | border: 0px; 374 | transform: translateX(-100%); 375 | } 376 | } 377 | 378 | @media screen and (min-width: 1600px) { 379 | font-size: 1.015rem; 380 | } 381 | 382 | @media screen and (max-width: 768px) { 383 | font-size: 0.925rem; 384 | } 385 | 386 | @media screen and (max-width: 480px) { 387 | font-size: 0.905rem; 388 | } 389 | } 390 | 391 | .app__filled_btn { 392 | position: relative; 393 | display: flex; 394 | flex-direction: row; 395 | align-items: center; 396 | justify-content: center; 397 | 398 | flex: none; 399 | order: 0; 400 | flex-grow: 0; 401 | 402 | width: fit-content; 403 | outline: none; 404 | border: none; 405 | padding: 0.75rem 1.5rem; 406 | 407 | background: var(--primaryColor); 408 | border-radius: var(--borderRadius); 409 | 410 | color: var(--whiteColor); 411 | text-decoration: none; 412 | font-size: 1rem; 413 | font-family: inherit; 414 | text-transform: capitalize; 415 | font-weight: 600; 416 | line-height: 1.5rem; 417 | 418 | overflow: hidden; 419 | z-index: 1; 420 | 421 | cursor: pointer; 422 | transition: all 0.5s ease-in-out; 423 | 424 | &::before { 425 | content: ""; 426 | width: 100%; 427 | height: 100%; 428 | margin: auto; 429 | position: absolute; 430 | top: 0; 431 | left: -100%; 432 | background: var(--secondaryColor); 433 | transition: all 0.5s ease; 434 | z-index: -1; 435 | } 436 | 437 | &:hover:before { 438 | top: 0; 439 | left: 0; 440 | border: 0px; 441 | transform: translateX(0); 442 | } 443 | 444 | &:disabled { 445 | &:hover:before { 446 | top: 0; 447 | left: 0; 448 | border: 0px; 449 | transform: translateX(-100%); 450 | } 451 | } 452 | 453 | @media screen and (min-width: 1600px) { 454 | font-size: 1.015rem; 455 | } 456 | 457 | @media screen and (max-width: 768px) { 458 | font-size: 0.925rem; 459 | } 460 | 461 | @media screen and (max-width: 480px) { 462 | font-size: 0.905rem; 463 | } 464 | } 465 | 466 | .app__text_btn { 467 | display: flex; 468 | flex-direction: row; 469 | align-items: center; 470 | justify-content: center; 471 | 472 | flex: none; 473 | order: 0; 474 | flex-grow: 0; 475 | 476 | outline: none; 477 | background-color: transparent; 478 | border: none; 479 | padding: 0; 480 | margin: 0; 481 | 482 | color: var(--primaryColor); 483 | text-decoration: none; 484 | font-size: 1rem; 485 | font-family: inherit; 486 | text-transform: none; 487 | font-weight: 600; 488 | line-height: 1.5rem; 489 | 490 | cursor: pointer; 491 | transition: all 0.15s ease-in-out; 492 | 493 | &:hover { 494 | text-decoration: underline; 495 | } 496 | 497 | @media screen and (min-width: 1600px) { 498 | font-size: 1.015rem; 499 | } 500 | 501 | @media screen and (max-width: 768px) { 502 | font-size: 0.925rem; 503 | } 504 | 505 | @media screen and (max-width: 480px) { 506 | font-size: 0.905rem; 507 | } 508 | } 509 | 510 | .app__icon_btn { 511 | position: relative; 512 | display: flex; 513 | flex-direction: row; 514 | align-items: center; 515 | justify-content: center; 516 | 517 | flex: none; 518 | order: 0; 519 | flex-grow: 0; 520 | 521 | outline: none; 522 | background-color: var(--primaryColor50); 523 | border: none; 524 | border-radius: 100%; 525 | padding: 0.75rem; 526 | 527 | color: var(--whiteColor); 528 | 529 | font-size: 1rem; 530 | aspect-ratio: 1 / 1; 531 | 532 | overflow: hidden; 533 | z-index: 1; 534 | 535 | cursor: pointer; 536 | transition: all 0.5s ease-in-out; 537 | 538 | &::before { 539 | content: ""; 540 | width: 100%; 541 | height: 100%; 542 | margin: auto; 543 | position: absolute; 544 | top: 0; 545 | left: -100%; 546 | background: var(--primaryColor); 547 | transition: all 0.5s ease; 548 | z-index: -1; 549 | } 550 | 551 | &:hover:before { 552 | top: 0; 553 | left: 0; 554 | border: 0px; 555 | transform: translateX(0); 556 | } 557 | 558 | @media screen and (min-width: 1600px) { 559 | font-size: 1.015rem; 560 | } 561 | 562 | @media screen and (max-width: 768px) { 563 | font-size: 0.925rem; 564 | } 565 | 566 | @media screen and (max-width: 480px) { 567 | font-size: 0.905rem; 568 | } 569 | } 570 | 571 | .btn__radius_left { 572 | border-radius: 100% 70% 70% 100%; 573 | } 574 | 575 | .btn__radius_right { 576 | border-radius: 70% 100% 100% 70%; 577 | } 578 | 579 | .small__btn { 580 | padding: 0.5rem 1rem; 581 | } 582 | 583 | @keyframes animate { 584 | 0% { 585 | transform: rotate(0deg); 586 | } 587 | 588 | 100% { 589 | transform: rotate(360deg); 590 | } 591 | } 592 | 593 | @keyframes drop-in { 594 | from { 595 | opacity: 0; 596 | transform: translateX(-100px); 597 | } 598 | 599 | to { 600 | opacity: 1; 601 | transform: translate(0px); 602 | } 603 | } 604 | 605 | @keyframes drop-out { 606 | from { 607 | opacity: 0; 608 | transform: translateX(100px); 609 | } 610 | 611 | to { 612 | opacity: 1; 613 | transform: translate(0px); 614 | } 615 | } 616 | 617 | @keyframes slide-in { 618 | from { 619 | opacity: 0; 620 | transform: translateY(-100px); 621 | } 622 | 623 | to { 624 | opacity: 1; 625 | transform: translate(0px); 626 | } 627 | } 628 | 629 | @keyframes slide-out { 630 | from { 631 | opacity: 0; 632 | transform: translateY(100px); 633 | } 634 | 635 | to { 636 | opacity: 1; 637 | transform: translate(0px); 638 | } 639 | } 640 | 641 | .drop_in { 642 | animation: drop-in 1s ease 200ms backwards; 643 | } 644 | 645 | .drop_out { 646 | animation: drop-out 1s ease 200ms backwards; 647 | } 648 | 649 | .slide_in { 650 | animation: slide-in 1s ease 200ms backwards; 651 | } 652 | 653 | .slide_out { 654 | animation: slide-out 1s ease 200ms backwards; 655 | } 656 | 657 | .constrained-width { 658 | width: 100%; 659 | } 660 | 661 | @media screen and (max-width: 1920px) { 662 | .constrained-width { 663 | max-width: 1440px; 664 | } 665 | } 666 | 667 | @media screen and (max-width: 1500px) { 668 | .constrained-width { 669 | max-width: 1200px; 670 | } 671 | } 672 | 673 | @media screen and (max-width: 1367px) { 674 | .constrained-width { 675 | max-width: 1080px; 676 | } 677 | } 678 | 679 | @media screen and (max-width: 900px) { 680 | .constrained-width { 681 | max-width: 100%; 682 | } 683 | } 684 | --------------------------------------------------------------------------------