diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c78926c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.git +.env* +.dockerignore +Dockerfile +README.md +.next +build +dist \ No newline at end of file diff --git a/.gitea/config/registry.yml b/.gitea/config/registry.yml new file mode 100644 index 0000000..bb192a2 --- /dev/null +++ b/.gitea/config/registry.yml @@ -0,0 +1,8 @@ +version: "1.0" +registries: + - name: seu-registry + host: seu-registry.com + type: container + credentials: + username: ${REGISTRY_USERNAME} + password: ${REGISTRY_PASSWORD} \ No newline at end of file diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..67485ca --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,58 @@ +name: Docker Build and Push + +on: + push: + branches: [ main ] + tags: [ 'v*' ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: seu-registry.com/historias-magicas + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Container Registry + uses: docker/login-action@v2 + with: + registry: seu-registry.com + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=seu-registry.com/historias-magicas:buildcache + cache-to: type=registry,ref=seu-registry.com/historias-magicas:buildcache,mode=max + + - name: Update Portainer stack + if: github.ref == 'refs/heads/main' + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + script: | + cd /opt/portainer + docker stack deploy -c portainer-stack.yml historias-magicas \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7d5a422 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,18 @@ +name: Deploy + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Deploy to production + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + run: | + echo "$DEPLOY_KEY" > deploy_key + chmod 600 deploy_key + ssh -i deploy_key user@seu-servidor.com 'cd /app && ./scripts/deploy.sh' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ca97815..47face3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,141 +5,43 @@ Todas as mudanças notáveis neste projeto serão documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/), e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). -## [0.1.0] - 2024-03-23 +## [0.2.0] - 2024-03-21 ### Adicionado -- Landing page para pais - - Design moderno e atraente - - Seções de benefícios e funcionalidades - - Demonstração de métricas e análises - - Gráficos interativos com recharts - - Gráfico de evolução do aluno - - Comparativo antes/depois - - Depoimentos de usuários - - CTAs estratégicos - - Demonstração interativa - - Copywriting focado em resultados - - Imagens otimizadas e responsivas - - Rota dedicada em /para-pais - -- Edge function `generate-story` para geração de histórias com IA - - Integração com OpenAI GPT para criação de texto - - Integração com DALL-E para geração de imagens - - Sistema de logs estruturados para monitoramento - - Tratamento robusto de erros e validações - -- Componente `StoryGenerator` para interface de criação - - Fluxo de seleção de categorias (tema, disciplina, personagem, cenário) - - Feedback visual do processo de geração - - Validações de campos obrigatórios - - Navegação automática entre etapas - - Tratamento de erros com feedback visual - -- Nova estrutura de dados para páginas de histórias - - Tabela `story_pages` para melhor organização - - Relacionamentos explícitos entre histórias e páginas - - Suporte a ordenação por número da página - -- Otimização de carregamento de imagens - - Lazy loading com placeholders - - Pré-carregamento da próxima imagem - - Cache de imagens no frontend - - Transformações de imagem no Supabase Storage - - Múltiplas resoluções de imagem - -- Sistema de cache de imagens no frontend - - Implementação de imageCache.ts - - Prevenção de recarregamento desnecessário - - Melhor performance em navegação - -### Modificado -- Reorganização da landing page para pais - - Reordenação das seções para melhor fluxo - - Hero → Por que escolher → Como Funciona → Análise → Diferença → Depoimentos → CTA - - Otimização da jornada do usuário - - Melhor hierarquia de informações - - Fluxo narrativo mais coerente - - Progressão lógica de informações - - Posicionamento estratégico do CTA - -- Reorganização da estrutura de arquivos - - Remoção da pasta /pages/story - - Consolidação dos componentes de história em /pages/student-dashboard - - Melhor organização hierárquica das rotas - -- Otimização global de imagens - - Conversão automática para WebP - - Redimensionamento otimizado por contexto - - Parâmetros de qualidade personalizados - - Função utilitária centralizada - - Implementação em todas as rotas - - Otimização contextual por uso - - Pré-carregamento otimizado - -- Otimização de imagens de capa - - Uso da primeira página como capa - - Tamanho reduzido para thumbnails - - Carregamento lazy para melhor performance -- Refatoração da interface de capa - - Tipagem específica para cover na interface Story - - Padronização do uso da primeira página como capa - - Otimização de queries para busca de capas - - Parâmetros de transformação para thumbnails - -- Padronização da interface de histórias - - Consistência visual entre dashboard e lista - - Cards de história com mesmo estilo e comportamento - - Melhor experiência do usuário na navegação - -- Atualização do schema do banco para suportar novas categorias - - Adição de tabelas para temas, disciplinas, personagens e cenários - - Relacionamentos entre histórias e categorias - - Índices para otimização de consultas +- Configuração Docker para ambiente de produção +- Pipeline de CI/CD no Gitea Actions +- Integração com Redis para cache +- Healthcheck da aplicação +- Adiciona seções: + - Hero com CTA e social proof + - Problemas e Soluções + - Como a Magia Acontece + - Comparação antes/depois + - Benefícios Mágicos em layout horizontal + - Testimoniais + - Planos e preços + - FAQ + - CTA final + - Footer ### Técnico -- Melhorias no tratamento de imagens - - Tratamento de URLs indefinidas - - Imagem padrão para fallback - - Otimização consistente em edge functions - - Melhor tipagem para URLs - - Prevenção de erros em runtime +- Dockerfile com multi-stage build para otimização +- Configuração de registry no Gitea +- Cache de histórias com Redis +- Scripts de deploy e monitoramento -- Implementação de logs estruturados com prefixos por contexto -- Validações de dados em múltiplas camadas -- Tratamento de respostas da IA com fallbacks -- Otimização de queries no banco de dados -- Feedback em tempo real do processo de geração -- Queries otimizadas para nova estrutura de dados -- Melhor tratamento de estados de loading e erro -- Implementação de componente ImageWithLoading -- Sistema de cache de imagens -- Otimização de URLs de imagem - -- Refatoração de componentes para melhor reuso - - Separação de lógica de carregamento de imagens - - Componentes mais modulares e reutilizáveis - - Melhor organização do código +### Modificado +- Atualização do next.config.js para suporte standalone +- Adaptação da API para usar Redis cache +- Configuração de redes Docker +- Otimiza UX/UI com: + - Animações suaves + - Gradientes modernos + - Layout responsivo + - Elementos interativos + - Social proof estratégico ### Segurança -- Validação de dados de entrada na edge function -- Verificação de permissões do usuário -- Sanitização de prompts para a IA -- Proteção contra dados sensíveis nos logs - -### Próximos Passos -- [ ] Implementar cache de respostas da IA -- [ ] Adicionar retry policy para falhas de geração -- [ ] Melhorar prompts para histórias mais educativas -- [ ] Adicionar métricas de uso e performance - -- Melhorias na página de demonstração - - Reorganização do layout e componentes - - Priorização do conteúdo da história - - Dashboard de métricas movido para baixo - - Fluxo interativo de demonstração - - Simulação de gravação e análise - - CTAs personalizados para escolas e pais - - Separação de dados mock - - Feedback visual aprimorado - - Estilos mais modernos e consistentes - - Melhor experiência de demonstração \ No newline at end of file +- Implementação de healthchecks +- Configuração de redes isoladas +- Proteção de variáveis de ambiente \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..04bf83c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM node:18-alpine AS builder + +WORKDIR /app + +# Adicionar dependência do Redis +RUN apk add --no-cache redis + +# Copiar arquivos de dependências +COPY package*.json ./ +COPY yarn.lock ./ + +# Instalar dependências +RUN yarn install --frozen-lockfile + +# Copiar código fonte +COPY . . + +# Build da aplicação +RUN yarn build + +# Production stage +FROM node:18-alpine AS runner + +WORKDIR /app + +# Adicionar dependência do Redis +RUN apk add --no-cache redis + +# Copiar arquivos necessários do builder +COPY --from=builder /app/next.config.js ./ +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# Expor porta +EXPOSE 3000 + +# Comando para rodar a aplicação +CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4ea0146 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + historias-magicas: + image: ${REGISTRY}/historias-magicas:${TAG} + environment: + - NODE_ENV=production + - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} + - NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - REDIS_URL=redis://redis:6379 + networks: + - network_public + deploy: + labels: + - "traefik.enable=true" + - "traefik.http.routers.historias-magicas.rule=Host(`${DOMAIN}`)" + - "traefik.http.routers.historias-magicas.entrypoints=websecure" + - "traefik.http.routers.historias-magicas.tls.certresolver=letsencrypt" + - "traefik.http.services.historias-magicas.loadbalancer.server.port=3000" + - "traefik.docker.network=traefik-public" + replicas: 1 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + +networks: + traefik-public: + external: true + redis-network: + external: true \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..73ea147 --- /dev/null +++ b/next.config.js @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + images: { + domains: [ + 'oaidalleapiprodscus.blob.core.windows.net', + // outros domínios necessários + ], + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package.json b/package.json index b433e48..af9399c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "preview": "vite preview", "typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "format": "prettier --write \"src/**/*.{ts,tsx}\"" + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "docker:build": "docker build -t historias-magicas .", + "docker:run": "docker run -p 3000:3000 historias-magicas", + "deploy:prod": "docker-compose up -d --build" }, "dependencies": { "@radix-ui/react-accordion": "^1.2.2", diff --git a/portainer-stack.yml b/portainer-stack.yml new file mode 100644 index 0000000..253a9c6 --- /dev/null +++ b/portainer-stack.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + historias-magicas: + image: ${REGISTRY}/historias-magicas:${TAG} + environment: + - NODE_ENV=production + - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} + - NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - REDIS_URL=redis://redis:6379 + networks: + - traefik-public + - redis-network + deploy: + labels: + - "traefik.enable=true" + - "traefik.http.routers.historias-magicas.rule=Host(`${DOMAIN}`)" + - "traefik.http.routers.historias-magicas.entrypoints=websecure" + - "traefik.http.routers.historias-magicas.tls.certresolver=letsencrypt" + - "traefik.http.services.historias-magicas.loadbalancer.server.port=3000" + - "traefik.docker.network=traefik-public" + replicas: 2 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + +networks: + traefik-public: + external: true + redis-network: + external: true \ No newline at end of file diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..ed4e910 --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,5 @@ +import { Redis } from 'ioredis'; + +const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379'); + +export default redis; \ No newline at end of file diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 0ce48b2..e1e561a 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,3 +1,4 @@ +import { StoryPrompt } from '@/types/story-generator' import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL @@ -13,4 +14,26 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, { persistSession: true, detectSessionInUrl: true } -}) \ No newline at end of file +}) + +export const generateStoryFunction = async (prompt: StoryPrompt) => { + const { data: { session } } = await supabase.auth.getSession() + + const response = await fetch( + 'https://seu-project-ref.supabase.co/functions/v1/generate-story', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session?.access_token}`, + }, + body: JSON.stringify(prompt), + } + ) + + if (!response.ok) { + throw new Error('Falha ao gerar história') + } + + return response.json() +} \ No newline at end of file diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts new file mode 100644 index 0000000..414fa4d --- /dev/null +++ b/src/pages/api/health.ts @@ -0,0 +1,17 @@ +import redis from '@/lib/redis'; + +export default async function handler(req, res) { + try { + // Verifica conexão com Redis + await redis.ping(); + + // Verifica conexão com Supabase + const { data, error } = await supabase.from('stories').select('id').limit(1); + + if (error) throw error; + + res.status(200).json({ status: 'healthy' }); + } catch (error) { + res.status(500).json({ status: 'unhealthy', error: error.message }); + } +} \ No newline at end of file diff --git a/src/pages/api/stories/[id].ts b/src/pages/api/stories/[id].ts new file mode 100644 index 0000000..26799c6 --- /dev/null +++ b/src/pages/api/stories/[id].ts @@ -0,0 +1,24 @@ +import redis from '@/lib/redis'; +import { supabase } from '@/lib/supabase'; + +export default async function handler(req, res) { + const { id } = req.query; + + // Tenta pegar do cache primeiro + const cachedStory = await redis.get(`story:${id}`); + if (cachedStory) { + return res.json(JSON.parse(cachedStory)); + } + + // Se não estiver no cache, busca do Supabase + const { data: story } = await supabase + .from('stories') + .select('*') + .eq('id', id) + .single(); + + // Salva no cache por 1 hora + await redis.setex(`story:${id}`, 3600, JSON.stringify(story)); + + return res.json(story); +} \ No newline at end of file diff --git a/src/pages/landing/EducationalForParents.tsx b/src/pages/landing/EducationalForParents.tsx new file mode 100644 index 0000000..cb3bae0 --- /dev/null +++ b/src/pages/landing/EducationalForParents.tsx @@ -0,0 +1,807 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ArrowRight, Wand2, Shield, Star, BookOpen, + Brain, Target, Users, Award, CheckCircle, + Clock, Heart, Sparkles, ScrollText, Lock, X, + Facebook, Instagram, Twitter, Youtube +} from 'lucide-react'; + +export function EducationalForParents(): JSX.Element { + const navigate = useNavigate(); + + return ( +
+ Histórias educativas personalizadas que encantam e ensinam, criadas especialmente + para o desenvolvimento único do seu filho. +
+ +
+ {challenge.description}
+{benefit.description}
+{step.description}
+{benefit.description}
+
+ "{testimonial.text}"
+{testimonial.name}
+{testimonial.role}
++ ✨ Momento mágico: {testimonial.magicMoment} +
++ Escolha o plano perfeito para a jornada mágica do seu filho +
+ +{plan.description}
++ Garantia mágica de 30 dias ou seu dinheiro de volta +
+{item.answer}
++ Transforme a educação do seu filho em uma aventura inesquecível +
+ ++ Oferta por tempo limitado! +
++ 7 dias grátis + Bônus especial de boas-vindas +
++ Garantia de 30 dias ou seu dinheiro de volta +
+