diff --git a/src/app/(dashboards)/ranks/page.js b/src/app/(dashboards)/ranks/page.js
index 74b9967..f7e27c4 100644
--- a/src/app/(dashboards)/ranks/page.js
+++ b/src/app/(dashboards)/ranks/page.js
@@ -3,7 +3,7 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { createClient } from '@/utils/supabase/client';
-import { Trophy, Medal, Award, Briefcase, CalendarPlus } from 'lucide-react';
+import { Trophy, Medal, Award, Briefcase, CalendarPlus, X } from 'lucide-react';
import RankSidebar from '@/components/View/RankSidebar';
@@ -14,11 +14,9 @@ export default function RankingPage() {
const [currentUserId, setCurrentUserId] = useState(null);
const [loading, setLoading] = useState(true);
- // Filtros
+ // Estado para controlar qual modal de ranking completo está aberto: 'works' | 'meetings' | null
+ const [activeModal, setActiveModal] = useState(null);
const [periodFilter, setPeriodFilter] = useState('all');
-
- // Novo estado para alternar entre Trabalhos e Agendamentos
- const [rankingType, setRankingType] = useState('works'); // 'works' | 'meetings'
useEffect(() => {
async function loadRankingData() {
@@ -32,7 +30,6 @@ export default function RankingPage() {
if (userData) setCurrentUserId(userData.id);
}
- // Busca os Trabalhos (Work) e os Agendamentos (Meeting) onde ele é o 'creator'
const { data: usersData, error } = await supabase
.from('User')
.select(`
@@ -56,34 +53,42 @@ export default function RankingPage() {
loadRankingData();
}, [periodFilter]);
- // Processa, filtra e ordena os dados SEMPRE que a aba (rankingType) mudar, sem precisar chamar o banco de novo
- const rankings = useMemo(() => {
+ // 1. Processa o Ranking de TRABALHOS (AGORA SEM CORTE DE QUANTIDADE)
+ const worksRanking = useMemo(() => {
return allUsersData.map(user => {
- const worksCount = user.Work ? user.Work.length : 0;
- // Caso o Supabase exija o nome da Foreign Key, o retorno pode vir aninhado diferente.
- // Mas por padrão, se só houver uma relação, "Meeting.length" funciona perfeitamente.
- const meetingsCount = user.Meeting ? user.Meeting.length : 0;
-
- // Define qual é a pontuação atual baseada na aba selecionada
- const currentScore = rankingType === 'works' ? worksCount : meetingsCount;
-
- // Cálculo de Level fictício: A cada 2 ações, sobe 1 level (começa no 1)
- const calculatedLevel = Math.floor(currentScore / 2) + 1;
+ const score = user.Work ? user.Work.length : 0;
+ const level = Math.floor(score / 2) + 1;
+ return {
+ id: user.id,
+ name: `${user.user_name || ''} ${user.last_name || ''}`.trim(),
+ score,
+ level
+ };
+ })
+ .filter(user => user.score > 0)
+ .sort((a, b) => b.score - a.score); // Sem o .slice(), mantemos a lista inteira
+ }, [allUsersData]);
+ // 2. Processa o Ranking de AGENDAMENTOS (AGORA SEM CORTE DE QUANTIDADE)
+ const meetingsRanking = useMemo(() => {
+ return allUsersData.map(user => {
+ const score = user.Meeting ? user.Meeting.length : 0;
+ const level = Math.floor(score / 2) + 1;
return {
id: user.id,
name: `${user.user_name || ''} ${user.last_name || ''}`.trim(),
- score: currentScore,
- level: calculatedLevel
+ score,
+ level
};
})
- .filter(user => user.score > 0) // Esconde quem tem 0 na categoria selecionada
- .sort((a, b) => b.score - a.score) // Ordena do maior pro menor
- .slice(0, 50); // Top 50
- }, [allUsersData, rankingType]);
+ .filter(user => user.score > 0)
+ .sort((a, b) => b.score - a.score);
+ }, [allUsersData]);
const handleViewProfile = (userId) => {
router.push(`/profile/${userId}`);
+ // Opcional: fechar o modal ao navegar para o perfil
+ setActiveModal(null);
};
const getRankStyles = (index) => {
@@ -95,17 +100,81 @@ export default function RankingPage() {
}
};
+ // Função de renderização da lista (usada tanto na tela principal quanto no modal)
+ const renderRankingList = (rankingData, emptyMessage, labelScore) => {
+ if (rankingData.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Rank
+
Membro
+
Nível
+
{labelScore}
+
+
+ {rankingData.map((user, index) => {
+ const rankStyle = getRankStyles(index);
+ const isMe = currentUserId === user.id;
+
+ return (
+
handleViewProfile(user.id)}
+ className={`flex items-center p-4 rounded-xl border ${rankStyle.border} ${rankStyle.bg} hover:bg-gray-800 transition-all cursor-pointer relative overflow-hidden group`}
+ >
+ {isMe &&
}
+
+
+ {index < 3 ? rankStyle.icon : #{index + 1} }
+
+
+
+
+
+ {user.name?.charAt(0).toUpperCase() || '?'}
+
+
+
+
+ {user.name || 'Usuário Desconhecido'} {isMe && Você }
+
+
+
+
+
+
+
+
+ {user.score} {labelScore}
+
+
+
+ );
+ })}
+
+ );
+ };
+
return (
-
+
- {/* CABEÇALHO SUPERIOR */}
-
+
- Tabela de Classificação
+ Quadro de Líderes
@@ -125,103 +194,168 @@ export default function RankingPage() {
- {/* ABAS SELETORAS DE TIPO DE RANKING */}
-
- setRankingType('works')}
- className={`flex items-center gap-2 px-6 py-3 rounded-xl font-bold uppercase tracking-wide text-sm transition-all ${
- rankingType === 'works'
- ? 'bg-amber-500/20 text-amber-400 border border-amber-500/50'
- : 'bg-gray-900 text-gray-500 border border-transparent hover:bg-gray-800'
- }`}
- >
-
- Trabalhos
-
-
- setRankingType('meetings')}
- className={`flex items-center gap-2 px-6 py-3 rounded-xl font-bold uppercase tracking-wide text-sm transition-all ${
- rankingType === 'meetings'
- ? 'bg-amber-500/20 text-amber-400 border border-amber-500/50'
- : 'bg-gray-900 text-gray-500 border border-transparent hover:bg-gray-800'
- }`}
- >
-
- Agendamentos
-
-
-
{loading ? (
- ) : rankings.length === 0 ? (
-
-
-
- Nenhum jogador pontuou na categoria de {rankingType === 'works' ? 'Trabalhos' : 'Agendamentos'} .
-
-
) : (
-
- {/* CABEÇALHO DA TABELA */}
-
-
Rank
-
Membro
-
Nível
-
{rankingType === 'works' ? 'Trabalhos' : 'Agendamentos'}
-
+
+
+ {/* SEÇÃO 1: TRABALHOS (Exibe apenas Top 3) */}
+
+
+
+
+
+
+
Top Contribuidores
+
Baseado em Trabalhos Publicados
+
+
+
+ {/* Pega apenas do índice 0 até o 3 para a tela principal */}
+ {renderRankingList(worksRanking.slice(0, 3), "Nenhum jogador publicou trabalhos.", "Pubs")}
- {/* LISTA DE JOGADORES */}
- {rankings.map((user, index) => {
- const rankStyle = getRankStyles(index);
- const isMe = currentUserId === user.id;
+ {/* Botão para abrir o Modal de Trabalhos */}
+ {worksRanking.length > 3 && (
+ setActiveModal('works')}
+ className="w-full mt-4 py-3 border-2 border-dashed border-amber-500/30 text-amber-500 font-bold text-xs uppercase tracking-widest rounded-xl hover:bg-amber-500/10 hover:border-amber-400 transition-all"
+ >
+ + Ver ranking completo ({worksRanking.length} membros)
+
+ )}
+
+
+ {/* SEÇÃO 2: AGENDAMENTOS (Exibe apenas Top 3) */}
+
+
+
+
+
+
+
Top Organizadores
+
Baseado em Raids Agendadas
+
+
+
+ {/* Pega apenas do índice 0 até o 3 para a tela principal */}
+ {renderRankingList(meetingsRanking.slice(0, 3), "Nenhum jogador agendou missões.", "Raids")}
- return (
- handleViewProfile(user.id)}
- className={`flex items-center p-4 rounded-xl border ${rankStyle.border} ${rankStyle.bg} hover:bg-gray-800 transition-all cursor-pointer relative overflow-hidden group`}
+ {/* Botão para abrir o Modal de Agendamentos */}
+ {meetingsRanking.length > 3 && (
+
setActiveModal('meetings')}
+ className="w-full mt-4 py-3 border-2 border-dashed border-blue-500/30 text-blue-500 font-bold text-xs uppercase tracking-widest rounded-xl hover:bg-blue-500/10 hover:border-blue-400 transition-all"
>
- {isMe &&
}
+ + Ver ranking completo ({meetingsRanking.length} membros)
+
+ )}
+
+
+
+ )}
+
+
+ {/* MODAL DE RANKING COMPLETO */}
+ {/* MODAL DE RANKING COMPLETO */}
+ {activeModal && (() => {
+ // Descobre qual lista estamos exibindo no momento
+ const activeRankingList = activeModal === 'works' ? worksRanking : meetingsRanking;
+ const labelScore = activeModal === 'works' ? 'Pubs' : 'Raids';
+
+ // Encontra a posição do usuário logado na lista
+ const myRankIndex = activeRankingList.findIndex(u => u.id === currentUserId);
+ const myRankData = myRankIndex !== -1 ? activeRankingList[myRankIndex] : null;
-
- {index < 3 ? rankStyle.icon :
#{index + 1} }
+ return (
+
+ {/* Container Principal do Modal com flex-col para separar Header, Corpo e Rodapé */}
+
+
+ {/* 1. Header do Modal (Fixo no Topo) */}
+
+
+ {activeModal === 'works' ? (
+
+ ) : (
+
+ )}
+
+
+ Ranking Completo
+
+
+ {activeModal === 'works' ? 'Todos os contribuintes de trabalhos' : 'Todos os organizadores de raids'}
+
+
+
setActiveModal(null)}
+ className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
+ >
+
+
+
-
-
-
- {user.name?.charAt(0).toUpperCase() || '?'}
-
+ {/* 2. Corpo do Modal (Área Rolável) */}
+
+ {activeModal === 'works'
+ ? renderRankingList(worksRanking, "Nenhum jogador publicou trabalhos.", "Pubs")
+ : renderRankingList(meetingsRanking, "Nenhum jogador agendou missões.", "Raids")
+ }
+
+
+ {/* 3. Rodapé do Modal (Travado na Base com o Seu Rank) */}
+
+
+ Sua Posição Atual
+
+
+ {myRankData ? (
+
+
+
+ {myRankIndex < 3 ? getRankStyles(myRankIndex).icon : #{myRankIndex + 1} }
-
-
- {user.name || 'Usuário Desconhecido'} {isMe && Você }
-
+
+
+
+
+ {myRankData.name?.charAt(0).toUpperCase() || '?'}
+
+
+
+
+ {myRankData.name || 'Você'} Você
+
+
-
- {/* LEVEL CALCULADO */}
-
+
+
Lvl {myRankData.level}
+
- {/* CONTAGEM DINÂMICA */}
-
-
- {user.score}
{rankingType === 'works' ? 'Pubs' : 'Raids'}
+
+
+ {myRankData.score} {labelScore}
+
+
-
- );
- })}
+ ) : (
+
+
Você ainda não pontuou e não entrou neste ranking.
+
+ )}
+
+
- )}
-
+ );
+ })()}
+
);
}
\ No newline at end of file
diff --git a/src/app/actions.js b/src/app/actions.js
index 931ed5c..48ecb54 100644
--- a/src/app/actions.js
+++ b/src/app/actions.js
@@ -4,6 +4,7 @@ import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
import { meetingService } from '@/services/meeting.service'
+import { workService } from '@/services/work.service'
export async function authenticate(previousState, formData) {
const email = formData.get('email');
@@ -204,14 +205,14 @@ export async function publishWorkAction(prevState, formData) {
const fileName = `${Date.now()}_${safeFileName}`;
const { data: storageData, error: storageError } = await supabase.storage
- .from('works_archives') // O BUCKET DEVE ESTAR PÚBLICO NO SUPABASE
+ .from('trabalhos_arquivos') // O BUCKET DEVE ESTAR PÚBLICO NO SUPABASE
.upload(fileName, file);
if (storageError) throw storageError;
// Pegar a URL pública do arquivo
const { data: { publicUrl } } = supabase.storage
- .from('works_archives')
+ .from('trabalhos_arquivos')
.getPublicUrl(fileName);
// 3. Salvar no Banco de Dados usando o Service
@@ -255,13 +256,13 @@ export async function updateWorkAction(prevState, formData) {
const fileName = `${Date.now()}_${safeFileName}`;
const { error: storageError } = await supabase.storage
- .from('works_archives')
+ .from('trabalhos_arquivos')
.upload(fileName, file);
if (storageError) throw storageError;
const { data: { publicUrl } } = supabase.storage
- .from('works_archives')
+ .from('trabalhos_arquivos')
.getPublicUrl(fileName);
finalArchiveUrl = publicUrl;
diff --git a/src/proxy.js b/src/proxy.js
index 8376183..ef57327 100644
--- a/src/proxy.js
+++ b/src/proxy.js
@@ -54,6 +54,8 @@ export default async function proxy(req) {
export const config = {
matcher: [
"/home/:path*",
- "/groups/:path*"
+ "/groups/:path*",
+ "/works/:path*",
+ "/ranks/:path*"
],
};
\ No newline at end of file