diff --git a/src/app/roadmap/actions.ts b/src/app/roadmap/actions.ts new file mode 100644 index 0000000..2761f77 --- /dev/null +++ b/src/app/roadmap/actions.ts @@ -0,0 +1,38 @@ +'use server'; + +import fs from 'fs/promises'; +import path from 'path'; +import { revalidatePath } from 'next/cache'; +import { Feature } from '@/components/FeatureCard'; + +const DATA_PATH = path.join(process.cwd(), 'src', 'app', 'roadmap', 'data', 'roadmap.json'); + +export async function incrementVote(featureId: string): Promise { + try { + // Ler o arquivo JSON atual + const content = await fs.readFile(DATA_PATH, 'utf-8'); + const features: Feature[] = JSON.parse(content); + + // Encontrar e atualizar a feature + const featureIndex = features.findIndex(f => f.id === featureId); + if (featureIndex === -1) return null; + + // Incrementar o contador de votos + features[featureIndex] = { + ...features[featureIndex], + votesCount: features[featureIndex].votesCount + 1 + }; + + // Salvar o arquivo atualizado + await fs.writeFile(DATA_PATH, JSON.stringify(features, null, 2), 'utf-8'); + + // Revalidar a página para atualizar os dados + revalidatePath('/roadmap'); + + // Retornar a feature atualizada + return features[featureIndex]; + } catch (error) { + console.error('Erro ao atualizar voto:', error); + return null; + } +} \ No newline at end of file diff --git a/src/app/roadmap/data/roadmap.json b/src/app/roadmap/data/roadmap.json new file mode 100644 index 0000000..6c5a559 --- /dev/null +++ b/src/app/roadmap/data/roadmap.json @@ -0,0 +1,30 @@ +[ + { + "id": "feature-aprovada-abc", + "title": "Integração com Calendário Google", + "description": "Poder exportar nossos eventos para o Google Calendar com um clique.", + "status": "SUGGESTED", + "votesCount": 524 + }, + { + "id": "outra-sugestao-def", + "title": "Adicionar Tema Escuro", + "description": "Criar uma opção para alternar entre os modos claro e escuro no perfil.", + "status": "SUGGESTED", + "votesCount": 33 + }, + { + "id": "feature-em-progresso-123", + "title": "Exportar Relatórios em PDF", + "description": "Permitir gerar um PDF dos relatórios mensais diretamente da dashboard.", + "status": "IN_PROGRESS", + "votesCount": 399 + }, + { + "id": "feature-concluida-456", + "title": "Login Social com Google e GitHub", + "description": "Permitir que novos usuários se cadastrem e façam login usando suas contas sociais.", + "status": "DONE", + "votesCount": 3426 + } +] \ No newline at end of file diff --git a/src/app/roadmap/page.tsx b/src/app/roadmap/page.tsx new file mode 100644 index 0000000..2ea62a7 --- /dev/null +++ b/src/app/roadmap/page.tsx @@ -0,0 +1,92 @@ +import fs from 'fs/promises'; +import path from 'path'; +import RoadmapColumn from '@/components/RoadmapColumn'; +import { Feature } from '@/components/FeatureCard'; +import { FiTarget, FiClock, FiCheckCircle } from 'react-icons/fi'; +import Container from '@/components/Container'; + +const DATA_PATH = path.join(process.cwd(), 'src', 'app', 'roadmap', 'data', 'roadmap.json'); + +// Definição das colunas com um estilo mais refinado +const columns = [ + { + key: 'SUGGESTED', + title: 'Sugestões', + bg: 'bg-slate-50', + headerBg: 'bg-slate-100', + headerText: 'text-slate-800', + icon: , + borderColor: 'border-slate-200', + }, + { + key: 'IN_PROGRESS', + title: 'Estamos Cozinhando', + bg: 'bg-blue-50', + headerBg: 'bg-blue-100', + headerText: 'text-blue-800', + icon: , + borderColor: 'border-blue-200', + }, + { + key: 'DONE', + title: 'Tudo Pronto!', + bg: 'bg-green-50', + headerBg: 'bg-green-100', + headerText: 'text-green-800', + icon: , + borderColor: 'border-green-200', + }, +]; + +type FeaturesByStatus = { + [key: string]: Feature[]; +}; + +export default async function RoadmapPage() { + const file = await fs.readFile(DATA_PATH, 'utf-8'); + const features: Feature[] = JSON.parse(file); + + // Agrupando features de forma mais eficiente com `reduce` + const featuresByStatus = features.reduce((acc, feature) => { + if (!acc[feature.status]) { + acc[feature.status] = []; + } + acc[feature.status].push(feature); + return acc; + }, { SUGGESTED: [], IN_PROGRESS: [], DONE: [] }); + + return ( + +
+
+

+ Nosso Futuro +

+

+ O Diaum está sempre em evolução e queremos que você faça parte disso. Confira as funcionalidades que estão no forno, nossos próximos passos e vote nas suas favoritas. +

+
+ +
+ {columns.map(col => ( +
+
+ {col.icon} + + {col.title} + +
+
+ {/* Garante que a coluna será renderizada mesmo sem features */} + +
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/FeatureCard.tsx b/src/components/FeatureCard.tsx new file mode 100644 index 0000000..83e6394 --- /dev/null +++ b/src/components/FeatureCard.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { FaThumbsUp } from 'react-icons/fa'; +import { FiExternalLink } from 'react-icons/fi'; +import { incrementVote } from '@/app/roadmap/actions'; +import { useState, useTransition, useEffect } from 'react'; + +export type Feature = { + id: string; + title: string; + description: string; + status: 'SUGGESTED' | 'IN_PROGRESS' | 'DONE'; + votesCount: number; +}; + +export function FeatureCard({ feature: initialFeature }: { feature: Feature }) { + const [feature, setFeature] = useState(initialFeature); + const [isPending, startTransition] = useTransition(); + const [optimisticVotes, setOptimisticVotes] = useState(feature.votesCount); + const [hasVoted, setHasVoted] = useState(false); + + // Verificar se o usuário já votou nesta feature + useEffect(() => { + const votedFeatures = JSON.parse(localStorage.getItem('votedFeatures') || '[]'); + setHasVoted(votedFeatures.includes(feature.id)); + }, [feature.id]); + + const handleVote = () => { + // Verificar se já votou + if (hasVoted) return; + + // Salvar voto no localStorage + const votedFeatures = JSON.parse(localStorage.getItem('votedFeatures') || '[]'); + localStorage.setItem('votedFeatures', JSON.stringify([...votedFeatures, feature.id])); + setHasVoted(true); + + // Atualização otimista + setOptimisticVotes(prev => prev + 1); + + // Server Action + startTransition(async () => { + const updatedFeature = await incrementVote(feature.id); + if (updatedFeature) { + setFeature(updatedFeature); + } else { + // Reverter em caso de erro + setOptimisticVotes(feature.votesCount); + // Remover do localStorage em caso de erro + const votedFeatures = JSON.parse(localStorage.getItem('votedFeatures') || '[]'); + localStorage.setItem( + 'votedFeatures', + JSON.stringify(votedFeatures.filter((id: string) => id !== feature.id)) + ); + setHasVoted(false); + } + }); + }; + + return ( +
+
+
+
+

+ {feature.title} +

+
+

{feature.description}

+
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/RoadmapColumn.tsx b/src/components/RoadmapColumn.tsx new file mode 100644 index 0000000..d251231 --- /dev/null +++ b/src/components/RoadmapColumn.tsx @@ -0,0 +1,15 @@ +import { Feature, FeatureCard } from './FeatureCard'; + +type RoadmapColumnProps = { + features: Feature[]; +}; + +export default function RoadmapColumn({ features }: RoadmapColumnProps) { + return ( +
+ {features.map((feature) => ( + + ))} +
+ ); +} \ No newline at end of file