remove node_modules

main
GuiSantos1367890 1 month ago
commit 5828b2a46a

@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1
# ── Stage 1: build (Node) ───────────────────────────────────────────────────
# Compila o site Vite. Corre `tsc -b` (gate de TypeScript) + `vite build`.
FROM node:22-alpine AS build
WORKDIR /app
# Instala dependências primeiro (camada cacheável).
COPY package.json package-lock.json* ./
RUN npm ci || npm install
# Copia o resto do código e gera o build estático.
COPY . .
RUN npm run build
# ── Stage 2: runtime (nginx) ────────────────────────────────────────────────
# Imagem final isolada: serve `dist/` estático. Sem Node nem código-fonte.
FROM nginx:1.27-alpine AS runtime
# Config nginx (SPA fallback + gzip + cache de assets).
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Conteúdo estático compilado.
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
# Healthcheck simples (página inicial responde 200).
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]

@ -0,0 +1,56 @@
# RESILIENCE — Microsite
Single-page institutional microsite for the **RESILIENCE** research project
(*Residential Energy-Efficiency Strategies Incorporating Lifecycle Sustainability
and Circular Economy*), INESC Coimbra.
- **Project code (Streamline):** SLC20260008
- **Client:** INESC Coimbra — Álvaro Gomes
- **Methodology:** Spec-Driven Development — see [`specs/`](./specs)
## Stack
Vite · React · TypeScript · Tailwind CSS v4 · shadcn/ui · @tanstack/react-router
· framer-motion · lucide-react.
## Getting started
```bash
npm install
npm run dev # dev server (http://localhost:5173)
npm run build # type-check (tsc -b) + production build
npm run preview # preview the production build
```
> **Requisito:** Node.js ≥ 18 e npm instalados e disponíveis no PATH.
## Docker (funcionamento isolado — recomendado)
Não requer Node no host: o build corre dentro da imagem (multi-stage node→nginx).
```bash
docker build -t resilience-microsite .
docker run --rm -p 8080:80 resilience-microsite # http://localhost:8080
# ou:
docker compose up --build
```
Ver [`specs/06-deployment.md`](./specs/06-deployment.md) para detalhes.
## Structure
```
src/
data/project.ts # conteúdo canónico (fonte: input_files/)
lib/ # utils + scroll helper
components/
ui/ # primitivos shadcn (button, card, badge, dialog, sheet)
layout/ # Header, Footer, Section
sections/ # Hero, Goal, WorkPlan, Gantt, Indicators
App.tsx # composição da single page
router.tsx # @tanstack/react-router (rota /)
```
## Sections
Header (anchors: Home · Work plan · Indicators) → Hero → Goal & contribution →
Work plan (cards + interactive Gantt) → Indicators (clickable cards) → Footer
(partners + funding).
## Notes
- Conteúdo em inglês; fonte de verdade em `input_files/` (não inventar conteúdo).
- Logos dos parceiros são placeholders até serem fornecidos (ver `specs/CHANGELOG.md`).
- Paleta navy institucional definida em `src/index.css` (afinável).

@ -0,0 +1,159 @@
# Prompt de arranque
# Contexto
Gestor de projeto na empresa Streamline, responsável pela área de web-design e web-development
# Cliente
INESCC - Álvaro Gomes
# Metodologia
Spec-driven development (SDD)
# Regras de negócio
- Deve ser registado um código de projeto associado para cada situação
- Existem sistemas próprios da Streamline
- Existem sistemas associados a clientes específicos, algumas dedicadas, outras em sistemas partilhados da Streamline
- O site tem de ser obrigatoriamente em inglês, e deve seguir o design system do INESCC, mas também deve ser moderno e institucional, seguindo as melhores práticas de UX/UI
# Objetivos
Manter uma estrutura de pastas para fazer perfil e guardar histórico e rastreabilidade de caracterização, manutenção e operação da infraestrutura TI, para a própria empresa Streamline, mas também para os seus clientes que têm contratualizada a gestão da sua infraestrutura TI.
# Tarefas do agente IA
- Implementar o frontend do projeto em react, seguindo os ficheiros presentes no input files.
- Arquitetura em react e pacotes utilizados:
- tantstack router - para âncoras e navegação entre secções com scroll suave
- shadcn (para componentes)
- tailwind (para estilos)
- motion para animações
- lucide para icones
# Regras de prioridade
- Priorizar a implementação das secções principais e dos componentes mais importantes, seguindo a hierarquia apresentada nos ficheiros de input, e depois ir implementando os restantes componentes.
- Priorizar a implementação dos estilos do tailwind, seguindo as cores e estilos do design system do inescc, e depois ir ajustando os detalhes visuais e as animações.
# Estrutura de entrega
- O projeto deve ser um micro-website de uma só página para apresentação de um projeto chamado "resilience", as informações acerca dele estão presentes no documento input_files\Notas Site.docx.
# Critérios de aceitação
A aplicação deve arrancar com npm install e npm run dev, não pode ter erros de TypeScript, e as rotas principais devem estar acessíveis.
# Critérios de instalação
- O projeto deve ser instalado utilizando npm, com um ficheiro package.json presente na raiz do projeto, e deve ser possível instalar as dependências utilizando o comando `npm install`.
## Comandos por ordem de instalação de pacotes
1. npm create vite@latest
2. npm install tailwindcss @tailwindcss/vite
3. npm install -D @types/node
4. npx shadcn@latest init
5. npm i @tanstack/react-router @tanstack/react-router-devtools
6. npm i framer-motion
7. npm i lucide-react
## Não esquecer de configurar o tsconfig.json para incluir os paths dos ficheiros de configuração do shadcn, tailwind e router
``` typescript
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
```
``` json
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
// ...
}
}
```
``` typescript
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
```
# Regras de UX/UI
- Deve ser utilizado o design system shadcn para os componentes, e o tailwind para os estilos, seguindo as melhores práticas de design e usabilidade.
- O estilo visual deve ser institucional, mas moderno, seguindo as cores, botões e arredondamentos do inescc (https://www.inescc.pt/), e deve ser responsivo, adaptando-se a diferentes tamanhos de ecrã.
- Font: Outfit
- Animações, utilizando o motion, para transições suaves entre páginas e componentes, mas sem exageros, mantendo a simplicidade e a clareza da interface.
# Regras sobre os input_files
Usar os ficheiros em input_files como fonte de verdade para conteúdo, estrutura e referências visuais.
# Secções Obrigatórias
#### `Utiliza estrutura para organizar o conteúdo do microsite, seguindo a hierarquia apresentada nos ficheiros de input_files/Notas Site.docx`
### Header
Nome do projeto e menu de navegação, seguindo o design system do shadcn para os componentes de header e menu, com ancoras para os seguintes tópicos:
- Home
- Work plan
- Indicadores
### Hero
- Hero sobre o projeto:
- Nome
- Descrição
- Equipa
- Datas (inicio e fim)
- Project code
- Suporte financeiro
### Objetivos e contributo
Esta secção apresenta de forma breve o objetivo central do projeto `Resilience`, o seu enquadramento e o contributo da equipa para a sua concretização. Deve explicar, em poucas linhas, qual o problema que o projeto aborda, qual a solução proposta e qual o impacto esperado para o cliente e para o contexto científico e técnico.
A apresentação deve combinar:
- um texto introdutório breve `GOAL`;
- pontos-chave em destaque sobre o contributo da equipa;
- elementos visuais simples, como cartões ou ícones discretos;
- uma leitura fluida, com boa separação entre informação principal e secundária.
### Work plan
- Secção interativa com o work plan - presente no documento input_files/Notas Site.docx, onde cada ponto do plano de trabalho é apresentado como um cartão, e ao dar hover no cartão `(desktop)` e clicar `(mobile)`, é apresentado um cronograma, cria esse componente em código, inspirado em shadcn, através da imagem input_files/cronograma.png.
### Indicadores
- Secção de indicadores, onde são apresentados journals, conferências e outros indicadores de impacto do projeto, seguindo a estrutura apresentada no documento input_files/Notas Site.docx.
Cada um desses indicadores deve ser apresentado como um cartão, seguindo o design system do shadcn, e deve ser possível clicar em cada cartão para obter mais detalhes sobre o indicador.
### Footer
- Logos dos parceiros, seguindo a hierarquia apresentada no documento input_files/Notas Site.docx, e seguindo o design system do shadcn para os cartões de cada parceiro - INESC Coimbra - Instituto de Engenharia de Sistemas e Computadores de Coimbra, Associação para o Desenvolvimento da Aerodinâmica Industrial, Instituto Politécnico de Coimbra ISCAC.
# Ordem de trabalhos
Antes de editar, apresentar um plano curto; depois, executar e no fim resumir o que foi alterado e o que ficou por fazer
# Output
Criar pasta `specs/` com estrutura SDD
<!-- AI Agent Run Output
--

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

@ -0,0 +1,16 @@
services:
microsite:
build:
context: .
dockerfile: Dockerfile
image: resilience-microsite:latest
container_name: resilience-microsite
ports:
- "8080:80"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RESILIENCE — Residential Energy-Efficiency Strategies</title>
<meta
name="description"
content="RESILIENCE — Residential Energy-Efficiency Strategies Incorporating Lifecycle Sustainability and Circular Economy. INESC Coimbra research project."
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@ -0,0 +1,36 @@
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Compressão para texto/JS/CSS.
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/json
image/svg+xml application/xml application/xml+rss;
# Assets com hash (Vite -> /assets/*.[hash].*): cache longo e imutável.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# SPA fallback: qualquer rota/refresh devolve index.html.
location / {
try_files $uri $uri/ /index.html;
}
# index.html nunca em cache (garante deploy fresco).
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Páginas de erro também caem no SPA.
error_page 404 /index.html;
}

3207
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,36 @@
{
"name": "resilience-microsite",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "Microsite RESILIENCE — INESC Coimbra (projeto SLC20260008, Streamline)",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-router": "^1.95.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.18.0",
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@tanstack/react-router-devtools": "^1.95.0",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"typescript": "~5.7.2",
"vite": "^6.0.5"
}
}

@ -0,0 +1,33 @@
**Prompt de Arranque — Sessão do Agente**
- **Contexto:** Projeto `SLC20260008` — Microsite "Resilience" (INESCC / Streamline).
- **Ficheiros relevantes:** `boot.md`, pasta `specs/`, cronograma.png e Notas Site.docx na pasta `input_files/` (usar como fonte de verdade).
Antes de responder
1. Lê e entende completamente `boot.md`.
2. Não escrever código nem alterar ficheiros de código até a especificação SDD estar confirmada ou atualizada.
Objetivo da sessão
- Implementar o código fonte relacionado com `boot.md` seguindo a metodologia Spec-Driven Development (SDD).
Entregáveis esperados
- Versão do SDD atualizada (se necessário) antes de qualquer alteração de código.
- Código scaffold e/ou componentes implementados apenas após confirmação da SDD.
Regras e restrições
- Responder em português.
- Ser conciso e direto; priorizar tarefas conforme o SDD.
- Utilizar Tailwind + shadcn + @tanstack/react-router + framer-motion + lucide conforme `boot.md`.
- Manter rastreabilidade: registar alterações em `specs/` e referenciar ficheiros modificados.
Critérios de aceitação (por entrega)
- Para SDD: ficheiro em `specs/` atualizado e aceite.
- Para código: aplicação que arranca com `npm install` e `npm run dev` sem erros TypeScript; rotas principais funcionais.
Fluxo recomendado
1. Validar SDD (ler `boot.md` e `specs/`).
2. Propor alterações na SDD (se necessário) e aguardar aprovação.
3. Após aprovação, implementar em small increments (Header → Hero → Secções → Testes).
Caso de dúvida
- Pergunte antes de codificar.

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#0b2a4a"/>
<rect x="20" y="14" width="7" height="36" rx="3.5" fill="#1e6fd9"/>
<path d="M34 14h10a11 11 0 0 1 0 22h-3l9 14h-9l-8-13v13h-7V14h8z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

@ -0,0 +1,35 @@
# 00 — Visão geral
## Objetivo
Microsite **single-page** institucional para apresentação do projeto científico
**RESILIENCE** (*Residential Energy-Efficiency Strategies Incorporating Lifecycle
Sustainability and Circular Economy*).
## Âmbito
- Uma só página com navegação por âncoras e scroll suave.
- Idioma **obrigatoriamente inglês** (conteúdo). UI e copy em inglês.
- Visual institucional + moderno, alinhado com o design system do INESCC
(https://www.inescc.pt/) — paleta navy/azul, alto contraste, cantos suaves.
- Responsivo (mobile-first).
- **Empacotado em Docker** para funcionamento independente e isolado (sem
dependências no host). Ver [06-deployment.md](./06-deployment.md).
## Fontes de verdade (`input_files/`)
- `Notas Site.docx` — conteúdo textual canónico (título, equipa, datas, custos,
código, beneficiários, sinopse, work plan, indicadores). Ver [03-content.md](./03-content.md).
- `cronograma.png` — base visual do componente de cronograma (Gantt) do Work Plan.
## Secções obrigatórias (hierarquia)
1. **Header** — nome do projeto + navegação (Home, Work plan, Indicadores).
2. **Hero** — nome, descrição, equipa, datas, project code, suporte financeiro.
3. **Objetivos e contributo** — Goal + pontos-chave de contributo (cartões/ícones).
4. **Work plan** — cartões por tarefa; hover (desktop) / clique (mobile) revela cronograma.
5. **Indicadores** — journals, conferências, dissertações; cartões clicáveis com detalhe.
6. **Footer** — logos/cartões dos parceiros (beneficiários) + suporte financeiro.
## Critérios de aceitação (resumo)
- `npm install` + `npm run dev` arrancam sem erros.
- Sem erros de TypeScript (`tsc --noEmit` / `npm run build` limpo).
- Âncoras principais (#home, #work-plan, #indicators) acessíveis e funcionais.
Detalhe em [05-acceptance.md](./05-acceptance.md).

@ -0,0 +1,47 @@
# 01 — Design system
Inspirado no INESCC (institucional, navy, alto contraste, cantos suaves) com
camada moderna (espaçamento generoso, micro-animações discretas).
## Tipografia
- **Família:** `Outfit` (Google Fonts), fallback `system-ui, sans-serif`.
- Escala (Tailwind): display `text-5xl/6xl` (Hero), headings `text-2xl/3xl`,
corpo `text-base`, meta/labels `text-sm` uppercase tracking-wide.
- Pesos: 300/400/500/600/700.
## Paleta (tokens → CSS variables, expostos via Tailwind/shadcn)
> Valores iniciais propostos; afináveis após validação contra a marca INESCC.
| Token | Uso | HSL / Hex (proposto) |
|-------|-----|----------------------|
| `--primary` | Navy institucional INESCC | `#0B2A4A` |
| `--primary-foreground` | Texto sobre primary | `#FFFFFF` |
| `--accent` | Azul vivo / interação | `#1E6FD9` |
| `--secondary` | Superfícies suaves | `#EEF3F8` |
| `--muted` | Fundos neutros | `#F5F7FA` |
| `--muted-foreground` | Texto secundário | `#5B6B7B` |
| `--background` | Fundo página | `#FFFFFF` |
| `--foreground` | Texto principal | `#0E1A26` |
| `--border` | Linhas/contornos | `#DCE3EB` |
| `--energy` | Acento "energia/sustentabilidade" | `#2BA66B` |
Modo claro por defeito (institucional). Dark mode **fora de âmbito** v1.
## Forma e elevação
- **Border-radius:** base `0.625rem` (`--radius`), cartões `rounded-xl`, botões `rounded-lg`.
- **Sombras:** subtis (`shadow-sm`/`shadow-md` no hover). Sem neon/exageros.
- **Contornos:** cartões com `border` + fundo branco; hover eleva e intensifica accent.
## Iconografia
- `lucide-react`, traço fino, tamanho 2024px, cor `muted-foreground`/`accent`.
## Animações (framer-motion)
- Entradas por scroll: `fade + translateY(16px)`, `duration 0.40.5s`, `ease-out`,
`whileInView` com `viewport once`.
- Hover de cartões: elevação + leve `scale 1.01`.
- Stagger discreto em grelhas (delay 0.050.08s). Sem parallax agressivo.
- Respeitar `prefers-reduced-motion`.
## Responsividade
- Breakpoints Tailwind (`sm/md/lg/xl`). Grelhas: 1 col (mobile) → 23 (md/lg).
- Header colapsa em menu (sheet/drawer shadcn) < `md`.

@ -0,0 +1,63 @@
# 02 — Arquitetura
## Stack
- **Build:** Vite + React + TypeScript.
- **Estilos:** Tailwind CSS (`@tailwindcss/vite`).
- **Componentes:** shadcn/ui (Radix + Tailwind).
- **Routing/âncoras:** `@tanstack/react-router` (scroll suave entre secções).
- **Animações:** `framer-motion` (motion).
- **Ícones:** `lucide-react`.
- **Alias:** `@/*``./src/*` (tsconfig + vite resolve).
- **Empacotamento/runtime:** Docker (multi-stage node→nginx) para execução
isolada e independente do host. Ver [06-deployment.md](./06-deployment.md).
## Estrutura de pastas (proposta)
```
src/
main.tsx # bootstrap + RouterProvider
router.tsx # rootRoute + indexRoute (single page)
index.css # Tailwind + tokens (CSS vars do design system)
lib/
utils.ts # cn() (shadcn)
scroll.ts # helper de scroll suave para âncoras
data/
project.ts # conteúdo canónico (de 03-content.md), tipado
components/
ui/ # primitivos shadcn (button, card, sheet, badge, dialog...)
layout/
Header.tsx
Footer.tsx
Section.tsx # wrapper de secção (id âncora + animação in-view)
sections/
Hero.tsx
Goal.tsx # Objetivos e contributo
WorkPlan.tsx
Gantt.tsx # cronograma (inspirado em cronograma.png)
WorkPlanCard.tsx
Indicators.tsx
IndicatorCard.tsx
App.tsx # composição das secções na ordem da spec
# raiz (deployment — ver 06-deployment.md)
Dockerfile # multi-stage: node (build) → nginx (runtime)
.dockerignore # exclui node_modules/dist/.git do contexto
nginx.conf # SPA fallback + gzip + cache de assets
docker-compose.yml # build + port mapping (8080:80) + healthcheck
```
## Routing / navegação
- Single-page: uma rota `/` que renderiza todas as secções em sequência.
- Navegação por **âncoras**: `#home`, `#goal`, `#work-plan`, `#indicators`.
- Scroll suave via helper (`element.scrollIntoView({ behavior: 'smooth' })`)
acionado pelos itens de menu; TanStack Router para estrutura e (opcional) hash.
- IDs estáveis em cada `<Section>` para deep-linking.
## Dados
- Sem backend. Conteúdo estático tipado em `src/data/project.ts`, derivado
exclusivamente de [03-content.md](./03-content.md).
- Logos dos parceiros como assets em `src/assets/partners/` (placeholder se em falta).
## Decisões abertas (precisam validação)
1. Logos oficiais dos parceiros — não fornecidos em `input_files/`; usar
placeholder textual/cartão até receber ficheiros? **(assumir placeholder)**
2. Hex exatos da marca INESCC — usar paleta proposta em 01 e afinar depois.

@ -0,0 +1,60 @@
# 02 — Arquitetura
## Stack
- **Build:** Vite + React + TypeScript.
- **Estilos:** Tailwind CSS (`@tailwindcss/vite`).
- **Componentes:** shadcn/ui (Radix + Tailwind).
- **Routing/âncoras:** `@tanstack/react-router` (scroll suave entre secções).
- **Animações:** `framer-motion` (motion).
- **Ícones:** `lucide-react`.
- **Alias:** `@/*``./src/*` (tsconfig + vite resolve).
- **Solução baseada em docker**.
## Tipo de sistema
- Docker container para execução independente
- Utilizar nginx.conf para servir os ficheiros estáticos gerados pelo build do Vite.
## Estrutura de pastas (proposta)
```
src/
main.tsx # bootstrap + RouterProvider
router.tsx # rootRoute + indexRoute (single page)
index.css # Tailwind + tokens (CSS vars do design system)
lib/
utils.ts # cn() (shadcn)
scroll.ts # helper de scroll suave para âncoras
data/
project.ts # conteúdo canónico (de 03-content.md), tipado
components/
ui/ # primitivos shadcn (button, card, sheet, badge, dialog...)
layout/
Header.tsx
Footer.tsx
Section.tsx # wrapper de secção (id âncora + animação in-view)
sections/
Hero.tsx
Goal.tsx # Objetivos e contributo
WorkPlan.tsx
Gantt.tsx # cronograma (inspirado em cronograma.png)
WorkPlanCard.tsx
Indicators.tsx
IndicatorCard.tsx
App.tsx # composição das secções na ordem da spec
```
## Routing / navegação
- Single-page: uma rota `/` que renderiza todas as secções em sequência.
- Navegação por **âncoras**: `#home`, `#goal`, `#work-plan`, `#indicators`.
- Scroll suave via helper (`element.scrollIntoView({ behavior: 'smooth' })`)
acionado pelos itens de menu; TanStack Router para estrutura e (opcional) hash.
- IDs estáveis em cada `<Section>` para deep-linking.
## Dados
- Sem backend. Conteúdo estático tipado em `src/data/project.ts`, derivado
exclusivamente de [03-content.md](./03-content.md).
- Logos dos parceiros como assets em `src/assets/partners/` (placeholder se em falta).
## Decisões abertas (precisam validação)
1. Logos oficiais dos parceiros — não fornecidos em `input_files/`; usar
placeholder textual/cartão até receber ficheiros? **(assumir placeholder)**
2. Hex exatos da marca INESCC — usar paleta proposta em 01 e afinar depois.

@ -0,0 +1,95 @@
# 03 — Conteúdo canónico (fonte de verdade)
Extraído de `input_files/Notas Site.docx` e `input_files/cronograma.png`.
**Não inventar conteúdo.** Idioma: inglês.
## Identidade
- **Título:** RESILIENCE — Residential Energy-Efficiency Strategies Incorporating
Lifecycle Sustainability and Circular Economy
- **Acrónimo:** RESILIENCE
## Hero / meta
- **Project Team:** Álvaro Gomes; Carla Henriques; Eugénio Rodrigues; Marco
Fernandes; Patrícia P. Silva; Adélio Gaspar; Humberto Jorge; Marcos Tenente;
Sona Mammadzada; Luís Ventura.
- **Dates:** Start 28-07-2025 · End 26-07-2028
- **Total Eligible Cost:** 215.308,80 €
- **EU Financial Support:** 183.012,48 €
- **National Financial Support:** 32.296,32 €
- **Project Code:** 2023.17768.ICDT | COMPETE2030-FEDER-00888800; operação n.º 15224
- **Project DOI:** https://doi.org/10.54499/2023.17768.ICDT
## Synopsis
**Goal —** This project aims to bridge existing gaps in energy efficiency
assessment methodologies by adopting a comprehensive and integrated approach that
expands the assessment to other lifecycle phases of EE technologies, incorporates
circular economy principles, and provides decision-makers with the tools they need
to select the portfolios of measures that will be targeted by the EE programs.
**Contribution —** A comprehensive, decision-support framework combining:
- Multiple Benefits of EE
- Hybrid Lifecycle Analysis for full impact coverage
- Circular resource assessment
- Portfolio optimization for EE investment strategies
- Decision-makers' preferences
## Work Plan (tarefas)
| # | Tarefa |
|---|--------|
| T1 | Review of EE technologies and policies |
| T2 | Review of indicators |
| T3 | Selection of indicators |
| T4 | Energy performance simulation |
| T5 | Hybrid lifecycle analysis |
| T6 | Model for designing energy efficiency funding policies |
| T7 | Sustainability benchmark certificate |
### Cronograma (de `cronograma.png`) — janela 08/2025 → 07/2028 (36 meses)
Barras aproximadas por mês (1 = ago/2025). A afinar visualmente sobre a imagem.
| Tarefa | Início (mês) | Fim (mês) aprox. |
|--------|--------------|------------------|
| T1 | 1 | 5 |
| T2 | 1 | 6 |
| T3 | 5 | 9 |
| T4 | 6 | 17 |
| T5 | 13 | 24 |
| T6 | 22 | 31 |
| T7 | 31 | 36 |
**Milestones:** M1M10 distribuídos ao longo do projeto.
**Reports:** 1st Progress Report, 2nd Progress Report, Final Report.
> Posições exatas de milestones a refinar contra a imagem na fase de implementação.
## Indicators
### Journals
- Tenente, M., Carvalho, T., Gomes, Á. et al. *Integrating multiple impacts and
lifecycle assessment in the evaluation of energy efficiency funding programs.*
Energy Efficiency 18, 96 (2025). https://doi.org/10.1007/s12053-025-10375-5
### Conferences
- *A Critical Review of Policy and Multi-Criteria Indicators for Scaling
Residential Building Resilience in Portugal's EPBD Implementation.* ECEEE Summer
Study, 2026.
- *Optimization of Energy Efficiency Renovations in Buildings: Unlocking Multiple
Benefits Across Environmental, Economic, and Social Dimensions.* ECEEE Summer
Study, 2026.
### MSc Dissertations
- *Cidades Inteligentes, Sustentáveis e Resilientes: Papel das CER — Proposta de
uma Taxonomia.*
- *Disseminação de renováveis variáveis e da eletrificação numa rede de
distribuição — análise simulada de impactos e o papel da demand response.*
- *Reservas Estratégicas de Energia Elétrica em Portugal — Estimativa considerando
a Incerteza e a Gestão da Procura.*
### PhD Thesis
- *(sem entradas no documento)*
## Beneficiaries / Partners (Footer)
- **INESC Coimbra** — Instituto de Engenharia de Sistemas e Computadores de Coimbra
- **ADAI** — Associação para o Desenvolvimento da Aerodinâmica Industrial
- **IPC ISCAC** — Instituto Politécnico de Coimbra (ISCAC)
## Financiamento (selos no footer)
COMPETE2030 · FEDER (EU) · República Portuguesa — com montantes EU/Nacional acima.

@ -0,0 +1,56 @@
# 04 — Especificação por secção
Ordem de render = ordem desta lista. Cada secção é envolvida por `<Section id>`
com animação `whileInView`.
## 1. Header (`layout/Header.tsx`)
- Fixo no topo (`sticky`, `backdrop-blur`, fundo translúcido sobre scroll).
- Esquerda: acrónimo **RESILIENCE** (link para `#home`).
- Direita (≥ md): nav inline — **Home** (`#home`), **Work plan** (`#work-plan`),
**Indicators** (`#indicators`).
- < md: botão menu `Sheet` shadcn com os mesmos links.
- Clique em item → scroll suave para a âncora; estado ativo conforme secção visível (opcional).
- Componentes shadcn: `button`, `sheet`, (opcional `navigation-menu`).
## 2. Hero (`sections/Hero.tsx`) — id `home`
- Fundo institucional (gradiente navy subtil / formas geométricas discretas).
- Título grande (acrónimo) + subtítulo (título completo) + descrição curta (Goal resumido).
- Bloco de **meta** em cartões/linhas: Dates (start/end), Project Code, EU + National support.
- Lista compacta da **equipa** (badges ou texto).
- CTA secundário: "View work plan" → `#work-plan`.
- Animação: entrada em stagger dos blocos.
## 3. Objetivos e contributo (`sections/Goal.tsx`) — id `goal`
- Coluna texto: **Goal** (parágrafo de 03-content).
- Grelha de **contribution** (5 pontos) como cartões com ícone lucide:
Multiple Benefits, Hybrid Lifecycle Analysis, Circular resource assessment,
Portfolio optimization, Decision-makers' preferences.
- Boa separação visual entre info principal (Goal) e secundária (cartões).
## 4. Work plan (`sections/WorkPlan.tsx`) — id `work-plan`
- Intro curta + grelha de **7 cartões** (T1T7), cada um com número, título, ícone.
- Interação: **hover (desktop)** / **clique (mobile)** revela o **cronograma**.
- Desktop: ao hover num cartão, destaca a respetiva linha no `Gantt`.
- Mobile: clique abre `Dialog`/`Drawer` com o cronograma focado nessa tarefa.
- **`Gantt.tsx`** (componente próprio, inspirado em `cronograma.png`):
- Eixo temporal 36 meses (ago/2025 → jul/2028), agrupado por ano.
- Uma linha por tarefa, barra posicionada por `startMonth`/`endMonth` (de 03-content).
- Marcadores de milestones (M1M10) e reports (linhas verticais).
- Implementação CSS grid + Tailwind; cores do design system; tooltip por barra.
- Responsivo: scroll horizontal em mobile.
## 5. Indicators (`sections/Indicators.tsx`) — id `indicators`
- Sub-grupos: **Journals**, **Conferences**, **MSc Dissertations**, (PhD — vazio/oculto).
- Cada item = `IndicatorCard` (tipo via badge, título, autores/venue/ano).
- Clique no cartão → `Dialog` com detalhe completo + link (DOI/URL quando exista).
- Grelha responsiva; ícones lucide por tipo (BookOpen, Presentation, GraduationCap).
## 6. Footer (`layout/Footer.tsx`)
- Cartões dos **3 beneficiários** (INESC Coimbra, ADAI, IPCISCAC) — logo/nome.
- Linha de **financiamento**: COMPETE2030 · FEDER · República Portuguesa + montantes.
- Project code + DOI. Copyright Streamline/INESCC.
- Logos: assets em `src/assets/partners/`; placeholder textual se não fornecidos.
## Wrapper `Section.tsx`
- Props: `id`, `className`, `children`, `title?`, `eyebrow?`.
- Aplica `scroll-mt` (compensar header fixo) + animação in-view padrão.

@ -0,0 +1,43 @@
# 05 — Critérios de aceitação e verificação
## Funcionais
- [ ] `npm install` conclui sem erros.
- [ ] `npm run dev` arranca e serve a aplicação sem erros de runtime.
- [ ] `npm run build` (ou `tsc --noEmit`) sem **erros de TypeScript**.
- [ ] Âncoras `#home`, `#work-plan`, `#indicators` (+`#goal`) navegáveis com scroll suave.
- [ ] Header colapsa em menu < md; links funcionam em mobile e desktop.
- [ ] Work plan: hover (desktop) e clique (mobile) revelam o cronograma.
- [ ] Indicators: cartões clicáveis abrem detalhe.
- [ ] Footer lista os 3 beneficiários e o suporte financeiro.
## Containerização (Docker) — ver [06-deployment.md](./06-deployment.md)
- [ ] **RF-D1** `docker build -t resilience-microsite .` conclui sem erros (o build
corre `npm run build``tsc -b`; erros de TS fazem o build falhar).
- [ ] **RF-D2** `docker run -p 8080:80 resilience-microsite` serve o site em
http://localhost:8080 sem depender de Node/npm no host.
- [ ] **RF-D3** `docker compose up --build` arranca o serviço com a porta mapeada.
- [ ] **RF-D4** Refresh/deep-link de uma âncora (ex.: `/#work-plan`) devolve 200
(SPA fallback do nginx para `index.html`).
- [ ] **RF-D5** Imagem final isolada e sem estado: não inclui código-fonte nem
Node; não requer volumes nem serviços externos.
- [ ] **RF-D6** Versões base pinadas e `.dockerignore` exclui `node_modules`/`dist`.
## Conteúdo
- [ ] Todo o texto em inglês e fiel a [03-content.md](./03-content.md) (sem invenção).
- [ ] Datas, código de projeto, montantes e DOI corretos.
## Visual / UX
- [ ] Paleta navy institucional INESCC + tipografia Outfit aplicadas.
- [ ] Responsivo (mobile → desktop) sem overflow/quebras.
- [ ] Animações discretas; respeitam `prefers-reduced-motion`.
## Rastreabilidade
- [ ] Cada incremento de código referenciado em [CHANGELOG.md](./CHANGELOG.md).
## Plano de verificação
1. **Com Node local:** `npm install``npm run build` (gate TypeScript) →
`npm run dev` → inspeção manual das 6 secções + navegação.
2. **Sem Node local (recomendado nesta máquina):** `docker build` (gate TS dentro
da imagem) → `docker run -p 8080:80` → inspeção manual em http://localhost:8080.
3. Teste responsivo (largura mobile/desktop) e teclado/scroll.
4. Teste de SPA fallback: refrescar com hash `/#indicators` devolve a página.

@ -0,0 +1,49 @@
# 06 — Deployment & containerização (Docker)
## Objetivo
Encapsular o microsite num **contentor Docker** para funcionamento **independente
e isolado**: a aplicação corre da mesma forma em qualquer host com Docker, sem
depender de um Node/npm instalado na máquina nem de configuração externa.
> Motivação adicional: nesta máquina não existe Node/npm instalados — o Docker
> remove essa dependência do host (o build corre dentro da imagem). Ver
> [CHANGELOG.md](./CHANGELOG.md).
## Estratégia
**Multi-stage build:**
1. **Stage `build`** (`node:22-alpine`): `npm ci` (ou `npm install`) + `npm run
build` → gera `dist/` (estático Vite).
2. **Stage `runtime`** (`nginx:1.27-alpine`): serve o `dist/` como conteúdo
estático. Imagem final pequena, sem Node nem código-fonte.
Como é uma SPA single-page, o runtime é um servidor estático; o nginx trata de
fallback de rotas para `index.html` e do cache de assets.
## Artefactos a criar (após aprovação)
| Ficheiro | Função |
|----------|--------|
| `Dockerfile` | Build multi-stage (node → nginx) |
| `.dockerignore` | Excluir `node_modules`, `dist`, `.git`, etc. do contexto |
| `nginx.conf` | Config nginx: porta 80, SPA fallback, gzip, cache de assets |
| `docker-compose.yml` | Orquestração local (build + port mapping + healthcheck) |
## Contrato de operação
- **Build:** `docker build -t resilience-microsite .`
- **Run:** `docker run --rm -p 8080:80 resilience-microsite` → http://localhost:8080
- **Compose:** `docker compose up --build` (porta exposta `8080:80` por defeito).
- **Porta interna:** 80 (nginx). **Porta externa:** configurável (default 8080).
- **Sem estado:** não há volumes nem dependências externas (site 100% estático).
- **Healthcheck:** GET `/` devolve 200.
## nginx — requisitos
- `try_files $uri $uri/ /index.html;` (suporte a deep-link de âncoras/refresh).
- `gzip on` para HTML/CSS/JS.
- Cache longo para assets com hash (`/assets/*`), `no-cache` para `index.html`.
- Servir em IPv4/IPv6, porta 80.
## Notas
- O `Dockerfile` torna a verificação do gate de TypeScript reprodutível mesmo sem
Node no host: `npm run build` (que corre `tsc -b`) executa no stage de build; se
houver erros de TS, o `docker build` falha — passando a ser o método de
verificação de aceitação enquanto não houver Node instalado localmente.
- Versões base pinadas (`node:22-alpine`, `nginx:1.27-alpine`) para reprodutibilidade.

@ -0,0 +1,62 @@
# CHANGELOG — rastreabilidade
Registo de decisões e ficheiros alterados, por incremento. Referenciar a secção
da SDD que justifica cada alteração.
## v0.3 — Containerização Docker (2026-06-12) — SDD
- Novo requisito: encapsular o site em **Docker** para funcionamento independente
e isolado. Criado [06-deployment.md](./06-deployment.md) (build multi-stage
node→nginx, SPA fallback, compose, healthcheck).
- Novos requisitos funcionais **RF-D1..RF-D6** em [05-acceptance.md](./05-acceptance.md).
- Atualizados [00-overview.md](./00-overview.md), [02-architecture.md](./02-architecture.md)
(stack + estrutura: `Dockerfile`, `.dockerignore`, `nginx.conf`,
`docker-compose.yml`) e índice [README.md](./README.md).
- Benefício direto: o build de TypeScript passa a ser verificável via
`docker build` mesmo sem Node instalado no host (ver [[node-not-installed]]).
- **Estado:** SDD de deployment **aprovada**. Ficheiros Docker criados:
`Dockerfile` (multi-stage node:22-alpine → nginx:1.27-alpine; `npm ci || npm
install`; healthcheck), `.dockerignore`, `nginx.conf` (SPA fallback + gzip +
cache de assets), `docker-compose.yml` (porta `8080:80`, restart, healthcheck).
- **Verificação pendente:** Docker **não está instalado** nesta máquina (`docker`
não está no PATH). Não foi possível correr `docker build` para validar
RF-D1..RF-D6 nem o gate de TS via imagem. Executar assim que houver Docker.
## v0.1 — SDD inicial (2026-06-11)
- Criada estrutura `specs/` (0005 + README + CHANGELOG) a partir de `boot.md`,
`prompt.md` e `input_files/` (`Notas Site.docx`, `cronograma.png`).
- Conteúdo canónico extraído para [03-content.md](./03-content.md).
- **Estado:** SDD **aprovada** (2026-06-11). Início da implementação.
### Decisões (aprovadas pelo PM)
1. Logos dos parceiros → **placeholders** (cartões com sigla/nome) até receber ficheiros.
2. Paleta → usar a **proposta** (primary `#0B2A4A`, accent `#1E6FD9`); afinar depois.
3. Posições de milestones/reports no cronograma — estimadas da imagem; refinar no `Gantt`.
## v0.2 — Implementação (2026-06-11)
Scaffold + implementação completa (SDD §02/§04). Ficheiros criados:
**Config / raiz**
- `package.json`, `vite.config.ts`, `tsconfig.json`, `tsconfig.app.json`,
`tsconfig.node.json`, `components.json`, `index.html`, `.gitignore`,
`README.md`, `public/favicon.svg`
**Core**
- `src/main.tsx`, `src/router.tsx` (tanstack-router, rota `/`), `src/index.css`
(tokens do design system §01), `src/vite-env.d.ts`, `src/App.tsx`
- `src/lib/utils.ts` (cn), `src/lib/scroll.ts` (scroll suave + navItems)
- `src/data/project.ts` (conteúdo canónico §03, tipado)
**UI (shadcn)** — `src/components/ui/`: `button`, `card`, `badge`, `dialog`, `sheet`
**Layout** — `src/components/layout/`: `Header` (nav + sheet mobile), `Footer`
(parceiros placeholder + financiamento), `Section` (wrapper âncora + in-view)
**Sections** — `src/components/sections/`: `Hero`, `Goal`, `WorkPlan`,
`WorkPlanCard`, `Gantt` (cronograma inspirado em `cronograma.png`), `Indicators`,
`IndicatorCard`
### Verificação
- **Pendente:** Node.js/npm não estão instalados nesta máquina (não há `node.exe`
em C:). Não foi possível correr `npm install` / `npm run build` para validar o
gate de TypeScript. Código revisto manualmente. Executar a verificação assim
que o Node estiver disponível (ver `specs/05-acceptance.md`).

@ -0,0 +1,31 @@
# SDD — Microsite "RESILIENCE"
**Código de projeto:** SLC20260008
**Cliente:** INESC Coimbra — Álvaro Gomes
**Empresa:** Streamline (web-design / web-development)
**Metodologia:** Spec-Driven Development (SDD)
Especificação que governa a implementação do microsite. Nenhum código deve ser
escrito ou alterado sem que a SDD correspondente esteja aceite. Toda a alteração
de código deve referenciar a secção da spec que a justifica.
## Estrutura
| Ficheiro | Conteúdo |
|----------|----------|
| [00-overview.md](./00-overview.md) | Objetivo, âmbito, fontes de verdade, critérios de aceitação |
| [01-design-system.md](./01-design-system.md) | Cores, tipografia, espaçamento, tokens shadcn/Tailwind |
| [02-architecture.md](./02-architecture.md) | Stack, estrutura de pastas, routing, navegação, animações |
| [03-content.md](./03-content.md) | Conteúdo canónico extraído de `input_files/` (fonte de verdade) |
| [04-sections.md](./04-sections.md) | Especificação por secção/componente |
| [05-acceptance.md](./05-acceptance.md) | Critérios de aceitação e plano de verificação |
| [06-deployment.md](./06-deployment.md) | Containerização Docker (funcionamento isolado) |
| [CHANGELOG.md](./CHANGELOG.md) | Rastreabilidade: decisões e ficheiros alterados |
## Estado
- [x] SDD redigida (v0.1) e aprovada
- [x] Scaffold do projeto (Vite + TS + Tailwind + shadcn + router)
- [x] Header → Hero → Secções → Footer
- [x] Containerização Docker (v0.3 — [06-deployment.md](./06-deployment.md)) — ficheiros criados
- [ ] Verificação dos critérios de aceitação (via `docker build`) — **pendente: Docker não instalado**

@ -0,0 +1,33 @@
**Prompt de Arranque — Sessão do Agente**
- **Contexto:** Projeto `SLC20260008` — Microsite "Resilience" (INESCC / Streamline).
- **Ficheiros relevantes:** `boot.md`, pasta `specs/`, cronograma.png e Notas Site.docx na pasta `input_files/` (usar como fonte de verdade).
Antes de responder
1. Lê e entende completamente `boot.md`.
2. Não escrever código nem alterar ficheiros de código até a especificação SDD estar confirmada ou atualizada.
Objetivo da sessão
- Implementar o código fonte relacionado com `boot.md` seguindo a metodologia Spec-Driven Development (SDD).
Entregáveis esperados
- Versão do SDD atualizada (se necessário) antes de qualquer alteração de código.
- Código scaffold e/ou componentes implementados apenas após confirmação da SDD.
Regras e restrições
- Responder em português.
- Ser conciso e direto; priorizar tarefas conforme o SDD.
- Utilizar Tailwind + shadcn + @tanstack/react-router + framer-motion + lucide conforme `boot.md`.
- Manter rastreabilidade: registar alterações em `specs/` e referenciar ficheiros modificados.
Critérios de aceitação (por entrega)
- Para SDD: ficheiro em `specs/` atualizado e aceite.
- Para código: aplicação que arranca com `npm install` e `npm run dev` sem erros TypeScript; rotas principais funcionais.
Fluxo recomendado
1. Validar SDD (ler `boot.md` e `specs/`).
2. Propor alterações na SDD (se necessário) e aguardar aprovação.
3. Após aprovação, implementar em small increments (Header → Hero → Secções → Testes).
Caso de dúvida
- Pergunte antes de codificar.

@ -0,0 +1,21 @@
import { Header } from "@/components/layout/Header"
import { Footer } from "@/components/layout/Footer"
import { Hero } from "@/components/sections/Hero"
import { Goal } from "@/components/sections/Goal"
import { WorkPlan } from "@/components/sections/WorkPlan"
import { Indicators } from "@/components/sections/Indicators"
export default function App() {
return (
<div className="min-h-screen bg-background">
<Header />
<main>
<Hero />
<Goal />
<WorkPlan />
<Indicators />
</main>
<Footer />
</div>
)
}

@ -0,0 +1,97 @@
import { ExternalLink } from "lucide-react";
import { Card } from "@/components/ui/card";
import { project, partners, funders } from "@/data/project";
export function Footer() {
return (
<footer className="border-t border-border bg-primary text-primary-foreground">
<div className="mx-auto w-full max-w-6xl px-5 py-16 md:px-8">
<div className="mb-4 text-sm font-semibold uppercase tracking-widest text-brand-foreground/70">
Beneficiaries
</div>
{/* Partner cards (placeholders — logos a fornecer, ver specs/CHANGELOG.md) */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{partners.map((p) => (
<Card
key={p.short}
className="group border-white/15 bg-transparent text-primary-foreground transition-colors hover:bg-white/10"
>
<div className="flex h-32 items-center justify-center p-6">
<img
src={p.image}
alt={p.name}
className="max-h-full w-auto object-contain opacity-80 transition-opacity group-hover:opacity-100"
/>
</div>
<div className="border-t border-white/10 px-4 py-3">
<p className="text-sm font-medium">{p.name}</p>
</div>
</Card>
))}
</div>
{/* Funding */}
<div className="mt-12 grid gap-8 border-t border-white/15 pt-10 md:grid-cols-2">
<div>
<div className="mb-3 text-sm font-semibold uppercase tracking-widest text-brand-foreground/70">
Financial support
</div>
<div className="flex flex-wrap gap-3">
{funders.map((f) => (
<span
key={f.name}
className="rounded-md border border-white/20 bg-white/80 px-3 py-1.5 text-sm text-primary-foreground/90"
>
<img src={f.image} alt={f.name} className="inline h-10" />
</span>
))}
</div>
<dl className="mt-5 space-y-1 text-sm text-primary-foreground/75">
<div className="flex gap-2">
<dt className="text-primary-foreground/55">EU support:</dt>
<dd>{project.funding.euSupport}</dd>
</div>
<div className="flex gap-2">
<dt className="text-primary-foreground/55">
National support:
</dt>
<dd>{project.funding.nationalSupport}</dd>
</div>
<div className="flex gap-2">
<dt className="text-primary-foreground/55">
Total eligible cost:
</dt>
<dd>{project.funding.totalEligibleCost}</dd>
</div>
</dl>
</div>
<div className="md:text-right">
<div className="mb-3 text-sm font-semibold uppercase tracking-widest text-brand-foreground/70">
Project reference
</div>
<p className="text-sm text-primary-foreground/80">
{project.projectCode}
</p>
<a
href={project.doi}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1.5 text-sm text-primary-foreground/90 underline-offset-4 hover:underline md:justify-end"
>
{project.doi.replace("https://", "")}
<ExternalLink className="h-3.5 w-3.5" />
</a>
</div>
</div>
<div className="mt-12 border-t border-white/15 pt-6 text-xs text-primary-foreground/55">
© {project.dates.end.slice(-4)} {project.acronym} INESC Coimbra.
Microsite developed by Streamline.
</div>
</div>
</footer>
);
}

@ -0,0 +1,104 @@
import { useEffect, useState } from "react"
import { Menu } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { cn } from "@/lib/utils"
import { navItems, scrollToSection } from "@/lib/scroll"
import { project } from "@/data/project"
export function Header() {
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8)
onScroll()
window.addEventListener("scroll", onScroll, { passive: true })
return () => window.removeEventListener("scroll", onScroll)
}, [])
return (
<header
className={cn(
"fixed inset-x-0 top-0 z-40 transition-all duration-300",
scrolled
? "border-b border-border bg-background/85 backdrop-blur-md"
: "border-b border-transparent bg-transparent"
)}
>
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-5 md:px-8">
<button
onClick={() => scrollToSection("home")}
className={cn(
"flex items-center gap-2 text-lg font-bold tracking-tight focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors duration-300",
scrolled ? "text-primary" : "text-white"
)}
>
<span className="inline-block h-5 w-1.5 rounded-full bg-brand" />
{project.acronym}
</button>
{/* Desktop nav */}
<nav className="hidden items-center gap-1 md:flex">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={cn(
"rounded-md px-4 py-2 text-sm font-medium transition-colors duration-300",
scrolled
? "text-muted-foreground hover:bg-secondary hover:text-primary"
: "text-white/80 hover:bg-white/10 hover:text-white"
)}
>
{item.label}
</button>
))}
</nav>
{/* Mobile menu */}
<div className="md:hidden">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Open menu"
className={cn(
"transition-colors duration-300",
!scrolled && "border-white/30 bg-transparent text-white hover:bg-white/10 hover:text-white"
)}
>
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>{project.acronym}</SheetTitle>
</SheetHeader>
<nav className="flex flex-col gap-1">
{navItems.map((item) => (
<SheetClose asChild key={item.id}>
<button
onClick={() => scrollToSection(item.id)}
className="rounded-md px-3 py-3 text-left text-base font-medium text-foreground transition-colors hover:bg-secondary"
>
{item.label}
</button>
</SheetClose>
))}
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
)
}

@ -0,0 +1,57 @@
import * as React from "react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
interface SectionProps {
id: string
className?: string
containerClassName?: string
eyebrow?: string
title?: string
description?: string
children: React.ReactNode
}
export function Section({
id,
className,
containerClassName,
eyebrow,
title,
description,
children,
}: SectionProps) {
return (
<section id={id} className={cn("scroll-mt-20 py-20 md:py-28", className)}>
<div className={cn("mx-auto w-full max-w-6xl px-5 md:px-8", containerClassName)}>
{(eyebrow || title || description) && (
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="mb-12 max-w-2xl"
>
{eyebrow && (
<span className="text-sm font-semibold uppercase tracking-widest text-brand">
{eyebrow}
</span>
)}
{title && (
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-foreground md:text-4xl">
{title}
</h2>
)}
{description && (
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
{description}
</p>
)}
</motion.div>
)}
{children}
</div>
</section>
)
}

@ -0,0 +1,198 @@
import { Flag, FileText } from "lucide-react"
import { cn } from "@/lib/utils"
import { tasks, milestones, reports, timeline } from "@/data/project"
const MONTH_ABBR = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
// Constrói segmentos de ano a partir do mês inicial (ago/2025) e total de meses.
function buildYearSegments() {
const segments: { year: number; span: number; startMonthIndex: number }[] = []
let monthOfYear = timeline.startMonthOfYear // 8 = agosto
let year = timeline.startYear
let current = { year, span: 0, startMonthIndex: 1 }
for (let m = 1; m <= timeline.totalMonths; m++) {
if (current.span === 0) current.startMonthIndex = m
current.span++
if (monthOfYear === 12) {
segments.push(current)
year++
monthOfYear = 1
current = { year, span: 0, startMonthIndex: m + 1 }
} else {
monthOfYear++
}
}
if (current.span > 0) segments.push(current)
return segments
}
const pct = (n: number) => (n / timeline.totalMonths) * 100
interface GanttProps {
activeTaskId?: string | null
compact?: boolean
}
export function Gantt({ activeTaskId, compact }: GanttProps) {
const yearSegments = buildYearSegments()
return (
<div className="overflow-x-auto rounded-xl border border-border bg-card">
<div className={cn("min-w-[760px]", compact && "min-w-[680px]")}>
{/* Year + month axis */}
<div className="grid grid-cols-[180px_1fr] border-b border-border bg-secondary/40">
<div className="flex items-end px-4 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Task
</div>
<div>
{/* Years */}
<div className="flex">
{yearSegments.map((s) => (
<div
key={s.year}
className="border-l border-border py-1.5 text-center text-xs font-semibold text-foreground"
style={{ width: `${pct(s.span)}%` }}
>
{s.year}
</div>
))}
</div>
{/* Months */}
<div className="flex border-t border-border/60">
{Array.from({ length: timeline.totalMonths }, (_, i) => {
const monthOfYear =
((timeline.startMonthOfYear - 1 + i) % 12)
return (
<div
key={i}
className="border-l border-border/40 py-1 text-center text-[10px] text-muted-foreground"
style={{ width: `${pct(1)}%` }}
>
{MONTH_ABBR[monthOfYear][0]}
</div>
)
})}
</div>
</div>
</div>
{/* Task rows */}
<div className="relative">
{/* Report vertical lines (over the whole rows area) */}
<div className="pointer-events-none absolute inset-0 ml-[180px]">
{reports.map((r) => (
<div
key={r.label}
className="absolute top-0 bottom-0 border-l border-dashed border-brand/50"
style={{ left: `${pct(r.month)}%` }}
title={r.label}
/>
))}
</div>
{tasks.map((t, idx) => {
const isActive = activeTaskId === t.id
return (
<div
key={t.id}
className={cn(
"grid grid-cols-[180px_1fr] items-center border-b border-border/60 transition-colors",
idx % 2 === 1 && "bg-muted/40",
isActive && "bg-brand/5"
)}
>
<div className="flex items-center gap-2 px-4 py-3">
<span
className={cn(
"flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-[11px] font-bold",
isActive
? "bg-brand text-brand-foreground"
: "bg-secondary text-primary"
)}
>
{t.id}
</span>
<span
className={cn(
"truncate text-xs",
isActive
? "font-semibold text-foreground"
: "text-muted-foreground"
)}
title={t.title}
>
{t.title}
</span>
</div>
<div className="relative h-10">
<div
className={cn(
"absolute top-1/2 h-3.5 -translate-y-1/2 rounded-full transition-all",
isActive
? "bg-brand shadow-sm ring-2 ring-brand/30"
: "bg-primary/70"
)}
style={{
left: `${pct(t.startMonth - 1)}%`,
width: `${pct(t.endMonth - t.startMonth + 1)}%`,
}}
/>
</div>
</div>
)
})}
{/* Milestones row */}
<div className="grid grid-cols-[180px_1fr] items-center border-b border-border/60">
<div className="flex items-center gap-1.5 px-4 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<Flag className="h-3.5 w-3.5 text-energy" />
Milestones
</div>
<div className="relative h-9">
{milestones.map((m) => (
<div
key={m.id}
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2"
style={{ left: `${pct(m.month - 0.5)}%` }}
title={`${m.id} · month ${m.month}`}
>
<div className="flex flex-col items-center">
<div className="h-2.5 w-2.5 rotate-45 rounded-[2px] bg-energy" />
<span className="mt-0.5 text-[9px] font-medium text-muted-foreground">
{m.id}
</span>
</div>
</div>
))}
</div>
</div>
{/* Reports row */}
<div className="grid grid-cols-[180px_1fr] items-center">
<div className="flex items-center gap-1.5 px-4 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<FileText className="h-3.5 w-3.5 text-brand" />
Reports
</div>
<div className="relative h-9">
{reports.map((r) => (
<div
key={r.label}
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 whitespace-nowrap text-center"
style={{ left: `${pct(r.month)}%` }}
>
<span className="rounded bg-brand/10 px-1.5 py-0.5 text-[9px] font-medium text-brand">
{r.label}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}

@ -0,0 +1,79 @@
import { motion } from "framer-motion"
import {
Layers,
Recycle,
RefreshCw,
TrendingUp,
Users,
type LucideIcon,
} from "lucide-react"
import { Card } from "@/components/ui/card"
import { Section } from "@/components/layout/Section"
import { goal } from "@/data/project"
const iconMap: Record<string, LucideIcon> = {
Layers,
Recycle,
RefreshCw,
TrendingUp,
Users,
}
export function Goal() {
return (
<Section
id="goal"
eyebrow="Goal & contribution"
title="Closing the gaps in energy-efficiency assessment"
className="bg-background"
>
<div className="grid gap-12 lg:grid-cols-12">
{/* Goal text */}
<div className="lg:col-span-5">
<div className="rounded-xl border border-border bg-secondary/50 p-6">
<span className="text-sm font-semibold uppercase tracking-widest text-brand">
The challenge
</span>
<p className="mt-4 text-lg leading-relaxed text-foreground">
{goal.text}
</p>
</div>
</div>
{/* Contribution cards */}
<div className="lg:col-span-7">
<h3 className="mb-5 text-sm font-semibold uppercase tracking-widest text-muted-foreground">
Our contribution a decision-support framework
</h3>
<div className="grid gap-4 sm:grid-cols-2">
{goal.contributions.map((c, i) => {
const Icon = iconMap[c.icon] ?? Layers
return (
<motion.div
key={c.title}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.45, delay: i * 0.06, ease: "easeOut" }}
>
<Card className="h-full p-5 transition-all hover:-translate-y-1 hover:border-brand/40 hover:shadow-md">
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-brand/10 text-brand">
<Icon className="h-5 w-5" />
</div>
<h4 className="mt-4 font-semibold text-foreground">
{c.title}
</h4>
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
{c.description}
</p>
</Card>
</motion.div>
)
})}
</div>
</div>
</div>
</Section>
)
}

@ -0,0 +1,157 @@
import type { ReactNode } from "react"
import { motion } from "framer-motion"
import { ArrowDown, Calendar, Hash, Users, Euro } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { scrollToSection } from "@/lib/scroll"
import { project } from "@/data/project"
const fade = {
hidden: { opacity: 0, y: 18 },
show: { opacity: 1, y: 0 },
}
export function Hero() {
return (
<section
id="home"
className="relative scroll-mt-20 overflow-hidden bg-primary text-primary-foreground"
>
{/* Background decoration */}
<div className="pointer-events-none absolute inset-0">
<div className="absolute -right-24 -top-24 h-96 w-96 rounded-full bg-brand/25 blur-3xl" />
<div className="absolute -bottom-32 -left-24 h-96 w-96 rounded-full bg-energy/15 blur-3xl" />
<div
className="absolute inset-0 opacity-[0.07]"
style={{
backgroundImage:
"linear-gradient(to right, #fff 1px, transparent 1px), linear-gradient(to bottom, #fff 1px, transparent 1px)",
backgroundSize: "48px 48px",
}}
/>
</div>
<div className="relative mx-auto w-full max-w-6xl px-5 pb-24 pt-36 md:px-8 md:pb-32 md:pt-44">
<motion.div
initial="hidden"
animate="show"
transition={{ staggerChildren: 0.08 }}
>
<motion.div variants={fade} transition={{ duration: 0.5 }}>
<Badge className="bg-white/10 text-primary-foreground backdrop-blur">
INESC Coimbra · Research Project
</Badge>
</motion.div>
<motion.h1
variants={fade}
transition={{ duration: 0.5 }}
className="mt-6 text-5xl font-bold leading-[1.05] tracking-tight md:text-7xl"
>
{project.acronym}
</motion.h1>
<motion.p
variants={fade}
transition={{ duration: 0.5 }}
className="mt-5 max-w-3xl text-lg font-medium text-primary-foreground/85 md:text-2xl"
>
{project.title}
</motion.p>
<motion.p
variants={fade}
transition={{ duration: 0.5 }}
className="mt-4 max-w-2xl text-base leading-relaxed text-primary-foreground/70"
>
{project.tagline}
</motion.p>
<motion.div
variants={fade}
transition={{ duration: 0.5 }}
className="mt-8 flex flex-wrap gap-3"
>
<Button variant="brand" onClick={() => scrollToSection("work-plan")}>
View work plan
<ArrowDown className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={() => scrollToSection("goal")}
className="border-white/25 bg-transparent text-primary-foreground hover:bg-white/10 hover:text-primary-foreground"
>
About the project
</Button>
</motion.div>
{/* Meta grid */}
<motion.div
variants={fade}
transition={{ duration: 0.5 }}
className="mt-14 grid gap-px overflow-hidden rounded-xl border border-white/15 bg-white/10 sm:grid-cols-2 lg:grid-cols-4"
>
<MetaCell icon={<Calendar className="h-4 w-4" />} label="Duration">
{project.dates.start} {project.dates.end}
</MetaCell>
<MetaCell icon={<Hash className="h-4 w-4" />} label="Project code">
2023.17768.ICDT
</MetaCell>
<MetaCell icon={<Euro className="h-4 w-4" />} label="EU support">
{project.funding.euSupport}
</MetaCell>
<MetaCell icon={<Users className="h-4 w-4" />} label="Team">
{project.team.length} researchers
</MetaCell>
</motion.div>
{/* Team list */}
<motion.div
variants={fade}
transition={{ duration: 0.5 }}
className="mt-8"
>
<div className="mb-3 text-xs font-semibold uppercase tracking-widest text-primary-foreground/55">
Project team
</div>
<div className="flex flex-wrap gap-2">
{project.team.map((member) => (
<span
key={member}
className="rounded-full border border-white/15 bg-white/5 px-3 py-1 text-sm text-primary-foreground/85"
>
{member}
</span>
))}
</div>
</motion.div>
</motion.div>
</div>
</section>
)
}
function MetaCell({
icon,
label,
children,
}: {
icon: ReactNode
label: string
children: ReactNode
}) {
return (
<div className="bg-primary/40 p-5">
<div className="flex items-center gap-2 text-primary-foreground/60">
{icon}
<span className="text-xs font-semibold uppercase tracking-wider">
{label}
</span>
</div>
<div className="mt-2 text-base font-semibold text-primary-foreground">
{children}
</div>
</div>
)
}

@ -0,0 +1,78 @@
import { motion } from "framer-motion"
import {
BookOpen,
Presentation,
GraduationCap,
FileBadge,
ArrowUpRight,
type LucideIcon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Card } from "@/components/ui/card"
import type { Indicator, IndicatorType } from "@/data/project"
const typeMeta: Record<
IndicatorType,
{ label: string; icon: LucideIcon }
> = {
journal: { label: "Journal", icon: BookOpen },
conference: { label: "Conference", icon: Presentation },
msc: { label: "MSc Dissertation", icon: GraduationCap },
phd: { label: "PhD Thesis", icon: FileBadge },
}
interface IndicatorCardProps {
indicator: Indicator
index: number
onOpen: () => void
}
export function IndicatorCard({ indicator, index, onOpen }: IndicatorCardProps) {
const meta = typeMeta[indicator.type]
const Icon = meta.icon
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.4, delay: index * 0.05, ease: "easeOut" }}
>
<button
type="button"
onClick={onOpen}
className="group block h-full w-full text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
aria-label={`Open details: ${indicator.title}`}
>
<Card className="flex h-full flex-col p-5 transition-all hover:-translate-y-1 hover:border-brand/40 hover:shadow-md">
<div className="flex items-center justify-between">
<Badge variant="brand" className="gap-1.5">
<Icon className="h-3.5 w-3.5" />
{meta.label}
</Badge>
{indicator.year && (
<span className="text-xs font-medium text-muted-foreground">
{indicator.year}
</span>
)}
</div>
<h4 className="mt-4 line-clamp-4 font-semibold leading-snug text-foreground">
{indicator.title}
</h4>
{(indicator.authors || indicator.venue) && (
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">
{[indicator.authors, indicator.venue].filter(Boolean).join(" · ")}
</p>
)}
<span className="mt-auto pt-4 inline-flex items-center gap-1 text-sm font-medium text-brand">
View details
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</span>
</Card>
</button>
</motion.div>
)
}
export { typeMeta }

@ -0,0 +1,105 @@
import { useState } from "react"
import { ExternalLink } from "lucide-react"
import { Section } from "@/components/layout/Section"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
IndicatorCard,
typeMeta,
} from "@/components/sections/IndicatorCard"
import { indicators, type Indicator, type IndicatorType } from "@/data/project"
const groups: { type: IndicatorType; heading: string }[] = [
{ type: "journal", heading: "Journals" },
{ type: "conference", heading: "Conferences" },
{ type: "msc", heading: "MSc Dissertations" },
]
export function Indicators() {
const [open, setOpen] = useState<Indicator | null>(null)
return (
<Section
id="indicators"
eyebrow="Indicators"
title="Impact & dissemination"
description="Journals, conference papers and dissertations produced within the project. Select any card for full details."
className="bg-background"
>
<div className="space-y-12">
{groups.map((g) => {
const items = indicators.filter((i) => i.type === g.type)
if (items.length === 0) return null
return (
<div key={g.type}>
<div className="mb-5 flex items-center gap-3">
<h3 className="text-lg font-semibold text-foreground">
{g.heading}
</h3>
<Badge variant="secondary">{items.length}</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{items.map((indicator, i) => (
<IndicatorCard
key={indicator.id}
indicator={indicator}
index={i}
onOpen={() => setOpen(indicator)}
/>
))}
</div>
</div>
)
})}
</div>
<Dialog open={open !== null} onOpenChange={(o) => !o && setOpen(null)}>
<DialogContent>
{open && (
<>
<DialogHeader>
<div className="mb-1">
<Badge variant="brand">{typeMeta[open.type].label}</Badge>
</div>
<DialogTitle>{open.title}</DialogTitle>
{open.authors && (
<DialogDescription>{open.authors}</DialogDescription>
)}
</DialogHeader>
<dl className="space-y-2 text-sm">
{open.venue && (
<div className="flex gap-2">
<dt className="w-20 shrink-0 text-muted-foreground">Venue</dt>
<dd className="text-foreground">{open.venue}</dd>
</div>
)}
{open.year && (
<div className="flex gap-2">
<dt className="w-20 shrink-0 text-muted-foreground">Year</dt>
<dd className="text-foreground">{open.year}</dd>
</div>
)}
</dl>
{open.url && (
<Button variant="brand" asChild className="mt-2 w-fit">
<a href={open.url} target="_blank" rel="noopener noreferrer">
Open publication
<ExternalLink className="h-4 w-4" />
</a>
</Button>
)}
</>
)}
</DialogContent>
</Dialog>
</Section>
)
}

@ -0,0 +1,80 @@
import { useState } from "react"
import { CalendarRange } from "lucide-react"
import { Section } from "@/components/layout/Section"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Gantt } from "@/components/sections/Gantt"
import { WorkPlanCard } from "@/components/sections/WorkPlanCard"
import { tasks } from "@/data/project"
export function WorkPlan() {
const [activeTaskId, setActiveTaskId] = useState<string | null>(null)
const [openTaskId, setOpenTaskId] = useState<string | null>(null)
const openTask = tasks.find((t) => t.id === openTaskId) ?? null
return (
<Section
id="work-plan"
eyebrow="Work plan"
title="Seven tasks across 36 months"
description="Hover a task to highlight it on the timeline, or open it to focus its schedule. The chart follows the project Gantt — months, milestones (M1M10) and progress reports."
className="bg-muted/40"
>
{/* Task cards */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{tasks.map((t, i) => (
<WorkPlanCard
key={t.id}
task={t}
index={i}
active={activeTaskId === t.id}
onActivate={() => setActiveTaskId(t.id)}
onDeactivate={() => setActiveTaskId((cur) => (cur === t.id ? null : cur))}
onOpen={() => setOpenTaskId(t.id)}
/>
))}
</div>
{/* Inline Gantt (desktop hover highlight) */}
<div className="mt-10">
<div className="mb-4 flex items-center gap-2 text-sm font-semibold uppercase tracking-widest text-muted-foreground">
<CalendarRange className="h-4 w-4 text-brand" />
Project timeline
</div>
<Gantt activeTaskId={activeTaskId} />
</div>
{/* Focused dialog (mobile click / any click) */}
<Dialog
open={openTaskId !== null}
onOpenChange={(o) => !o && setOpenTaskId(null)}
>
<DialogContent className="max-w-3xl">
{openTask && (
<>
<DialogHeader>
<div className="mb-1">
<Badge variant="brand">{openTask.id}</Badge>
</div>
<DialogTitle>{openTask.title}</DialogTitle>
<DialogDescription>{openTask.description}</DialogDescription>
</DialogHeader>
<div className="mt-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
Scheduled: month {openTask.startMonth} {openTask.endMonth}
</div>
<Gantt activeTaskId={openTask.id} compact />
</>
)}
</DialogContent>
</Dialog>
</Section>
)
}

@ -0,0 +1,73 @@
import { motion } from "framer-motion"
import { ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"
import type { Task } from "@/data/project"
interface WorkPlanCardProps {
task: Task
index: number
active: boolean
onActivate: () => void
onDeactivate: () => void
onOpen: () => void
}
export function WorkPlanCard({
task,
index,
active,
onActivate,
onDeactivate,
onOpen,
}: WorkPlanCardProps) {
return (
<motion.button
type="button"
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.4, delay: index * 0.05, ease: "easeOut" }}
onMouseEnter={onActivate}
onMouseLeave={onDeactivate}
onFocus={onActivate}
onBlur={onDeactivate}
onClick={onOpen}
aria-label={`${task.id}: ${task.title} — view timeline`}
className={cn(
"group flex h-full flex-col rounded-xl border bg-card p-5 text-left transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
active
? "-translate-y-1 border-brand/50 shadow-md"
: "border-border hover:-translate-y-1 hover:shadow-md"
)}
>
<div className="flex items-center justify-between">
<span
className={cn(
"flex h-9 w-9 items-center justify-center rounded-lg text-sm font-bold transition-colors",
active ? "bg-brand text-brand-foreground" : "bg-secondary text-primary"
)}
>
{task.id}
</span>
<ChevronRight
className={cn(
"h-5 w-5 transition-all",
active
? "translate-x-0.5 text-brand"
: "text-muted-foreground/40 group-hover:text-brand"
)}
/>
</div>
<h4 className="mt-4 font-semibold leading-snug text-foreground">
{task.title}
</h4>
<p className="mt-2 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
{task.description}
</p>
<span className="mt-4 text-xs font-medium uppercase tracking-wider text-muted-foreground/70">
Month {task.startMonth}{task.endMonth}
</span>
</motion.button>
)
}

@ -0,0 +1,34 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
brand: "border-transparent bg-brand/10 text-brand",
secondary: "border-transparent bg-secondary text-secondary-foreground",
outline: "text-foreground",
energy: "border-transparent bg-energy/10 text-energy",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
brand:
"bg-brand text-brand-foreground shadow-sm hover:bg-brand/90",
outline:
"border border-border bg-background hover:bg-secondary hover:text-secondary-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-secondary hover:text-secondary-foreground",
link: "text-brand underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-5 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-lg px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border border-border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-tight tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

@ -0,0 +1,116 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"animate-overlay fixed inset-0 z-50 bg-primary/40 backdrop-blur-sm",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"animate-content fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-background p-6 shadow-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
function DialogHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("flex flex-col space-y-1.5 text-left", className)}
{...props}
/>
)
}
function DialogFooter({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
}
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-snug tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

@ -0,0 +1,90 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = DialogPrimitive.Root
const SheetTrigger = DialogPrimitive.Trigger
const SheetClose = DialogPrimitive.Close
const SheetPortal = DialogPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"animate-overlay fixed inset-0 z-50 bg-primary/40 backdrop-blur-sm",
className
)}
{...props}
/>
))
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
const SheetContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"animate-sheet fixed inset-y-0 right-0 z-50 flex h-full w-3/4 max-w-sm flex-col gap-6 border-l border-border bg-background p-6 shadow-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring">
<X className="h-5 w-5" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = DialogPrimitive.Content.displayName
function SheetHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col space-y-2", className)} {...props} />
}
const SheetTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-base font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = DialogPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = DialogPrimitive.Description.displayName
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
}

@ -0,0 +1,254 @@
// Conteúdo canónico do projeto RESILIENCE.
// Fonte de verdade: input_files/Notas Site.docx + cronograma.png
// Ver specs/03-content.md. NÃO inventar conteúdo.
export const project = {
acronym: "RESILIENCE",
title:
"Residential Energy-Efficiency Strategies Incorporating Lifecycle Sustainability and Circular Economy",
tagline:
"A comprehensive, integrated framework to assess residential energy efficiency across its full lifecycle and circular economy.",
dates: {
start: "28-07-2025",
end: "26-07-2028",
},
projectCode: "2023.17768.ICDT | COMPETE2030-FEDER-00888800; operação n.º 15224",
doi: "https://doi.org/10.54499/2023.17768.ICDT",
funding: {
totalEligibleCost: "215.308,80 €",
euSupport: "183.012,48 €",
nationalSupport: "32.296,32 €",
},
team: [
"Álvaro Gomes",
"Carla Henriques",
"Eugénio Rodrigues",
"Marco Fernandes",
"Patrícia P. Silva",
"Adélio Gaspar",
"Humberto Jorge",
"Marcos Tenente",
"Sona Mammadzada",
"Luís Ventura",
],
} as const
export const goal = {
text: `This project aims to bridge existing gaps in energy efficiency assessment methodologies by adopting a comprehensive and integrated approach that expands the assessment to other lifecycle phases of EE technologies, incorporates circular economy principles, and provides decision-makers with the tools they need to select the portfolios of measures that will be targeted by the EE programs.`,
contributions: [
{
title: "Multiple Benefits of EE",
description:
"Captures the wider environmental, economic and social value of energy efficiency beyond energy savings.",
icon: "Layers",
},
{
title: "Hybrid Lifecycle Analysis",
description:
"Full impact coverage by combining process-based and input-output lifecycle assessment.",
icon: "Recycle",
},
{
title: "Circular Resource Assessment",
description:
"Embeds circular economy principles into the evaluation of efficiency measures.",
icon: "RefreshCw",
},
{
title: "Portfolio Optimization",
description:
"Optimizes EE investment strategies to select the best portfolios of measures.",
icon: "TrendingUp",
},
{
title: "Decision-makers' Preferences",
description:
"Integrates stakeholder preferences to support real-world policy decisions.",
icon: "Users",
},
],
} as const
// ── Work plan ───────────────────────────────────────────────────────────────
// Janela temporal: 36 meses, mês 1 = ago/2025 → mês 36 = jul/2028.
// startMonth/endMonth aproximados de cronograma.png (specs/03-content.md).
export type Task = {
id: string
title: string
startMonth: number
endMonth: number
}
export const tasks: Task[] = [
{
id: "T1",
title: "Review of EE technologies and policies",
startMonth: 1,
endMonth: 5,
},
{
id: "T2",
title: "Review of indicators",
startMonth: 1,
endMonth: 6,
},
{
id: "T3",
title: "Selection of indicators",
startMonth: 5,
endMonth: 9,
},
{
id: "T4",
title: "Energy performance simulation",
startMonth: 6,
endMonth: 17,
},
{
id: "T5",
title: "Hybrid lifecycle analysis",
startMonth: 13,
endMonth: 24,
},
{
id: "T6",
title: "Model for designing energy efficiency funding policies",
startMonth: 22,
endMonth: 31,
},
{
id: "T7",
title: "Sustainability benchmark certificate",
startMonth: 31,
endMonth: 36,
},
]
export type Milestone = { id: string; month: number }
// Posições aproximadas (refinar contra cronograma.png).
export const milestones: Milestone[] = [
{ id: "M1", month: 3 },
{ id: "M2", month: 4 },
{ id: "M3", month: 7 },
{ id: "M4", month: 9 },
{ id: "M5", month: 10 },
{ id: "M6", month: 16 },
{ id: "M7", month: 25 },
{ id: "M8", month: 33 },
{ id: "M9", month: 34 },
{ id: "M10", month: 35 },
]
export type Report = { label: string; month: number }
export const reports: Report[] = [
{ label: "1st Progress Report", month: 12 },
{ label: "2nd Progress Report", month: 24 },
{ label: "Final Report", month: 36 },
]
export const timeline = {
totalMonths: 36,
startYear: 2025,
startMonthOfYear: 8, // agosto
} as const
// ── Indicators ────────────────────────────────────────────────────────────
export type IndicatorType = "journal" | "conference" | "msc" | "phd"
export type Indicator = {
id: string
type: IndicatorType
title: string
meta?: string
authors?: string
venue?: string
year?: string
url?: string
}
export const indicators: Indicator[] = [
{
id: "j1",
type: "journal",
title:
"Integrating multiple impacts and lifecycle assessment in the evaluation of energy efficiency funding programs",
authors: "Tenente, M., Carvalho, T., Gomes, Á. et al.",
venue: "Energy Efficiency 18, 96",
year: "2025",
url: "https://doi.org/10.1007/s12053-025-10375-5",
},
{
id: "c1",
type: "conference",
title:
"A Critical Review of Policy and Multi-Criteria Indicators for Scaling Residential Building Resilience in Portugal's EPBD Implementation",
venue: "ECEEE Summer Study",
year: "2026",
},
{
id: "c2",
type: "conference",
title:
"Optimization of Energy Efficiency Renovations in Buildings: Unlocking Multiple Benefits Across Environmental, Economic, and Social Dimensions",
venue: "ECEEE Summer Study",
year: "2026",
},
{
id: "m1",
type: "msc",
title:
"Cidades Inteligentes, Sustentáveis e Resilientes: Papel das CER — Proposta de uma Taxonomia",
},
{
id: "m2",
type: "msc",
title:
"Disseminação de renováveis variáveis e da eletrificação numa rede de distribuição — análise simulada de impactos e o papel da demand response",
},
{
id: "m3",
type: "msc",
title:
"Reservas Estratégicas de Energia Elétrica em Portugal — Estimativa considerando a Incerteza e a Gestão da Procura",
},
]
// ── Partners (beneficiaries) ────────────────────────────────────────────────
export type Partner = { name: string; short: string; image: string; description: string }
export const partners: Partner[] = [
{
name: "INESC Coimbra",
short: "INESCC",
image: "/inescc.png",
description:
"Instituto de Engenharia de Sistemas e Computadores de Coimbra",
},
{
name: "ADAI",
short: "ADAI",
image: "/adai.png",
description: "Associação para o Desenvolvimento da Aerodinâmica Industrial",
},
{
name: "IPC ISCAC",
short: "ISCAC",
image: "/iscac.png",
description: "Instituto Politécnico de Coimbra ISCAC",
},
]
export const funders = [
{
name: "Portugal 2030",
image: "/Portugal2030.png",
},
{
name: "FCT",
image: "/FCT.png",
},
{
name: "União Europeia",
image: "/UE.png",
},
]

@ -0,0 +1,115 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* ── Design system tokens (INESCC institucional / moderno) ──────────────── */
:root {
--radius: 0.625rem;
--background: #ffffff;
--foreground: #0e1a26;
--card: #ffffff;
--card-foreground: #0e1a26;
--popover: #ffffff;
--popover-foreground: #0e1a26;
--primary: #0b2a4a; /* navy institucional */
--primary-foreground: #ffffff;
--secondary: #eef3f8;
--secondary-foreground: #0b2a4a;
--muted: #f5f7fa;
--muted-foreground: #5b6b7b;
--accent: #eef3f8;
--accent-foreground: #0b2a4a;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--border: #dce3eb;
--input: #dce3eb;
--ring: #1e6fd9;
/* Acentos de marca */
--brand: #1e6fd9; /* azul vivo / interação */
--brand-foreground: #ffffff;
--energy: #2ba66b; /* sustentabilidade */
}
@theme inline {
--font-sans: "Outfit", ui-sans-serif, system-ui, sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--color-energy: var(--energy);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/* ── Animations for Radix overlays (sem dependências extra) ─────────────── */
@keyframes overlay-in { from { opacity: 0 } to { opacity: 1 } }
@keyframes overlay-out { from { opacity: 1 } to { opacity: 0 } }
@keyframes content-in { from { opacity: 0; transform: translateY(8px) scale(0.98) } to { opacity: 1; transform: translateY(0) scale(1) } }
@keyframes content-out { from { opacity: 1 } to { opacity: 0 } }
@keyframes sheet-in { from { transform: translateX(100%) } to { transform: translateX(0) } }
@keyframes sheet-out { from { transform: translateX(0) } to { transform: translateX(100%) } }
[data-state="open"].animate-overlay { animation: overlay-in 0.2s ease-out }
[data-state="closed"].animate-overlay { animation: overlay-out 0.18s ease-in }
[data-state="open"].animate-content { animation: content-in 0.22s ease-out }
[data-state="closed"].animate-content { animation: content-out 0.18s ease-in }
[data-state="open"].animate-sheet { animation: sheet-in 0.28s cubic-bezier(0.32, 0.72, 0, 1) }
[data-state="closed"].animate-sheet { animation: sheet-out 0.24s ease-in }
@layer base {
* {
border-color: var(--color-border);
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
::selection {
background-color: color-mix(in srgb, var(--brand) 25%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}

@ -0,0 +1,20 @@
// Scroll suave para uma âncora de secção (compensado pelo header fixo via scroll-mt).
export function scrollToSection(id: string) {
const el = document.getElementById(id)
if (!el) return
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches
el.scrollIntoView({
behavior: prefersReduced ? "auto" : "smooth",
block: "start",
})
// Atualiza o hash sem dar o salto nativo do browser.
history.replaceState(null, "", `#${id}`)
}
export const navItems = [
{ id: "home", label: "Home" },
{ id: "work-plan", label: "Work plan" },
{ id: "indicators", label: "Indicators" },
] as const

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

@ -0,0 +1,14 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { RouterProvider } from "@tanstack/react-router"
import { router } from "@/router"
import "@/index.css"
const rootElement = document.getElementById("root")!
createRoot(rootElement).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)

@ -0,0 +1,25 @@
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router"
import App from "@/App"
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: App,
})
const routeTree = rootRoute.addChildren([indexRoute])
export const router = createRouter({ routeTree })
declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}

1
src/vite-env.d.ts vendored

@ -0,0 +1 @@
/// <reference types="vite/client" />

@ -0,0 +1,29 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

@ -0,0 +1,22 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,14 @@
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
Loading…
Cancel
Save