From 3e7919815fe757e924824349513e242ec5d8534e Mon Sep 17 00:00:00 2001 From: DevAlissu Date: Tue, 21 Apr 2026 23:31:05 -0400 Subject: [PATCH 1/5] adiciona typewriter nos papeis do hero - instala react-type-animation - subtitle alterna entre frontend backend fullstack mobile iot com cursor piscante - reserva altura fixa no container para evitar layout shift --- package-lock.json | 39 ++++++++++++++++++++++++++++++++++ package.json | 1 + src/features/home/HomePage.tsx | 29 +++++++++++++++++++++---- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb88151..a27dfef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-router": "7.13.0", + "react-type-animation": "^3.2.0", "tailwind-merge": "3.2.0", "zustand": "^5.0.12" }, @@ -2478,6 +2479,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2528,6 +2538,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2561,6 +2583,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2593,6 +2621,17 @@ } } }, + "node_modules/react-type-animation": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-type-animation/-/react-type-animation-3.2.0.tgz", + "integrity": "sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==", + "license": "MIT", + "peerDependencies": { + "prop-types": "^15.5.4", + "react": ">= 15.0.0", + "react-dom": ">= 15.0.0" + } + }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", diff --git a/package.json b/package.json index 58c31de..2783162 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-router": "7.13.0", + "react-type-animation": "^3.2.0", "tailwind-merge": "3.2.0", "zustand": "^5.0.12" }, diff --git a/src/features/home/HomePage.tsx b/src/features/home/HomePage.tsx index dd544ba..1d2c6ee 100644 --- a/src/features/home/HomePage.tsx +++ b/src/features/home/HomePage.tsx @@ -1,9 +1,23 @@ import { lazy, Suspense } from 'react'; +import { TypeAnimation } from 'react-type-animation'; const SnakeGame = lazy(() => import('../snake-game').then((m) => ({ default: m.SnakeGame })), ); +const ROLES = [ + 'Front-end developer', + 2000, + 'Back-end developer', + 2000, + 'Full-Stack developer', + 2000, + 'Mobile developer', + 2000, + 'IoT developer', + 2000, +]; + export function HomePage() { return (
@@ -16,10 +30,17 @@ export function HomePage() {

Alissu
-
- {`> Front-end developer `} - {`&&`} - {` Full-Stack developer`} +
+ +

From 08fb770bc01a1adca65fa90609a6cb6b8cc79d1f Mon Sep 17 00:00:00 2001 From: DevAlissu Date: Tue, 21 Apr 2026 23:31:08 -0400 Subject: [PATCH 2/5] usa lucide react para icones de contato e social - constants vira tsx com ComponentType para icon por social - Mail e Phone lucide substituem svgs custom - Instagram lucide para social - WhatsApp svg stroke inline combinando com estilo lucide --- .../contact/components/ContactSidebar.tsx | 32 ++++++++--------- src/features/contact/constants/index.ts | 13 ------- src/features/contact/constants/index.tsx | 36 +++++++++++++++++++ 3 files changed, 51 insertions(+), 30 deletions(-) delete mode 100644 src/features/contact/constants/index.ts create mode 100644 src/features/contact/constants/index.tsx diff --git a/src/features/contact/components/ContactSidebar.tsx b/src/features/contact/components/ContactSidebar.tsx index 537041a..e200572 100644 --- a/src/features/contact/components/ContactSidebar.tsx +++ b/src/features/contact/components/ContactSidebar.tsx @@ -1,10 +1,7 @@ import { useState } from 'react'; +import { Mail, Phone } from 'lucide-react'; import { CONTACT_EMAIL, CONTACT_PHONE, CONTACT_SOCIALS } from '../constants'; -const EMAIL_ICON = 'M13.3333 2.66667H2.66667C1.93333 2.66667 1.33333 3.26667 1.33333 4V12C1.33333 12.7333 1.93333 13.3333 2.66667 13.3333H13.3333C14.0667 13.3333 14.6667 12.7333 14.6667 12V4C14.6667 3.26667 14.0667 2.66667 13.3333 2.66667ZM13.3333 12H2.66667V5.33333L8 8.66667L13.3333 5.33333V12ZM8 7.33333L2.66667 4H13.3333L8 7.33333Z'; -const PHONE_ICON = 'M12.4 14C10.88 14 9.36 13.54 7.96 12.62C6.56 11.7 5.3 10.44 4.38 9.04C3.46 7.64 3 6.12 3 4.6C3 4.24 3.12 3.94 3.36 3.7C3.6 3.46 3.9 3.34 4.26 3.34H6.42C6.7 3.34 6.94 3.42 7.14 3.58C7.34 3.74 7.48 3.94 7.56 4.18L8.16 6.16C8.22 6.4 8.22 6.62 8.16 6.82C8.1 7.02 7.98 7.2 7.8 7.36L6.36 8.82C6.8 9.6 7.36 10.3 8.04 10.92C8.72 11.54 9.46 12.08 10.26 12.54L11.66 11.14C11.84 10.96 12.06 10.84 12.32 10.78C12.58 10.72 12.82 10.74 13.04 10.84L14.92 11.52C15.16 11.62 15.36 11.78 15.52 12C15.68 12.22 15.76 12.46 15.76 12.72V14.74C15.76 15.1 15.64 15.4 15.4 15.64C15.16 15.88 14.86 16 14.5 16C14.14 16 13.78 15.98 13.42 15.94C13.06 15.9 12.72 15.84 12.4 15.76V14Z'; -const SOCIAL_ICON = 'M11.3333 3.33333C11.3333 3.33333 11.2 2.4 10.8 2C10.2667 1.46667 9.66667 1.46667 9.4 1.43333C7.86667 1.33333 6 1.33333 6 1.33333C6 1.33333 4.13333 1.33333 2.6 1.43333C2.33333 1.46667 1.73333 1.46667 1.2 2C0.8 2.4 0.666667 3.33333 0.666667 3.33333C0.666667 3.33333 0.533333 4.4 0.533333 5.46667V6.53333C0.533333 7.6 0.666667 8.66667 0.666667 8.66667C0.666667 8.66667 0.8 9.6 1.2 10C1.73333 10.5333 2.4 10.5333 2.66667 10.6C3.73333 10.6667 6 10.6667 6 10.6667C6 10.6667 7.86667 10.6667 9.4 10.5667C9.66667 10.5333 10.2667 10.5333 10.8 10C11.2 9.6 11.3333 8.66667 11.3333 8.66667C11.3333 8.66667 11.4667 7.6 11.4667 6.53333V5.46667C11.4667 4.4 11.3333 3.33333 11.3333 3.33333ZM4.8 7.6V4.13333L7.73333 5.86667L4.8 7.6Z'; - export function ContactSidebar() { const [expandedContacts, setExpandedContacts] = useState(false); const [expandedSocial, setExpandedSocial] = useState(false); @@ -26,12 +23,12 @@ export function ContactSidebar() { {expandedContacts && (
-
- +
+ {CONTACT_EMAIL}
-
- +
+ {CONTACT_PHONE}
@@ -53,25 +50,26 @@ export function ContactSidebar() { {expandedSocial && (
- {CONTACT_SOCIALS.map((social) => - social.active ? ( + {CONTACT_SOCIALS.map((social) => { + const Icon = social.icon; + return social.active ? ( - - {social.name} + + {social.name} ) : ( -
- +
+ {social.name}
- ), - )} + ); + })}
)}
diff --git a/src/features/contact/constants/index.ts b/src/features/contact/constants/index.ts deleted file mode 100644 index 6f2227c..0000000 --- a/src/features/contact/constants/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const CONTACT_EMAIL = 'alissu.dev@gmail.com'; -export const CONTACT_PHONE = '+55 14 9 9970-46645'; - -export interface SocialLink { - name: string; - url: string; - active: boolean; -} - -export const CONTACT_SOCIALS: SocialLink[] = [ - { name: 'Instagram', url: 'https://instagram.com/alissu_sz_', active: true }, - { name: 'WhatsApp', url: 'https://wa.me/5514999704645', active: true }, -]; diff --git a/src/features/contact/constants/index.tsx b/src/features/contact/constants/index.tsx new file mode 100644 index 0000000..463767f --- /dev/null +++ b/src/features/contact/constants/index.tsx @@ -0,0 +1,36 @@ +import type { ComponentType, SVGProps } from 'react'; +import { Instagram } from 'lucide-react'; + +export const CONTACT_EMAIL = 'alissu.dev@gmail.com'; +export const CONTACT_PHONE = '+55 14 9 9970-46645'; + +type IconComponent = ComponentType>; + +function WhatsApp(props: SVGProps) { + return ( + + + + + ); +} + +export interface SocialLink { + name: string; + url: string; + active: boolean; + icon: IconComponent; +} + +export const CONTACT_SOCIALS: SocialLink[] = [ + { name: 'Instagram', url: 'https://instagram.com/alissu_sz_', active: true, icon: Instagram }, + { name: 'WhatsApp', url: 'https://wa.me/5514999704645', active: true, icon: WhatsApp }, +]; From 3e9b0cdfdfe09b695183efca4bce33a6fc64f828 Mon Sep 17 00:00:00 2001 From: DevAlissu Date: Tue, 21 Apr 2026 23:31:18 -0400 Subject: [PATCH 3/5] adiciona CodeRain no background do layout - canvas2d com 0 e 1 caindo em movimento continuo 60fps - hand rolled sem dependencias - cyan 46ECD5 em opacity 7 percent - respeita prefers reduced motion - visivel somente em lg desktop --- src/shared/components/decorative/CodeRain.tsx | 120 ++++++++++++++++++ src/shared/components/layout/Layout.tsx | 5 + 2 files changed, 125 insertions(+) create mode 100644 src/shared/components/decorative/CodeRain.tsx diff --git a/src/shared/components/decorative/CodeRain.tsx b/src/shared/components/decorative/CodeRain.tsx new file mode 100644 index 0000000..4af06f5 --- /dev/null +++ b/src/shared/components/decorative/CodeRain.tsx @@ -0,0 +1,120 @@ +import { useEffect, useRef } from 'react'; + +const CHARS = ['0', '1']; +const FONT_SIZE = 14; +const COLUMN_WIDTH = 20; +const TRAIL_LENGTH = 6; +const COLOR_RGB = '70, 236, 213'; +const SPEED_PX_PER_SEC = 220; + +interface Column { + headY: number; // continuous pixel position + lastSlot: number; + chars: string[]; +} + +interface CodeRainProps { + className?: string; +} + +export function CodeRain({ className = '' }: CodeRainProps) { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reducedMotion) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let rafId = 0; + let lastFrame = 0; + let cols: Column[] = []; + let columnCount = 0; + let dpr = 1; + + const randChar = () => CHARS[Math.floor(Math.random() * CHARS.length)]!; + + const resize = () => { + dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.floor(rect.width * dpr); + canvas.height = Math.floor(rect.height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + columnCount = Math.floor(rect.width / COLUMN_WIDTH); + cols = Array.from({ length: columnCount }, () => { + const startY = Math.random() * -rect.height; + return { + headY: startY, + lastSlot: Math.floor(startY / FONT_SIZE), + chars: Array.from({ length: TRAIL_LENGTH }, randChar), + }; + }); + ctx.font = `${FONT_SIZE}px 'Fira Code', monospace`; + ctx.textBaseline = 'top'; + }; + + const animate = (now: number) => { + const rect = canvas.getBoundingClientRect(); + const dt = lastFrame === 0 ? 0 : (now - lastFrame) / 1000; + lastFrame = now; + + ctx.clearRect(0, 0, rect.width, rect.height); + + for (let i = 0; i < columnCount; i++) { + const col = cols[i]!; + col.headY += SPEED_PX_PER_SEC * dt; + + // advance char stack when crossing a slot boundary + const currentSlot = Math.floor(col.headY / FONT_SIZE); + const slotsCrossed = currentSlot - col.lastSlot; + if (slotsCrossed > 0) { + for (let s = 0; s < slotsCrossed; s++) { + col.chars.pop(); + col.chars.unshift(randChar()); + } + col.lastSlot = currentSlot; + } + + const x = i * COLUMN_WIDTH; + for (let t = 0; t < TRAIL_LENGTH; t++) { + const y = col.headY - t * FONT_SIZE; + if (y < -FONT_SIZE || y > rect.height) continue; + const alpha = ((TRAIL_LENGTH - t) / TRAIL_LENGTH) * 0.9; + ctx.fillStyle = `rgba(${COLOR_RGB}, ${alpha})`; + ctx.fillText(col.chars[t]!, x, y); + } + + // respawn above the viewport once the trail has fully passed + if (col.headY - TRAIL_LENGTH * FONT_SIZE > rect.height && Math.random() > 0.985) { + col.headY = -FONT_SIZE * TRAIL_LENGTH; + col.lastSlot = Math.floor(col.headY / FONT_SIZE); + } + } + + rafId = requestAnimationFrame(animate); + }; + + resize(); + rafId = requestAnimationFrame(animate); + + const ro = new ResizeObserver(resize); + ro.observe(canvas); + + return () => { + cancelAnimationFrame(rafId); + ro.disconnect(); + }; + }, []); + + return ( +