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
42 changes: 28 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#020618" />
<meta name="description" content="Portfolio de Alison Silva (DevAlissu), engenheiro de software fullstack. Projetos em React, TypeScript, Python, FastAPI, Flutter e IoT." />
<meta name="author" content="Alison Silva" />

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DevAlissu | Portfolio</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

<meta property="og:type" content="website" />
<meta property="og:url" content="https://alissu.dev" />
<meta property="og:title" content="DevAlissu | Portfolio" />
<meta property="og:description" content="Portfolio interativo de um engenheiro de software fullstack com snake game, galeria de projetos e arquitetura feature-based." />
<meta property="og:site_name" content="DevAlissu" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="DevAlissu | Portfolio" />
<meta name="twitter:description" content="Portfolio interativo de um engenheiro de software fullstack." />

<link rel="canonical" href="https://alissu.dev" />

<title>DevAlissu | Portfolio</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Binary file added public/projects/aleam-ceap.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/aleam-ceap.png
Binary file not shown.
Binary file added public/projects/aleam-csv.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/aleam-csv.png
Binary file not shown.
Binary file added public/projects/aleam-gastos.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/aleam-gastos.png
Binary file not shown.
Binary file added public/projects/aleam-home.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/aleam-home.png
Binary file not shown.
Binary file added public/projects/aleam-vencimentos.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/aleam-vencimentos.png
Binary file not shown.
Binary file added public/projects/aleam-vlibras.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/aleam-vlibras.png
Binary file not shown.
Binary file added public/projects/biofogo-camadas.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/biofogo-camadas.png
Binary file not shown.
Binary file added public/projects/biofogo-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/biofogo-dark.png
Binary file not shown.
Binary file added public/projects/biofogo-mapa.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/biofogo-mapa.png
Binary file not shown.
Binary file added public/projects/biofogo-monitoramento.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/biofogo-monitoramento.png
Binary file not shown.
Binary file added public/projects/biofogo-previsao.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/biofogo-previsao.png
Binary file not shown.
Binary file added public/projects/biofogo-status.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/biofogo-status.png
Binary file not shown.
Binary file added public/projects/codebot-saas.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/projects/codebot-saas.png
Diff not rendered.
Binary file added public/projects/drawnomes-celebracao.jpg
Binary file removed public/projects/drawnomes-celebracao.png
Diff not rendered.
Binary file added public/projects/drawnomes-home.jpg
Binary file removed public/projects/drawnomes-home.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo1.jpg
Binary file removed public/projects/drawnomes-passo1.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo1b.jpg
Binary file removed public/projects/drawnomes-passo1b.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo2.jpg
Binary file removed public/projects/drawnomes-passo2.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo4.jpg
Binary file removed public/projects/drawnomes-passo4.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo5.jpg
Binary file removed public/projects/drawnomes-passo5.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo6.jpg
Binary file removed public/projects/drawnomes-passo6.png
Diff not rendered.
Binary file added public/projects/drawnomes-passo7.jpg
Binary file removed public/projects/drawnomes-passo7.png
Diff not rendered.
Binary file added public/projects/drawnomes-resultado.jpg
Binary file removed public/projects/drawnomes-resultado.png
Diff not rendered.
Binary file added public/projects/drawnomes-sorteio.jpg
Binary file removed public/projects/drawnomes-sorteio.png
Diff not rendered.
Binary file modified public/projects/emprestimo-1.jpg
Binary file modified public/projects/emprestimo-10.jpg
Binary file modified public/projects/emprestimo-11.jpg
Binary file modified public/projects/emprestimo-2.jpg
Binary file modified public/projects/emprestimo-3.jpg
Binary file modified public/projects/emprestimo-4.jpg
Binary file modified public/projects/emprestimo-5.jpg
Binary file modified public/projects/emprestimo-6.jpg
Binary file modified public/projects/emprestimo-7.jpg
Binary file modified public/projects/emprestimo-8.jpg
Binary file modified public/projects/emprestimo-9.jpg
Binary file added public/projects/horto-adote.jpg
Binary file removed public/projects/horto-adote.png
Diff not rendered.
Binary file added public/projects/horto-camera.jpg
Binary file removed public/projects/horto-camera.png
Diff not rendered.
Binary file added public/projects/horto-catalogo.jpg
Binary file removed public/projects/horto-catalogo.png
Diff not rendered.
Binary file added public/projects/horto-gerencie.jpg
Binary file removed public/projects/horto-gerencie.png
Diff not rendered.
Binary file added public/projects/horto-home.jpg
Binary file removed public/projects/horto-home.png
Diff not rendered.
Binary file added public/projects/horto-mudas.jpg
Binary file removed public/projects/horto-mudas.png
Diff not rendered.
Binary file added public/projects/horto-nome.jpg
Binary file removed public/projects/horto-nome.png
Diff not rendered.
Binary file added public/projects/horto-ola.jpg
Binary file removed public/projects/horto-ola.png
Diff not rendered.
Binary file added public/projects/horto-protinho.jpg
Binary file removed public/projects/horto-protinho.png
Diff not rendered.
Binary file modified public/projects/jungle-logic.jpg
Binary file added public/projects/melo-contas.jpg
Binary file removed public/projects/melo-contas.png
Diff not rendered.
Binary file added public/projects/melo-vendas.jpg
Binary file removed public/projects/melo-vendas.png
Diff not rendered.
Binary file added public/projects/ms-sefaz.jpg
Binary file removed public/projects/ms-sefaz.png
Diff not rendered.
Binary file added public/projects/nansen-dashboard.jpg
Binary file removed public/projects/nansen-dashboard.png
Diff not rendered.
Binary file added public/projects/nansen-faturamento.jpg
Binary file removed public/projects/nansen-faturamento.png
Diff not rendered.
Binary file added public/projects/nansen-home.jpg
Binary file removed public/projects/nansen-home.png
Diff not rendered.
Binary file added public/projects/nansen-login.jpg
Binary file removed public/projects/nansen-login.png
Diff not rendered.
Binary file added public/projects/nansen-missoes-lista.jpg
Binary file removed public/projects/nansen-missoes-lista.png
Diff not rendered.
Binary file added public/projects/nansen-missoes.jpg
Binary file removed public/projects/nansen-missoes.png
Diff not rendered.
Binary file added public/projects/sistema-melo.jpg
Binary file removed public/projects/sistema-melo.png
Diff not rendered.
Binary file added public/projects/sprint-tools-1.jpg
Binary file removed public/projects/sprint-tools-1.png
Diff not rendered.
Binary file added public/projects/yamaha-dashboard.jpg
Binary file removed public/projects/yamaha-dashboard.png
Diff not rendered.
12 changes: 7 additions & 5 deletions src/app/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { lazy } from 'react';
import { createBrowserRouter } from 'react-router';
import { Layout } from '../shared/components/layout';
import { HomePage } from '../features/home';
import { AboutPage } from '../features/about';
import { ProjectsPage } from '../features/projects';
import { ContactPage } from '../features/contact';
import { NotFoundPage } from '../features/not-found';

const HomePage = lazy(() => import('../features/home').then((m) => ({ default: m.HomePage })));
const AboutPage = lazy(() => import('../features/about').then((m) => ({ default: m.AboutPage })));
const ProjectsPage = lazy(() => import('../features/projects').then((m) => ({ default: m.ProjectsPage })));
const ContactPage = lazy(() => import('../features/contact').then((m) => ({ default: m.ContactPage })));
const NotFoundPage = lazy(() => import('../features/not-found').then((m) => ({ default: m.NotFoundPage })));

export const router = createBrowserRouter([
{
Expand Down
16 changes: 13 additions & 3 deletions src/features/about/components/TabItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@ interface TabItemProps {
export function TabItem({ tab, isActive, canClose, onSelect, onClose }: TabItemProps) {
return (
<div
className={`group relative flex items-center gap-2 px-4 py-3 border-r border-[#314158] cursor-pointer shrink-0 transition-colors ${
role="tab"
aria-selected={isActive}
tabIndex={0}
onClick={() => onSelect(tab)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect(tab);
}
}}
className={`group relative flex items-center gap-2 px-4 py-3 border-r border-[#314158] cursor-pointer shrink-0 transition-colors focus-visible:outline-2 focus-visible:outline-[#ffb86a] focus-visible:outline-offset-[-2px] ${
isActive ? 'text-[#f8fafc]' : 'text-[#90a1b9] hover:text-[#f8fafc]'
}`}
onClick={() => onSelect(tab)}
>
{isActive && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#ffb86a]" />
Expand All @@ -30,7 +39,8 @@ export function TabItem({ tab, isActive, canClose, onSelect, onClose }: TabItemP
e.stopPropagation();
onClose(tab);
}}
className="opacity-0 group-hover:opacity-100 hover:text-[#f8fafc] transition-opacity ml-1"
aria-label={`Fechar ${TAB_LABELS[tab]}`}
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:text-[#f8fafc] transition-opacity ml-1"
>
<X size={14} />
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/features/contact/components/ContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function ContactForm({ formData, formErrors, onChange, onSubmit }: Contac

<button
type="submit"
className="bg-[#ffd6a7] hover:bg-[#ffd6a7]/90 transition-colors px-4 py-2.5 rounded-lg font-['Fira_Code',sans-serif] text-[#020618] text-[14px]"
className="bg-[#ffd6a7] hover:bg-[#ffd6a7]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors px-4 py-2.5 rounded-lg font-['Fira_Code',sans-serif] text-[#020618] text-[14px] focus-visible:outline-2 focus-visible:outline-[#ffb86a] focus-visible:outline-offset-2"
>
enviar-mensagem
</button>
Expand Down
10 changes: 8 additions & 2 deletions src/features/contact/hooks/useContactForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ export function useContactForm() {

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (formStatus === 'success') return;

if (!EMAIL_REGEX.test(formData.email)) {
setFormErrors({ email: 'email invalido' });
const errors: ContactFormErrors = {};
if (!formData.name.trim()) errors.name = 'nome obrigatorio';
if (!EMAIL_REGEX.test(formData.email)) errors.email = 'email invalido';
if (!formData.message.trim()) errors.message = 'mensagem obrigatoria';

if (Object.keys(errors).length > 0) {
setFormErrors(errors);
setFormStatus('error');
return;
}
Expand Down
2 changes: 2 additions & 0 deletions src/features/contact/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export interface ContactFormData {
}

export interface ContactFormErrors {
name?: string;
email?: string;
message?: string;
}

export type ContactFormStatus = 'idle' | 'error' | 'success';
10 changes: 8 additions & 2 deletions src/features/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { SnakeGame } from '../snake-game';
import { lazy, Suspense } from 'react';

const SnakeGame = lazy(() =>
import('../snake-game').then((m) => ({ default: m.SnakeGame })),
);

export function HomePage() {
return (
Expand Down Expand Up @@ -41,7 +45,9 @@ export function HomePage() {
</div>

<div className="hidden lg:flex justify-end min-w-0">
<SnakeGame className="w-full" />
<Suspense fallback={<div className="w-full" />}>
<SnakeGame className="w-full" />
</Suspense>
</div>
</div>
</div>
Expand Down
31 changes: 17 additions & 14 deletions src/features/projects/components/ImageLightbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useRef } from 'react';
import { X, ChevronLeft, ChevronRight } from 'lucide-react';

interface ImageLightboxProps {
Expand All @@ -9,16 +9,12 @@ interface ImageLightboxProps {

export function ImageLightbox({ images, startIndex, onClose }: ImageLightboxProps) {
const [index, setIndex] = useState(startIndex);
const indexRef = useRef(index);
indexRef.current = index;

const prev = useCallback(() => {
setIndex((i) => (i > 0 ? i - 1 : images.length - 1));
}, [images.length]);

const next = useCallback(() => {
setIndex((i) => (i < images.length - 1 ? i + 1 : 0));
}, [images.length]);

const close = useCallback(() => onClose(index), [onClose, index]);
const prev = () => setIndex((i) => (i > 0 ? i - 1 : images.length - 1));
const next = () => setIndex((i) => (i < images.length - 1 ? i + 1 : 0));
const close = () => onClose(indexRef.current);

useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
Expand All @@ -29,16 +25,21 @@ export function ImageLightbox({ images, startIndex, onClose }: ImageLightboxProp
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [close, prev, next]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/90"
onClick={close}
role="dialog"
aria-modal="true"
aria-label="Visualizador de imagens"
>
<button
onClick={close}
className="absolute top-4 right-4 text-white/60 hover:text-white transition-colors z-10 p-2"
aria-label="Fechar visualizador"
className="absolute top-4 right-4 text-white/60 hover:text-white transition-colors z-10 p-2 focus-visible:outline-2 focus-visible:outline-white"
>
<X size={24} />
</button>
Expand All @@ -50,7 +51,8 @@ export function ImageLightbox({ images, startIndex, onClose }: ImageLightboxProp
{images.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); prev(); }}
className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 hover:text-white transition-colors z-10 p-2 rounded-full hover:bg-white/10"
aria-label="Imagem anterior"
className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 hover:text-white transition-colors z-10 p-2 rounded-full hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-white"
>
<ChevronLeft size={32} />
</button>
Expand All @@ -66,7 +68,8 @@ export function ImageLightbox({ images, startIndex, onClose }: ImageLightboxProp
{images.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); next(); }}
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/40 hover:text-white transition-colors z-10 p-2 rounded-full hover:bg-white/10"
aria-label="Proxima imagem"
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/40 hover:text-white transition-colors z-10 p-2 rounded-full hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-white"
>
<ChevronRight size={32} />
</button>
Expand Down
9 changes: 5 additions & 4 deletions src/features/projects/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { ExternalLink, Lock } from 'lucide-react';
import type { Project } from '../types';

Expand All @@ -7,7 +8,7 @@ interface ProjectCardProps {
onSelect: (project: Project) => void;
}

export function ProjectCard({ project, index, onSelect }: ProjectCardProps) {
export const ProjectCard = memo(function ProjectCard({ project, index, onSelect }: ProjectCardProps) {
return (
<div className="group transition-all duration-200">
<div className="mb-4">
Expand All @@ -20,7 +21,7 @@ export function ProjectCard({ project, index, onSelect }: ProjectCardProps) {
<div className="bg-[#1d293d] rounded-lg overflow-hidden border border-[#314158] group-hover:border-[#9d4edd]/50 transition-all">
{project.image ? (
<div className="relative h-[175px] bg-[#0a1628] overflow-hidden">
<img src={project.image} alt={project.title} className="w-full h-full object-cover object-top" />
<img src={project.image} alt={project.title} loading="lazy" width="400" height="175" className="w-full h-full object-cover object-top" />
</div>
) : (
<div className="h-[145px] bg-[#0a1628] flex items-center justify-center">
Expand Down Expand Up @@ -70,12 +71,12 @@ export function ProjectCard({ project, index, onSelect }: ProjectCardProps) {
</div>
<button
onClick={() => onSelect(project)}
className="bg-[#1d293d] border border-[#90a1b9] hover:border-[#f8fafc] transition-colors px-4 py-2 rounded-lg font-['Fira_Code',sans-serif] text-[#f8fafc] text-[13px] cursor-pointer"
className="bg-[#1d293d] border border-[#90a1b9] hover:border-[#f8fafc] transition-colors px-4 py-2 rounded-lg font-['Fira_Code',sans-serif] text-[#f8fafc] text-[13px] cursor-pointer focus-visible:outline-2 focus-visible:outline-[#ffb86a] focus-visible:outline-offset-2"
>
ver-detalhes
</button>
</div>
</div>
</div>
);
}
});
33 changes: 23 additions & 10 deletions src/features/projects/components/ProjectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
? [project.image]
: [];

// reset slide index when project changes to avoid out-of-bounds
useEffect(() => {
setActiveSlide(0);
}, [project.id]);

useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (lightboxOpen) return;
Expand Down Expand Up @@ -84,14 +89,15 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => setExpanded((p) => !p)}
className="hidden sm:flex text-[#90a1b9] hover:text-[#f8fafc] transition-colors p-1.5 rounded hover:bg-[#1d293d]"
title={expanded ? 'Reduzir' : 'Expandir'}
aria-label={expanded ? 'Reduzir modal' : 'Expandir modal'}
className="hidden sm:flex text-[#90a1b9] hover:text-[#f8fafc] transition-colors p-1.5 rounded hover:bg-[#1d293d] focus-visible:outline-2 focus-visible:outline-[#ffb86a]"
>
{expanded ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</button>
<button
onClick={onClose}
className="text-[#90a1b9] hover:text-[#f8fafc] transition-colors p-1.5 rounded hover:bg-[#1d293d]"
aria-label="Fechar modal"
className="text-[#90a1b9] hover:text-[#f8fafc] transition-colors p-1.5 rounded hover:bg-[#1d293d] focus-visible:outline-2 focus-visible:outline-[#ffb86a]"
>
<X size={18} />
</button>
Expand All @@ -104,29 +110,35 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
{allImages.length > 0 ? (
<div className="space-y-3">
{/* Active slide */}
<div
className="rounded-md overflow-hidden bg-[#1d293d] cursor-pointer group/img relative"
<button
type="button"
aria-label="Ampliar imagem"
className="rounded-md overflow-hidden bg-[#1d293d] group/img relative w-full focus-visible:outline-2 focus-visible:outline-[#ffb86a]"
onClick={() => setLightboxOpen(true)}
>
<img
src={allImages[activeSlide]}
alt={`${project.title} ${activeSlide + 1}`}
loading="lazy"
className={`w-full h-auto ${imgMaxH} object-contain group-hover/img:brightness-110 transition-all`}
/>
{allImages.length > 1 && (
<div className="absolute bottom-2 right-2 font-['Fira_Code',sans-serif] text-[10px] text-white/70 bg-black/50 px-2 py-0.5 rounded">
{activeSlide + 1} / {allImages.length}
</div>
)}
</div>
</button>

{/* Thumbnails */}
{allImages.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-1">
{allImages.map((img, i) => (
<div
<button
key={i}
className={`shrink-0 rounded overflow-hidden cursor-pointer transition-all w-16 h-12 sm:w-20 sm:h-14 ${
type="button"
aria-label={`Ir para imagem ${i + 1}`}
aria-current={i === activeSlide}
className={`shrink-0 rounded overflow-hidden transition-all w-16 h-12 sm:w-20 sm:h-14 focus-visible:outline-2 focus-visible:outline-[#ffb86a] ${
i === activeSlide
? 'ring-2 ring-[#9d4edd] opacity-100'
: 'opacity-50 hover:opacity-80'
Expand All @@ -135,10 +147,11 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
>
<img
src={img}
alt={`thumb ${i + 1}`}
alt=""
loading="lazy"
className="w-full h-full object-cover object-top"
/>
</div>
</button>
))}
</div>
)}
Expand Down
Loading
Loading