feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint
migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
This commit is contained in:
parent
6ef5a7a17e
commit
cf5603243c
106 changed files with 11927 additions and 1367 deletions
203
specs/032-performance-homepage/tasks.md
Normal file
203
specs/032-performance-homepage/tasks.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# Tasks — 032: Performance Homepage
|
||||
|
||||
> Ordem de execução respeita dependências. Cada task é atômica e pode ser validada individualmente.
|
||||
|
||||
---
|
||||
|
||||
## FASE 1 — Fundação (sem quebra de interface)
|
||||
|
||||
### TASK-01: Criar hook `useInView`
|
||||
- **Arquivo:** `frontend/src/hooks/useInView.ts` (criar)
|
||||
- **Ação:**
|
||||
```ts
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export function useInView(options?: IntersectionObserverInit) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [inView, setInView] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setInView(true)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, options)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return { ref, inView }
|
||||
}
|
||||
```
|
||||
- **Validação:** `get_errors` sem erros
|
||||
|
||||
---
|
||||
|
||||
### TASK-02: Mover `@keyframes fadeDown` para `index.css`
|
||||
- **Arquivo:** `frontend/src/index.css` (editar — adicionar ao final)
|
||||
- **Conteúdo a adicionar:**
|
||||
```css
|
||||
@keyframes fadeDown {
|
||||
0%, 100% { opacity: 0; transform: translateY(-4px); }
|
||||
50% { opacity: 1; transform: translateY(4px); }
|
||||
}
|
||||
```
|
||||
- **Arquivo:** `frontend/src/components/HomeScrollScene.tsx` (editar — remover o bloco `<style>` inline)
|
||||
- **Validação:** Build passa, setas do scroll hint continuam animadas
|
||||
|
||||
---
|
||||
|
||||
### TASK-03: `loading="lazy"` + `decoding="async"` em `SlideImage`
|
||||
- **Arquivo:** `frontend/src/components/PropertyRowCard.tsx`
|
||||
- **Alvo:** função `SlideImage`, tag `<img>`
|
||||
- **Adicionar atributos:** `loading="lazy"` e `decoding="async"`
|
||||
- **Cuidado:** Não adicionar `loading="lazy"` na imagem hero de `HomeScrollScene` (ela deve ser eager)
|
||||
- **Validação:** `get_errors` sem erros
|
||||
|
||||
---
|
||||
|
||||
### TASK-04: `will-change: transform` no track do `AgentsCarousel`
|
||||
- **Arquivo:** `frontend/src/components/AgentsCarousel.tsx`
|
||||
- **Alvo:** elemento `div` com `ref={trackRef}` que recebe `transform` no estilo inline
|
||||
- **Ação:** Adicionar `willChange: 'transform'` no objeto de style do track
|
||||
- **Validação:** `get_errors` sem erros
|
||||
|
||||
---
|
||||
|
||||
## FASE 2 — Refatoração de `AgentsCarousel` para receber props
|
||||
|
||||
### TASK-05: Adicionar props de dados em `AgentsCarousel`
|
||||
- **Arquivo:** `frontend/src/components/AgentsCarousel.tsx`
|
||||
- **Ação:**
|
||||
1. Definir `interface AgentsCarouselProps { agents: Agent[]; loading: boolean }`
|
||||
2. Receber `{ agents, loading }` como props do componente
|
||||
3. Remover `useEffect` que chama `getAgents()` e os estados `agents` / `loading`
|
||||
4. Remover import de `getAgents`
|
||||
5. Manter toda a lógica de carrossel (autoplay, prev/next, etc.) inalterada
|
||||
- **Validação:** `get_errors` sem erros (vai reportar erro de prop em `HomePage.tsx` — resolver na TASK-08)
|
||||
|
||||
---
|
||||
|
||||
## FASE 3 — Refatoração de `HomeScrollScene` para receber props
|
||||
|
||||
### TASK-06: Adicionar props de dados em `HomeScrollScene`
|
||||
- **Arquivo:** `frontend/src/components/HomeScrollScene.tsx`
|
||||
- **Ação:**
|
||||
1. Adicionar ao `HomeScrollSceneProps`: `properties: Property[]` e `loadingProperties: boolean`
|
||||
2. Remover `const [properties, setProperties] = useState<Property[]>([])`
|
||||
3. Remover `const [loading, setLoading] = useState(true)`
|
||||
4. Remover `useEffect(() => { getFeaturedProperties()... })`
|
||||
5. Remover import de `getFeaturedProperties`
|
||||
6. No JSX, substituir uso de `loading` por `loadingProperties`
|
||||
- **Validação:** `get_errors` (vai ter erro em uso — resolver na TASK-08)
|
||||
|
||||
---
|
||||
|
||||
### TASK-07: Refatorar `RiseCard` para usar `useInView`
|
||||
- **Arquivo:** `frontend/src/components/HomeScrollScene.tsx`
|
||||
- **Ação:**
|
||||
1. Importar `useInView` de `'../hooks/useInView'`
|
||||
2. Substituir o corpo de `RiseCard`: remover `useRef`, `useState(false)`, `useEffect` com `IntersectionObserver`
|
||||
3. Usar `const { ref, inView } = useInView({ threshold: 0.05 })`
|
||||
4. Manter a div com `ref={ref}` e classes condicionais em `inView`
|
||||
- **Validação:** `get_errors` sem erros
|
||||
|
||||
---
|
||||
|
||||
## FASE 4 — `HomePage` como orchestrator
|
||||
|
||||
### TASK-08: Paralelizar fetches + cache + preload em `HomePage`
|
||||
- **Arquivo:** `frontend/src/pages/HomePage.tsx`
|
||||
- **Ação:**
|
||||
1. Adicionar imports: `getFeaturedProperties` de `'../services/properties'`, `getAgents` de `'../services/agents'`, `Agent` de `'../types/agent'`, `Property` de `'../types/property'`
|
||||
2. Adicionar estados: `featuredProperties: Property[]`, `agents: Agent[]`, `loadingProperties: boolean`, `loadingAgents: boolean`
|
||||
3. Criar helpers de cache:
|
||||
```ts
|
||||
const CFG_CACHE_KEY = 'homepage_config_v1'
|
||||
const CFG_CACHE_TTL = 5 * 60 * 1000
|
||||
|
||||
function getCachedConfig(): HomepageConfig | null { ... }
|
||||
function setCachedConfig(data: HomepageConfig): void { ... }
|
||||
```
|
||||
4. Substituir `useEffect` de `getHomepageConfig` por `Promise.all`:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
const cached = getCachedConfig()
|
||||
const configFetch = cached
|
||||
? Promise.resolve(cached)
|
||||
: getHomepageConfig().then(d => { setCachedConfig(d); return d })
|
||||
|
||||
Promise.all([configFetch, getFeaturedProperties(), getAgents()])
|
||||
.then(([cfg, props, agts]) => {
|
||||
setConfig(cfg)
|
||||
setFeaturedProperties(props)
|
||||
setAgents(agts)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setLoadingProperties(false)
|
||||
setLoadingAgents(false)
|
||||
})
|
||||
}, [])
|
||||
```
|
||||
5. Adicionar `useEffect` de preload:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (!themedBackgroundImage) return
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'preload'
|
||||
link.as = 'image'
|
||||
link.href = themedBackgroundImage
|
||||
document.head.appendChild(link)
|
||||
return () => { document.head.removeChild(link) }
|
||||
}, [themedBackgroundImage])
|
||||
```
|
||||
6. Passar `properties={featuredProperties}` e `loadingProperties={loadingProperties}` para `<HomeScrollScene>`
|
||||
7. Passar `agents={agents}` e `loading={loadingAgents}` para `<AgentsCarousel>`
|
||||
- **Validação:** `get_errors` sem erros; `npm run build` passa
|
||||
|
||||
---
|
||||
|
||||
### TASK-09: `fetchPriority="high"` + `loading="eager"` na hero image
|
||||
- **Arquivo:** `frontend/src/components/HomeScrollScene.tsx`
|
||||
- **Alvo:** tag `<img>` do `backgroundImage` (dentro do bloco `{backgroundImage ? (`)
|
||||
- **Adicionar:** `fetchPriority="high"` e `loading="eager"` e `decoding="async"`
|
||||
- **Nota:** `fetchPriority` é atributo HTML5 — TypeScript pode reclamar; usar `{...{ fetchpriority: 'high' } as React.ImgHTMLAttributes<HTMLImageElement>}` se necessário, ou verificar suporte em `@types/react`
|
||||
- **Validação:** `get_errors` sem erros
|
||||
|
||||
---
|
||||
|
||||
## FASE 5 — Validação Final
|
||||
|
||||
### TASK-10: Build e checklist final
|
||||
- **Ação:**
|
||||
1. Executar `npm run build` no diretório `frontend/`
|
||||
2. Verificar zero erros TypeScript
|
||||
3. Testar manualmente:
|
||||
- [ ] Home carrega no tema dark sem erro visual
|
||||
- [ ] Home carrega no tema light sem erro visual
|
||||
- [ ] Cards de destaque aparecem com animação rise
|
||||
- [ ] Carousel de corretores funciona (autoplay, prev/next)
|
||||
- [ ] Troca de tema reage corretamente no gradiente hero
|
||||
- [ ] DevTools Network: 3 requests paralelos no load inicial
|
||||
- [ ] Segunda visita (< 5 min): `/homepage-config` não é chamado
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Qualidade
|
||||
|
||||
- [X] TASK-01 concluída — `hooks/useInView.ts` criado
|
||||
- [X] TASK-02 concluída — `@keyframes` em `index.css`, sem `<style>` inline
|
||||
- [X] TASK-03 concluída — `loading="lazy"` em `SlideImage`
|
||||
- [X] TASK-04 concluída — `will-change: transform` no carrossel
|
||||
- [X] TASK-05 concluída — `AgentsCarousel` recebe props
|
||||
- [X] TASK-06 concluída — `HomeScrollScene` recebe props
|
||||
- [X] TASK-07 concluída — `RiseCard` usa `useInView`
|
||||
- [X] TASK-08 concluída — `HomePage` paraleliza fetches + cache + preload
|
||||
- [X] TASK-09 concluída — hero image com prioridade correta
|
||||
- [X] TASK-10 concluída — build verde, smoke test manual OK
|
||||
Loading…
Add table
Add a link
Reference in a new issue