Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
<link rel="canonical" href="https://alissu.dev" />

<title>DevAlissu | Portfolio</title>
<script>
(function () {
if (location.hostname !== 'alissu.dev') return;
var s = document.createElement('script');
s.defer = true;
s.src = 'https://analytics.alissu.dev/script.js';
s.setAttribute('data-website-id', '698a3b1b-c749-4463-8ccb-e76f729dedf4');
document.head.appendChild(s);
})();
</script>
</head>

<body>
Expand Down
39 changes: 39 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
32 changes: 15 additions & 17 deletions src/features/contact/components/ContactSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -26,12 +23,12 @@ export function ContactSidebar() {

{expandedContacts && (
<div className="px-3 pb-3 space-y-2">
<div className="flex items-center gap-2 px-3 py-2">
<svg className="w-4 h-4" viewBox="0 0 16 16"><path d={EMAIL_ICON} fill="#62748E" /></svg>
<div className="flex items-center gap-2 px-3 py-2 text-[#62748E]">
<Mail size={16} strokeWidth={1.8} />
<span className="font-['Fira_Code',sans-serif] text-[#90a1b9] text-[16px]">{CONTACT_EMAIL}</span>
</div>
<div className="flex items-center gap-2 px-3 py-2">
<svg className="w-4 h-4" viewBox="0 0 16 16"><path d={PHONE_ICON} fill="#62748E" /></svg>
<div className="flex items-center gap-2 px-3 py-2 text-[#62748E]">
<Phone size={16} strokeWidth={1.8} />
<span className="font-['Fira_Code',sans-serif] text-[#90a1b9] text-[16px]">{CONTACT_PHONE}</span>
</div>
</div>
Expand All @@ -53,25 +50,26 @@ export function ContactSidebar() {

{expandedSocial && (
<div className="px-3 pb-3 space-y-2">
{CONTACT_SOCIALS.map((social) =>
social.active ? (
{CONTACT_SOCIALS.map((social) => {
const Icon = social.icon;
return social.active ? (
<a
key={social.name}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 hover:text-[#f8fafc] transition-colors"
className="flex items-center gap-2 px-3 py-2 text-[#f8fafc] hover:text-[#43D9AD] transition-colors"
>
<svg className="w-4 h-4" viewBox="0 0 12 12" fill="none"><path d={SOCIAL_ICON} fill="#F8FAFC" /></svg>
<span className="font-['Fira_Code',sans-serif] text-[#f8fafc] text-[16px]">{social.name}</span>
<Icon width={16} height={16} />
<span className="font-['Fira_Code',sans-serif] text-[16px]">{social.name}</span>
</a>
) : (
<div key={social.name} className="flex items-center gap-2 px-3 py-2">
<svg className="w-4 h-4" viewBox="0 0 12 12" fill="none"><path d={SOCIAL_ICON} fill="#62748E" /></svg>
<div key={social.name} className="flex items-center gap-2 px-3 py-2 text-[#62748E]">
<Icon width={16} height={16} />
<span className="font-['Fira_Code',sans-serif] text-[#90a1b9] text-[16px]">{social.name}</span>
</div>
),
)}
);
})}
</div>
)}
</div>
Expand Down
13 changes: 0 additions & 13 deletions src/features/contact/constants/index.ts

This file was deleted.

36 changes: 36 additions & 0 deletions src/features/contact/constants/index.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGProps<SVGSVGElement>>;

function WhatsApp(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
<path d="M8.5 9.5c0.2 1.5 1 3 2 4s2.5 1.8 4 2c0.5 0.1 1-0.1 1.3-0.5l0.5-0.7c0.2-0.3 0.1-0.6-0.2-0.8l-1.4-0.8c-0.3-0.2-0.6-0.1-0.8 0.1l-0.4 0.4c-0.8-0.3-1.5-0.8-2.1-1.4-0.6-0.6-1.1-1.3-1.4-2.1l0.4-0.4c0.2-0.2 0.3-0.5 0.1-0.8l-0.8-1.4c-0.2-0.3-0.5-0.4-0.8-0.2l-0.7 0.5c-0.4 0.3-0.6 0.8-0.5 1.3z" />
</svg>
);
}

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 },
];
2 changes: 2 additions & 0 deletions src/features/contact/hooks/useContactForm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { ContactFormData, ContactFormErrors, ContactFormStatus } from '../types';
import { trackEvent } from '../../../shared/services/analytics';

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

Expand Down Expand Up @@ -42,6 +43,7 @@ export function useContactForm() {
return;
}

trackEvent('contact-submit');
setFormStatus('success');
setFormErrors({});
};
Expand Down
29 changes: 25 additions & 4 deletions src/features/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-full relative">
Expand All @@ -16,10 +30,17 @@ export function HomePage() {
</p>
<h1 className="space-y-2">
<div className="text-5xl sm:text-6xl text-[#f8fafc] font-normal">Alissu</div>
<div className="text-2xl sm:text-3xl text-[#b14eff] font-normal">
<span className="font-['Fira_Code',sans-serif]">{`> Front-end developer `}</span>
<span className="font-['Buenard',serif]">{`&&`}</span>
<span className="font-['Fira_Code',sans-serif]">{` Full-Stack developer`}</span>
<div className="text-2xl sm:text-3xl text-[#b14eff] font-normal font-['Fira_Code',sans-serif] min-h-[1.5em]">
<span aria-hidden="true">&gt; </span>
<TypeAnimation
sequence={ROLES}
wrapper="span"
speed={55}
deletionSpeed={70}
cursor
repeat={Infinity}
aria-label="Front-end developer, Back-end developer, Full-Stack developer, Mobile developer, IoT developer"
/>
</div>
</h1>
</div>
Expand Down
10 changes: 8 additions & 2 deletions src/features/projects/ProjectsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import { useProjectFilter } from './hooks/useProjectFilter';
import { TechFilter } from './components/TechFilter';
import { ProjectCard } from './components/ProjectCard';
import { ProjectModal } from './components/ProjectModal';
import type { Project } from './types';
import { trackEvent } from '../../shared/services/analytics';

export function ProjectsPage() {
const { selectedTech, toggleTech, filteredProjects } = useProjectFilter();
const [selectedProject, setSelectedProject] = useState<Project | null>(null);

const handleSelect = useCallback((project: Project) => {
trackEvent('project-open', { id: project.id, title: project.title });
setSelectedProject(project);
}, []);

return (
<div className="h-full flex flex-col lg:flex-row min-w-0">
<TechFilter selectedTech={selectedTech} onToggle={toggleTech} />
Expand All @@ -27,7 +33,7 @@ export function ProjectsPage() {
key={project.id}
project={project}
index={i}
onSelect={setSelectedProject}
onSelect={handleSelect}
/>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/features/snake-game/SnakeGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function SnakeGame({ className = '' }: SnakeGameProps) {
{mode === 'competitive' && highScore > 0 && <HighScoreBadge highScore={highScore} />}
</div>

<div className="relative" {...swipeHandlers}>
<div className="relative pl-2 sm:pl-4" {...swipeHandlers}>
<GameCanvas
food={food}
gridSize={gridSize}
Expand Down
9 changes: 7 additions & 2 deletions src/features/snake-game/store/useGameStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PARTICLE_LIFETIME_MS,
MAX_DIRECTION_QUEUE,
} from '../constants';
import { trackEvent } from '../../../shared/services/analytics';

function pickFoodType(): FoodType {
const totalWeight = Object.values(FOOD_TYPES).reduce((s, t) => s + t.weight, 0);
Expand Down Expand Up @@ -134,6 +135,8 @@ export const useGameStore = create<GameStore>((set, get) => ({
},

beginCountdown: () => {
const { difficulty, mode } = get();
trackEvent('snake-start', { difficulty, mode });
set({
status: 'countdown',
score: 0,
Expand Down Expand Up @@ -166,9 +169,10 @@ export const useGameStore = create<GameStore>((set, get) => ({
},

gameOver: () => {
const { score, highScore, shakeKey } = get();
const { score, highScore, shakeKey, difficulty, mode } = get();
const newHighScore = Math.max(score, highScore);
if (newHighScore > highScore) saveHighScore(newHighScore);
trackEvent('snake-game-over', { score, difficulty, mode });
set({ status: 'game-over', highScore: newHighScore, combo: 0, shakeKey: shakeKey + 1 });
},

Expand Down Expand Up @@ -228,7 +232,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
},

moveSnake: () => {
const { snake, direction, directionQueue, food, status, mode, combo, lastEatTime, particles, shakeKey } = get();
const { snake, direction, directionQueue, food, status, mode, difficulty, combo, lastEatTime, particles, shakeKey } = get();
if (status !== 'playing') return;

let currentDirection = direction;
Expand Down Expand Up @@ -293,6 +297,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
if (!newFood || (mode === 'casual' && newScore >= FOOD_TO_WIN_CASUAL)) {
const newHighScore = Math.max(newScore, highScore);
if (newHighScore > highScore) saveHighScore(newHighScore);
trackEvent('snake-victory', { score: newScore, difficulty });
set({
...dirUpdate,
status: 'victory',
Expand Down
Loading
Loading