├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── App.tsx ├── assets │ └── logos │ │ ├── dd.svg │ │ └── ddvital.svg ├── components │ ├── LanguageSwitcher │ │ └── index.tsx │ ├── Terminal │ │ └── index.tsx │ ├── ThemeSwitcher │ │ └── index.tsx │ ├── layout │ │ └── NavBar │ │ │ ├── DesktopMenu.tsx │ │ │ ├── MobileMenu.tsx │ │ │ ├── index.tsx │ │ │ └── menuItems.ts │ └── ui │ │ ├── Button │ │ ├── Index.tsx │ │ └── button.scss │ │ ├── Icon │ │ ├── animated │ │ │ ├── Menu.tsx │ │ │ ├── Terminal.tsx │ │ │ └── index.ts │ │ ├── index.tsx │ │ └── paths.ts │ │ └── ScrollTop │ │ └── index.tsx ├── hooks │ └── useTheme.ts ├── i18n │ ├── index.ts │ └── locales │ │ ├── en.json │ │ └── pt.json ├── main.tsx ├── store │ └── terminal.ts ├── styles │ ├── animations.scss │ ├── global.scss │ ├── theme.scss │ └── variables.scss └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | David Vital. 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddvital", 3 | "private": true, 4 | "version": "3.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "i18next": "^24.2.0", 14 | "i18next-browser-languagedetector": "^8.0.2", 15 | "react": "^18.3.1", 16 | "react-console-emulator": "^5.0.2", 17 | "react-dom": "^18.3.1", 18 | "react-draggable": "^4.4.6", 19 | "react-i18next": "^15.2.0", 20 | "react-resizable": "^3.0.5", 21 | "zustand": "^5.0.2" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.17.0", 25 | "@types/react": "^18.3.17", 26 | "@types/react-dom": "^18.3.5", 27 | "@vitejs/plugin-react": "^4.3.4", 28 | "autoprefixer": "^10.4.20", 29 | "eslint": "^9.17.0", 30 | "eslint-plugin-react-hooks": "^5.0.0", 31 | "eslint-plugin-react-refresh": "^0.4.16", 32 | "globals": "^15.13.0", 33 | "postcss": "^8.4.49", 34 | "sass": "^1.83.0", 35 | "tailwindcss": "^3.4.17", 36 | "typescript": "~5.6.2", 37 | "typescript-eslint": "^8.18.1", 38 | "vite": "^6.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import LanguageSwitcher from './components/LanguageSwitcher' 3 | import { useTranslation } from 'react-i18next'; 4 | import { ThemeSwitcher } from './components/ThemeSwitcher'; 5 | import { Console } from './components/Terminal'; 6 | import { useTerminalStore } from './store/terminal'; 7 | 8 | function App() { 9 | const toggleTerminal = useTerminalStore((state) => state.toggleTerminal) 10 | 11 | const { t, i18n } = useTranslation(); 12 | 13 | const currentLanguage = i18n.language; 14 | 15 | return ( 16 |
17 |
18 | 19 |
  • {t('header.home')} || {currentLanguage}
  • 20 | 21 |

    Welcome to my portfolio

    22 |

    23 | This text will change color based on the theme 24 |

    25 |
    26 | 29 | 30 |
    31 | ) 32 | } 33 | 34 | export default App 35 | -------------------------------------------------------------------------------- /src/assets/logos/dd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/logos/ddvital.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/LanguageSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | 3 | const LanguageSwitcher = () => { 4 | const { t, i18n } = useTranslation(); 5 | const currentLanguage = i18n.language; 6 | 7 | const languages = [ 8 | { code: 'en', label: 'EN' }, 9 | { code: 'pt', label: 'PT' }, 10 | ]; 11 | 12 | return ( 13 |
    14 | {languages.map(({ code, label }) => ( 15 | 36 | ))} 37 |
    38 | ); 39 | }; 40 | 41 | export default LanguageSwitcher; 42 | -------------------------------------------------------------------------------- /src/components/Terminal/index.tsx: -------------------------------------------------------------------------------- 1 | import Terminal from 'react-console-emulator' 2 | import Draggable from 'react-draggable' 3 | import { useState, useRef } from 'react' 4 | import { useTerminalStore } from '../../store/terminal' 5 | 6 | export function Console() { 7 | const { isVisible, isMinimized, setMinimized } = useTerminalStore() 8 | const [isMaximized, setIsMaximized] = useState(false) 9 | const [position, setPosition] = useState({ x: 50, y: 50 }) 10 | const [isDragging, setIsDragging] = useState(false) 11 | const dragBounds = useRef(null) 12 | 13 | const handleMaximize = () => { 14 | if (!isMaximized) { 15 | // Store current position before maximizing 16 | setPosition({ x: dragBounds.current?.getBoundingClientRect().left || 50, y: dragBounds.current?.getBoundingClientRect().top || 50 }) 17 | } 18 | setIsMaximized(prev => !prev) 19 | } 20 | 21 | const handleClose = () => { 22 | const terminal = dragBounds.current 23 | if (terminal) { 24 | terminal.style.transform = 'scale(0.95) translateY(10px)' 25 | terminal.style.opacity = '0' 26 | } 27 | } 28 | 29 | const handleMinimize = () => { 30 | const terminal = dragBounds.current 31 | if (terminal) { 32 | terminal.style.transform = 'scale(0.8) translateY(100%)' 33 | terminal.style.opacity = '0' 34 | setTimeout(() => setMinimized(true), 150) 35 | } 36 | } 37 | 38 | const commands = { 39 | exit: { 40 | description: 'Close the terminal', 41 | usage: 'exit', 42 | fn: () => { 43 | return 'Terminal closed' 44 | } 45 | } 46 | } 47 | 48 | if (!isVisible) return null 49 | 50 | return ( 51 |
    61 | setIsDragging(true)} 67 | onStop={(_, data) => { 68 | setIsDragging(false) 69 | if (!isMaximized) { 70 | setPosition({ x: data.x, y: data.y }) 71 | } 72 | }} 73 | > 74 |
    88 | {/* Terminal Header */} 89 |
    90 |
    91 |
    104 | 105 | Terminal 106 | 107 |
    108 | 109 | {/* Terminal Content */} 110 | {!isMinimized && ( 111 | 122 | )} 123 | 124 |
    125 |
    126 |
    127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '../ui/Icon' 2 | import { useTheme } from '../../hooks/useTheme' 3 | 4 | export function ThemeSwitcher() { 5 | const { theme, setTheme } = useTheme() 6 | 7 | const toggleTheme = () => { 8 | const newTheme = theme === 'dark' ? 'light' : 'dark' 9 | setTheme(newTheme) 10 | } 11 | 12 | return ( 13 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/layout/NavBar/DesktopMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { menuItems } from "./menuItems" 3 | 4 | export function DesktopMenu({ activeSection }: { activeSection: string }) { 5 | 6 | const { t, i18n } = useTranslation(); 7 | 8 | return ( 9 |
    10 |
    11 | {menuItems.map((item) => ( 12 | 23 | {t(item.name)} 24 | 25 | ))} 26 |
    27 |
    28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/layout/NavBar/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import { menuItems } from "./menuItems" 3 | import { useTranslation } from "react-i18next" 4 | 5 | interface MobileMenuProps { 6 | isOpen: boolean 7 | setIsOpen: (value: boolean) => void 8 | activeSection: string 9 | } 10 | 11 | export function MobileMenu({ isOpen, setIsOpen, activeSection }: MobileMenuProps) { 12 | 13 | const { t } = useTranslation(); 14 | 15 | useEffect(() => { 16 | if (isOpen) { 17 | document.body.style.overflow = 'hidden' 18 | } else { 19 | document.body.style.overflow = 'unset' 20 | } 21 | }, [isOpen]) 22 | 23 | return ( 24 | <> 25 |
    42 |
    43 | {menuItems.map((item, index) => ( 44 | setIsOpen(false)} 64 | > 65 | {t(item.name)} 66 | 67 | ))} 68 |
    69 |
    70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/layout/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { DesktopMenu } from './DesktopMenu' 3 | import { MobileMenu } from './MobileMenu' 4 | import { Icon } from '../../ui/Icon' 5 | import { Hamburger } from '../../ui/Icon/animated' 6 | import { Button } from '../../ui/Button/Index' 7 | import { ThemeSwitcher } from '../../ThemeSwitcher' 8 | import { Logo } from '../../Logo' 9 | import { useTerminalStore } from '../../../store/terminal' 10 | import { menuItems } from './menuItems' 11 | 12 | export function Navbar() { 13 | const [isOpen, setIsOpen] = useState(false) 14 | const [isScrolled, setIsScrolled] = useState(false) 15 | const [isVisible, setIsVisible] = useState(true) 16 | const [prevScrollPos, setPrevScrollPos] = useState(0) 17 | const [activeSection, setActiveSection] = useState('') 18 | 19 | const toggleTerminal = useTerminalStore((state) => state.toggleTerminal) 20 | 21 | useEffect(() => { 22 | const handleScroll = () => { 23 | const currentScrollPos = window.scrollY 24 | setIsVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10) 25 | setIsScrolled(currentScrollPos > 20) 26 | setPrevScrollPos(currentScrollPos) 27 | 28 | const sections = menuItems.map(item => document.getElementById(item.sectionId)) 29 | const scrollPosition = window.scrollY + 100 30 | 31 | sections.forEach((section) => { 32 | if (!section) return 33 | 34 | if ( 35 | section.offsetTop <= scrollPosition && 36 | section.offsetTop + section.offsetHeight > scrollPosition 37 | ) { 38 | setActiveSection(section.id) 39 | console.log(section.id); 40 | 41 | } 42 | }) 43 | } 44 | 45 | window.addEventListener('scroll', handleScroll) 46 | return () => window.removeEventListener('scroll', handleScroll) 47 | }, [prevScrollPos]) 48 | 49 | return ( 50 | <> 51 | 97 | 98 | 99 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/layout/NavBar/menuItems.ts: -------------------------------------------------------------------------------- 1 | export const menuItems = [ 2 | { name: 'navbar.home', href: '#hero', sectionId: 'hero' }, 3 | { name: 'navbar.about', href: '#about', sectionId: 'about' }, 4 | { name: 'navbar.projects', href: '#projects', sectionId: 'projects' }, 5 | { name: 'navbar.skills', href: '#skills', sectionId: 'skills' }, 6 | { name: 'navbar.contact', href: '#contact', sectionId: 'contact' }, 7 | ] 8 | -------------------------------------------------------------------------------- /src/components/ui/Button/Index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react' 2 | import { Icon } from '../Icon' 3 | import './button.scss' 4 | 5 | interface ButtonProps extends ButtonHTMLAttributes { 6 | variant?: 'primary' | 'secondary' | 'ghost' 7 | size?: 'sm' | 'md' | 'lg' 8 | icon?: string 9 | iconPosition?: 'left' | 'right' 10 | animated?: boolean 11 | loading?: boolean 12 | children: ReactNode 13 | } 14 | 15 | export function Button({ 16 | variant = 'primary', 17 | size = 'md', 18 | icon, 19 | iconPosition = 'left', 20 | animated = false, 21 | loading = false, 22 | children, 23 | className, 24 | disabled, 25 | ...props 26 | }: ButtonProps) { 27 | const variants = { 28 | primary: 'bg-accent-primary hover:bg-accent-secondary text-white', 29 | secondary: 'bg-background-secondary hover:bg-gray-200 dark:hover:bg-gray-700', 30 | ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800', 31 | icon: 'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800', 32 | } 33 | 34 | const sizes = { 35 | xs: 'text-xs', 36 | sm: 'px-3 py-1.5 text-sm', 37 | md: 'px-4 py-2', 38 | lg: 'px-6 py-3 text-lg' 39 | } 40 | 41 | return ( 42 | 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /src/components/ui/Button/button.scss: -------------------------------------------------------------------------------- 1 | .btn-ripple { 2 | position: relative; 3 | overflow: hidden; 4 | 5 | &::after { 6 | content: ''; 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | top: 0; 11 | left: 0; 12 | pointer-events: none; 13 | background-image: radial-gradient(circle, #fff 10%, transparent 10.01%); 14 | background-repeat: no-repeat; 15 | background-position: 50%; 16 | transform: scale(10, 10); 17 | opacity: 0; 18 | transition: transform .5s, opacity 1s; 19 | } 20 | 21 | &:active::after { 22 | transform: scale(0, 0); 23 | opacity: .3; 24 | transition: 0s; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/Icon/animated/Menu.tsx: -------------------------------------------------------------------------------- 1 | interface HamburgerProps { 2 | isOpen: boolean 3 | } 4 | 5 | export function Hamburger({ isOpen }: HamburgerProps) { 6 | return ( 7 |
    8 | 16 | 24 |
    25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/Icon/animated/Terminal.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | 3 | export function Terminal(props: SVGProps) { 4 | return ( 5 | 12 | {/* Terminal Window */} 13 | 21 | 22 | {/* Command Prompt */} 23 | 29 | 35 | 36 | 37 | {/* Blinking Cursor */} 38 | 42 | 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/ui/Icon/animated/index.ts: -------------------------------------------------------------------------------- 1 | export { Terminal } from './Terminal' 2 | export { Hamburger } from './Menu' -------------------------------------------------------------------------------- /src/components/ui/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | import * as AnimatedIcons from './animated' 3 | import { iconPaths } from './paths' 4 | 5 | type StaticIconName = keyof typeof iconPaths 6 | type AnimatedIconName = 'Terminal' 7 | 8 | interface IconProps extends SVGProps { 9 | name: StaticIconName | AnimatedIconName 10 | size?: number 11 | animated?: boolean 12 | } 13 | 14 | export function Icon({ name, size = 24, animated = false, ...props }: IconProps) { 15 | // For animated icons 16 | if (animated) { 17 | const AnimatedIcon = AnimatedIcons[name as AnimatedIconName] 18 | if (!AnimatedIcon) { 19 | console.warn(`Animated icon "${name}" not found`) 20 | return null 21 | } 22 | return 23 | } 24 | 25 | // For static icons 26 | const path = iconPaths[name as StaticIconName] 27 | if (!path) { 28 | console.warn(`Static icon "${name}" not found`) 29 | return null 30 | } 31 | 32 | return ( 33 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/components/ui/Icon/paths.ts: -------------------------------------------------------------------------------- 1 | export const iconPaths = { 2 | ArrowUp: "M12 19V5m0 0l-7 7m7-7l7 7", 3 | arrowRight: "M5 12h14M12 5l7 7-7 7", 4 | sun: "M12 1.25C12.1989 1.25 12.3897 1.32902 12.5303 1.46967C12.671 1.61032 12.75 1.80109 12.75 2V3C12.75 3.19891 12.671 3.38968 12.5303 3.53033C12.3897 3.67098 12.1989 3.75 12 3.75C11.8011 3.75 11.6103 3.67098 11.4697 3.53033C11.329 3.38968 11.25 3.19891 11.25 3V2C11.25 1.80109 11.329 1.61032 11.4697 1.46967C11.6103 1.32902 11.8011 1.25 12 1.25ZM4.399 4.399C4.53963 4.25855 4.73025 4.17966 4.929 4.17966C5.12775 4.17966 5.31837 4.25855 5.459 4.399L5.852 4.791C5.98869 4.93239 6.06437 5.1218 6.06276 5.31845C6.06114 5.5151 5.98235 5.70325 5.84336 5.84237C5.70437 5.98149 5.5163 6.06046 5.31965 6.06226C5.123 6.06406 4.93352 5.98855 4.792 5.852L4.399 5.459C4.25855 5.31837 4.17966 5.12775 4.17966 4.929C4.17966 4.73025 4.25855 4.53963 4.399 4.399ZM19.601 4.399C19.7414 4.53963 19.8203 4.73025 19.8203 4.929C19.8203 5.12775 19.7414 5.31837 19.601 5.459L19.208 5.852C19.0658 5.98448 18.8778 6.0566 18.6835 6.05317C18.4892 6.04975 18.3038 5.97104 18.1664 5.83362C18.029 5.69621 17.9503 5.51082 17.9468 5.31652C17.9434 5.12222 18.0155 4.93417 18.148 4.792L18.541 4.399C18.6816 4.25855 18.8722 4.17966 19.071 4.17966C19.2698 4.17966 19.4604 4.25855 19.601 4.399ZM12 6.75C10.6076 6.75 9.27225 7.30312 8.28769 8.28769C7.30312 9.27225 6.75 10.6076 6.75 12C6.75 13.3924 7.30312 14.7277 8.28769 15.7123C9.27225 16.6969 10.6076 17.25 12 17.25C13.3924 17.25 14.7277 16.6969 15.7123 15.7123C16.6969 14.7277 17.25 13.3924 17.25 12C17.25 10.6076 16.6969 9.27225 15.7123 8.28769C14.7277 7.30312 13.3924 6.75 12 6.75ZM5.25 12C5.25 10.2098 5.96116 8.4929 7.22703 7.22703C8.4929 5.96116 10.2098 5.25 12 5.25C13.7902 5.25 15.5071 5.96116 16.773 7.22703C18.0388 8.4929 18.75 10.2098 18.75 12C18.75 13.7902 18.0388 15.5071 16.773 16.773C15.5071 18.0388 13.7902 18.75 12 18.75C10.2098 18.75 8.4929 18.0388 7.22703 16.773C5.96116 15.5071 5.25 13.7902 5.25 12ZM1.25 12C1.25 11.8011 1.32902 11.6103 1.46967 11.4697C1.61032 11.329 1.80109 11.25 2 11.25H3C3.19891 11.25 3.38968 11.329 3.53033 11.4697C3.67098 11.6103 3.75 11.8011 3.75 12C3.75 12.1989 3.67098 12.3897 3.53033 12.5303C3.38968 12.671 3.19891 12.75 3 12.75H2C1.80109 12.75 1.61032 12.671 1.46967 12.5303C1.32902 12.3897 1.25 12.1989 1.25 12ZM20.25 12C20.25 11.8011 20.329 11.6103 20.4697 11.4697C20.6103 11.329 20.8011 11.25 21 11.25H22C22.1989 11.25 22.3897 11.329 22.5303 11.4697C22.671 11.6103 22.75 11.8011 22.75 12C22.75 12.1989 22.671 12.3897 22.5303 12.5303C22.3897 12.671 22.1989 12.75 22 12.75H21C20.8011 12.75 20.6103 12.671 20.4697 12.5303C20.329 12.3897 20.25 12.1989 20.25 12ZM18.148 18.148C18.2886 18.0076 18.4792 17.9287 18.678 17.9287C18.8768 17.9287 19.0674 18.0076 19.208 18.148L19.601 18.541C19.6747 18.6097 19.7338 18.6925 19.7748 18.7845C19.8158 18.8765 19.8378 18.9758 19.8396 19.0765C19.8414 19.1772 19.8228 19.2772 19.7851 19.3706C19.7474 19.464 19.6913 19.5488 19.62 19.62C19.5488 19.6913 19.464 19.7474 19.3706 19.7851C19.2772 19.8228 19.1772 19.8414 19.0765 19.8396C18.9758 19.8378 18.8765 19.8158 18.7845 19.7748C18.6925 19.7338 18.6097 19.6747 18.541 19.601L18.148 19.208C18.0076 19.0674 17.9287 18.8768 17.9287 18.678C17.9287 18.4792 18.0076 18.2886 18.148 18.148ZM5.852 18.148C5.99245 18.2886 6.07134 18.4792 6.07134 18.678C6.07134 18.8768 5.99245 19.0674 5.852 19.208L5.459 19.601C5.39034 19.6747 5.30754 19.7338 5.21554 19.7748C5.12354 19.8158 5.02423 19.8378 4.92352 19.8396C4.82282 19.8414 4.72279 19.8228 4.6294 19.7851C4.53601 19.7474 4.45118 19.6913 4.37996 19.62C4.30874 19.5488 4.2526 19.464 4.21488 19.3706C4.17716 19.2772 4.15863 19.1772 4.16041 19.0765C4.16219 18.9758 4.18423 18.8765 4.22522 18.7845C4.26621 18.6925 4.32531 18.6097 4.399 18.541L4.791 18.148C4.86065 18.0783 4.94335 18.023 5.03438 17.9853C5.1254 17.9476 5.22297 17.9282 5.3215 17.9282C5.42003 17.9282 5.5176 17.9476 5.60862 17.9853C5.69965 18.023 5.78235 18.0783 5.852 18.148ZM12 20.25C12.1989 20.25 12.3897 20.329 12.5303 20.4697C12.671 20.6103 12.75 20.8011 12.75 21V22C12.75 22.1989 12.671 22.3897 12.5303 22.5303C12.3897 22.671 12.1989 22.75 12 22.75C11.8011 22.75 11.6103 22.671 11.4697 22.5303C11.329 22.3897 11.25 22.1989 11.25 22V21C11.25 20.8011 11.329 20.6103 11.4697 20.4697C11.6103 20.329 11.8011 20.25 12 20.25Z", 5 | moon: "M13.2548 2.83121C13.1195 2.56305 12.9059 2.34101 12.6415 2.19374C12.3772 2.04646 12.0742 1.98069 11.7716 2.00491C6.30072 2.44131 2 6.96875 2 12.4875C2 18.2911 6.77182 23 12.653 23C15.8292 23 18.671 21.6293 20.6162 19.4608C20.8178 19.237 20.9469 18.9586 20.9868 18.6617C21.0267 18.3648 20.9756 18.0629 20.8401 17.7949C20.7045 17.5269 20.4907 17.3051 20.2263 17.158C19.9619 17.011 19.6589 16.9455 19.3564 16.9699C19.1517 16.9859 18.944 16.9944 18.7333 16.9954C14.5389 16.9954 11.1348 13.6362 11.1348 9.48216C11.1348 7.56261 11.8567 5.82302 13.0497 4.50332C13.2509 4.27952 13.3798 4.00141 13.4196 3.70476C13.4594 3.40812 13.4084 3.10651 13.273 2.83871L13.2548 2.83121ZM19.4703 18.4276C18.9613 18.9909 18.3846 19.491 17.7531 19.9167C16.3807 20.8321 14.7815 21.3613 13.1274 21.4474C11.4732 21.5336 9.82646 21.1734 8.36408 20.4056C6.9017 19.6378 5.67889 18.4914 4.827 17.0895C3.97511 15.6875 3.52631 14.0831 3.52881 12.4485C3.52816 10.5751 4.11909 8.74806 5.21943 7.22145C6.31977 5.69485 7.87482 4.54453 9.66835 3.93046C10.3905 3.68477 11.141 3.52959 11.9023 3.46857C10.4193 5.1179 9.60162 7.24718 9.60452 9.45217C9.60488 10.6963 9.86627 11.9269 10.3722 13.0665C10.8782 14.2061 11.6178 15.23 12.5444 16.0737C13.471 16.9173 14.5646 17.5626 15.7563 17.9687C16.9481 18.3749 18.2121 18.5331 19.4688 18.4336L19.4703 18.4276Z", 6 | monitor: "M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-3M8 21h8M12 3v18" 7 | } as const 8 | 9 | export type IconName = keyof typeof iconPaths 10 | -------------------------------------------------------------------------------- /src/components/ui/ScrollTop/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Icon } from '../Icon' 3 | 4 | export function ScrollToTop() { 5 | const [isVisible, setIsVisible] = useState(false) 6 | const [isLeaving, setIsLeaving] = useState(false) 7 | 8 | useEffect(() => { 9 | const toggleVisibility = () => { 10 | setIsVisible(window.scrollY > 500) 11 | } 12 | 13 | window.addEventListener('scroll', toggleVisibility) 14 | return () => window.removeEventListener('scroll', toggleVisibility) 15 | }, []) 16 | 17 | const scrollToTop = () => { 18 | setIsLeaving(true) 19 | window.scrollTo({ 20 | top: 0, 21 | behavior: 'smooth' 22 | }) 23 | setTimeout(() => { 24 | setIsLeaving(false) 25 | setIsVisible(false) 26 | }, 500) 27 | } 28 | 29 | return ( 30 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | type Theme = 'light' | 'dark' | 'system' 4 | 5 | export function useTheme() { 6 | const [theme, setTheme] = useState(() => { 7 | if (typeof window !== 'undefined') { 8 | return (localStorage.getItem('theme') as Theme) || 'system' 9 | } 10 | return 'system' 11 | }) 12 | 13 | const handleThemeChange = (newTheme: Theme) => { 14 | const root = window.document.documentElement 15 | const isDark = newTheme === 'dark' || 16 | (newTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) 17 | 18 | // Add transition class before changing theme 19 | // root.classList.add('transition-colors', 'duration-300') 20 | root.classList.remove('light', 'dark') 21 | root.classList.add(isDark ? 'dark' : 'light') 22 | root.setAttribute('data-theme', isDark ? 'dark' : 'light') 23 | 24 | setTheme(newTheme) 25 | if (newTheme === 'system') { 26 | localStorage.removeItem('theme') 27 | } else { 28 | localStorage.setItem('theme', newTheme) 29 | } 30 | 31 | // Remove transition class after animation 32 | setTimeout(() => { 33 | // root.classList.remove('transition-colors', 'duration-300') 34 | }, 300) 35 | } 36 | 37 | useEffect(() => { 38 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 39 | 40 | const handleSystemThemeChange = () => { 41 | if (theme === 'system') { 42 | handleThemeChange('system') 43 | } 44 | } 45 | 46 | mediaQuery.addEventListener('change', handleSystemThemeChange) 47 | handleThemeChange(theme) 48 | 49 | return () => { 50 | mediaQuery.removeEventListener('change', handleSystemThemeChange) 51 | } 52 | }, [theme]) 53 | 54 | return { theme, setTheme: handleThemeChange } 55 | } 56 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | 5 | import enTranslation from './locales/en.json'; 6 | import ptTranslation from './locales/pt.json'; 7 | 8 | i18n 9 | .use(LanguageDetector) 10 | .use(initReactI18next) 11 | .init({ 12 | resources: { 13 | en: { 14 | translation: enTranslation, 15 | }, 16 | pt: { 17 | translation: ptTranslation, 18 | }, 19 | }, 20 | fallbackLng: 'en', 21 | detection: { 22 | order: ['localStorage', 'navigator'], 23 | lookupLocalStorage: 'i18nextLng', 24 | caches: ['localStorage'], 25 | }, 26 | interpolation: { 27 | escapeValue: false, 28 | }, 29 | }); 30 | 31 | export default i18n; 32 | -------------------------------------------------------------------------------- /src/i18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "navbar": { 3 | "home": "Home", 4 | "about": "About me", 5 | "projects": "Projects", 6 | "skills": "Skills", 7 | "contact": "Contact me" 8 | }, 9 | "hero": { 10 | "intro": "Introduction", 11 | "greeting": "Hi, I'm David Vital, ", 12 | "solution": "Modern soluctions with Timeless elegance", 13 | "creating": "Creating", 14 | "developing": "Developing", 15 | "planning": "Planning", 16 | "designing": "Designing" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/i18n/locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "navbar": { 3 | "home": "Home", 4 | "about": "Sobre mim", 5 | "projects": "Projetos", 6 | "skills": "Skills", 7 | "contact": "Contato" 8 | }, 9 | "hero": { 10 | "intro": "Introdução", 11 | "greeting": "Olá, meu nome é David Vital, ", 12 | "creating": "Criando", 13 | "solution": "Soluções modernas com elegância", 14 | "developing": "Desenvolvendo", 15 | "planning": "Planejando", 16 | "designing": "Designing" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './i18n' 5 | import './styles/global.scss' 6 | import './styles/theme.scss' 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /src/store/terminal.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | interface TerminalStore { 4 | isVisible: boolean 5 | isMinimized: boolean 6 | toggleTerminal: () => void 7 | setMinimized: (value: boolean) => void 8 | } 9 | 10 | export const useTerminalStore = create((set) => ({ 11 | isVisible: false, 12 | isMinimized: false, 13 | toggleTerminal: () => set((state) => ({ 14 | isVisible: !state.isVisible, 15 | isMinimized: false 16 | })), 17 | setMinimized: (value) => set({ isMinimized: value }) 18 | })) 19 | -------------------------------------------------------------------------------- /src/styles/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes leaveTop { 2 | 0% { 3 | transform: translateY(0); 4 | } 5 | 100% { 6 | transform: translateY(-100vh); 7 | } 8 | } 9 | 10 | .leave-top { 11 | animation: leaveTop 1.5s ease-in forwards; 12 | } 13 | 14 | .animated-text-container { 15 | overflow: hidden; 16 | position: relative; 17 | } 18 | 19 | .animated-text { 20 | display: block; 21 | font-size: 2.5rem; 22 | font-weight: 600; 23 | background: linear-gradient(90deg, var(--highlight-color), #000); 24 | -webkit-background-clip: text; 25 | -webkit-text-fill-color: transparent; 26 | position: absolute; 27 | width: 100%; 28 | } 29 | 30 | .slide-in { 31 | animation: slideIn 0.5s ease forwards; 32 | } 33 | 34 | .slide-out { 35 | animation: slideOut 0.5s ease forwards; 36 | } 37 | 38 | @keyframes slideIn { 39 | from { 40 | transform: translateY(100%); 41 | opacity: 0; 42 | } 43 | to { 44 | transform: translateY(0); 45 | opacity: 1; 46 | } 47 | } 48 | 49 | @keyframes slideOut { 50 | from { 51 | transform: translateY(0); 52 | opacity: 1; 53 | } 54 | to { 55 | transform: translateY(-100%); 56 | opacity: 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import './animations.scss'; 6 | 7 | * { 8 | box-sizing: border-box; 9 | padding: 0; 10 | margin: 0; 11 | } 12 | 13 | :root { 14 | @apply bg-white text-black; 15 | } 16 | 17 | .dark { 18 | background-color: theme('colors.background.primary'); 19 | color: theme('colors.text.primary'); 20 | } 21 | 22 | body { 23 | @apply transition-colors duration-300; 24 | } 25 | 26 | .solution span { 27 | font-style: italic; 28 | } -------------------------------------------------------------------------------- /src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | :root { 4 | @each $mode, $tokens in $tokens { 5 | @if $mode == 'light' { 6 | @each $category, $values in $tokens { 7 | @each $property, $value in $values { 8 | @if type-of($value) == 'map' { 9 | @each $subprop, $subval in $value { 10 | --#{$category}-#{$property}-#{$subprop}: #{$subval}; 11 | } 12 | } @else { 13 | --#{$category}-#{$property}: #{$value}; 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | .dark { 22 | @each $category, $values in map-get($tokens, 'dark') { 23 | @each $property, $value in $values { 24 | @if type-of($value) == 'map' { 25 | @each $subprop, $subval in $value { 26 | --#{$category}-#{$property}-#{$subprop}: #{$subval}; 27 | } 28 | } @else { 29 | --#{$category}-#{$property}: #{$value}; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // Design Tokens 2 | $tokens: ( 3 | light: ( 4 | colors: ( 5 | background: ( 6 | primary: #ffffff, 7 | secondary: #f3f4f6, 8 | tertiary: #f9fafb, 9 | hover: #e2e3e3, 10 | ), 11 | text: ( 12 | primary: #111827, 13 | secondary: #4b5563, 14 | muted: #6b7280 15 | ), 16 | accent: ( 17 | primary: #2563eb, 18 | secondary: #3b82f6, 19 | terminal: #19B0EC, 20 | hover: #1d4ed8 21 | ), 22 | gradient: ( 23 | primary: linear-gradient(135deg, #2563eb, #3b82f6), 24 | secondary: linear-gradient(135deg, #3b82f6, #60a5fa) 25 | ), 26 | border: #e5e7eb, 27 | success: #10b981, 28 | error: #ef4444, 29 | warning: #f59e0b 30 | ), 31 | fonts: ( 32 | family: ( 33 | sans: ('Inter', sans-serif), 34 | mono: ('Fira Code', monospace) 35 | ), 36 | size: ( 37 | xs: 0.75rem, 38 | sm: 0.875rem, 39 | base: 1rem, 40 | lg: 1.125rem, 41 | xl: 1.25rem, 42 | '2xl': 1.5rem 43 | ), 44 | weight: ( 45 | normal: 400, 46 | medium: 500, 47 | semibold: 600, 48 | bold: 700 49 | ) 50 | ), 51 | spacing: ( 52 | xs: 0.5rem, 53 | sm: 0.75rem, 54 | base: 1rem, 55 | lg: 1.5rem, 56 | xl: 2rem, 57 | '2xl': 3rem 58 | ), 59 | radius: ( 60 | sm: 0.25rem, 61 | base: 0.375rem, 62 | md: 0.5rem, 63 | lg: 0.75rem, 64 | full: 9999px 65 | ), 66 | shadows: ( 67 | sm: '0 1px 2px rgba(0, 0, 0, 0.05)', 68 | base: '0 1px 3px rgba(0, 0, 0, 0.1)', 69 | md: '0 4px 6px rgba(0, 0, 0, 0.1)', 70 | lg: '0 10px 15px rgba(0, 0, 0, 0.1)' 71 | ) 72 | ), 73 | dark: ( 74 | colors: ( 75 | background: ( 76 | primary: #111112, 77 | secondary: #191919, 78 | tertiary: #282828, 79 | hover: #273043 80 | ), 81 | text: ( 82 | primary: #E5E6ED, 83 | secondary: #737582, 84 | muted: #9ca3af 85 | ), 86 | accent: ( 87 | primary: #2867F0, 88 | secondary: #0048E8, 89 | hover: #0048E8 90 | ), 91 | gradient: ( 92 | primary: linear-gradient(135deg, #2867F0, #19B0EC), 93 | secondary: linear-gradient(135deg, #3b82f6, #60a5fa) 94 | ), 95 | border: #374151 96 | ) 97 | ) 98 | ); -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | archivo: ['Archivo', 'sans-serif'], 12 | poppins: ['Poppins', 'sans-serif'], 13 | }, 14 | colors: { 15 | background: { 16 | primary: 'var(--colors-background-primary)', 17 | secondary: 'var(--colors-background-secondary)', 18 | tertiary: 'var(--colors-background-tertiary)', 19 | hover: 'var(--colors-background-hover)', 20 | }, 21 | text: { 22 | primary: 'var(--colors-text-primary)', 23 | secondary: 'var(--colors-text-secondary)', 24 | muted: 'var(--colors-text-muted)', 25 | }, 26 | accent: { 27 | primary: 'var(--colors-accent-primary)', 28 | secondary: 'var(--colors-accent-secondary)', 29 | terminal: 'var(--colors-accent-terminal)', 30 | hover: 'var(--colors-accent-hover)', 31 | }, 32 | }, 33 | backgroundImage: { 34 | 'gradient-primary': 'linear-gradient(135deg, #2867F0, #19B0EC)', 35 | }, 36 | keyframes: { 37 | bounce: { 38 | '0%, 100%': { transform: 'translateY(0)' }, 39 | '50%': { transform: 'translateY(-20px)' } 40 | } 41 | }, 42 | animation: { 43 | bounce: 'bounce 1.5s ease-in-out infinite' 44 | } 45 | }, 46 | }, 47 | plugins: [], 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------