From e6609f1b535b31c9bd45a90f487052cd3daedec7 Mon Sep 17 00:00:00 2001 From: Zamba-Vitor Date: Fri, 1 May 2026 10:42:49 -0300 Subject: [PATCH 1/2] Ajustes no armazenamento dos arquivos --- src/app/actions.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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; From 381a12780716f1b286496b911aa1241c8e847d11 Mon Sep 17 00:00:00 2001 From: Zamba-Vitor Date: Sun, 3 May 2026 14:58:34 -0300 Subject: [PATCH 2/2] Ajustes estruturais na tela de ranqueamentos --- src/app/(dashboards)/ranks/page.js | 344 ++++++++++++++++++++--------- src/proxy.js | 4 +- 2 files changed, 242 insertions(+), 106 deletions(-) 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 ( +
+ +

{emptyMessage}

+
+ ); + } + + 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ê} +

+
+
+ +
+
Lvl {user.level}
+
+ +
+
+ {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 */} -
- - - -
- {loading ? (

Escanear Registros...

- ) : 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 && ( + + )} +
+ + {/* 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 && ( + + )} +
+ +
+ )} +
+ + {/* 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'} +

+
+ +
-
-
- - {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 {user.level}
-
+
+
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/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