@@ -17,10 +17,9 @@ import {
1717} from '@/db/queries/quizzes/quiz' ;
1818import { getUserGlobalRank , getUserProfile } from '@/db/queries/users' ;
1919import { redirect } from '@/i18n/routing' ;
20- import { getAllSponsors , getSponsors } from '@/lib/about/github-sponsors' ;
21- import { computeAchievements } from '@/lib/achievements' ;
2220import { getCurrentUser } from '@/lib/auth' ;
23- import { checkHasStarredRepo , resolveGitHubLogin } from '@/lib/github-stars' ;
21+ import { computeAchievements } from '@/lib/achievements' ;
22+ import { getUserStatsForAchievements } from '@/lib/user-stats' ;
2423
2524export async function generateMetadata ( {
2625 params,
@@ -56,62 +55,16 @@ export default async function DashboardPage({
5655
5756 const t = await getTranslations ( 'dashboard' ) ;
5857
59- // Active sponsors — used for the sponsor badge / button display in the UI
60- const sponsors = await getSponsors ( ) ;
61- // All-time sponsors (active + past) — used for the Supporter achievement check
62- const allSponsors = await getAllSponsors ( ) ;
63-
64- const userEmail = user . email . toLowerCase ( ) ;
65- const userName = ( user . name ?? '' ) . toLowerCase ( ) ;
66- const userImage = user . image ?? '' ;
67-
68- function findSponsor ( list : typeof sponsors ) {
69- return list . find ( s => {
70- if ( s . email && s . email . toLowerCase ( ) === userEmail ) return true ;
71- if ( userName && s . login && s . login . toLowerCase ( ) === userName )
72- return true ;
73- if ( userName && s . name && s . name . toLowerCase ( ) === userName ) return true ;
74- if (
75- userImage &&
76- s . avatarUrl &&
77- s . avatarUrl . trim ( ) . length > 0 &&
78- userImage . includes ( s . avatarUrl . split ( '?' ) [ 0 ] )
79- )
80- return true ;
81- return false ;
82- } ) ;
83- }
84-
85- const matchedSponsor = findSponsor ( sponsors ) ; // active — for UI display
86- const everSponsor = findSponsor ( allSponsors ) ; // all-time — for achievements
87-
88- // Determine the GitHub login to check against the stargazers list.
89- // Priority:
90- // 1. Matched sponsor login (most reliable — org PAT already resolved it)
91- // 2. For GitHub-OAuth users: resolve login from numeric providerId
92- // 3. user.name as last resort (may be a display name, not a login!)
93- let githubLogin = matchedSponsor ?. login || '' ;
94- if ( ! githubLogin && user . provider === 'github' && user . providerId ) {
95- githubLogin =
96- ( await resolveGitHubLogin ( user . providerId ) ) ?? user . name ?? '' ;
97- } else if ( ! githubLogin ) {
98- githubLogin = user . name ?? '' ;
99- }
100-
101- const hasStarredRepo = githubLogin
102- ? await checkHasStarredRepo ( githubLogin )
103- : false ;
104-
10558 const attempts = await getUserQuizStats ( session . id ) ;
10659 const lastAttempts = await getUserLastAttemptPerQuiz ( session . id , locale ) ;
10760
10861 const totalAttempts = attempts . length ;
10962
11063 const averageScore =
111- totalAttempts > 0
64+ lastAttempts . length > 0
11265 ? Math . round (
113- attempts . reduce ( ( acc , curr ) => acc + Number ( curr . percentage ) , 0 ) /
114- totalAttempts
66+ lastAttempts . reduce ( ( acc , curr ) => acc + Number ( curr . percentage ) , 0 ) /
67+ lastAttempts . length
11568 )
11669 : 0 ;
11770
@@ -204,34 +157,16 @@ export default async function DashboardPage({
204157 trendPercentage,
205158 } ;
206159
207- const perfectScores = attempts . filter (
208- a => Number ( a . percentage ) === 100
209- ) . length ;
210- const highScores = attempts . filter ( a => Number ( a . percentage ) >= 90 ) . length ;
211- const uniqueQuizzes = lastAttempts . length ;
160+ const userStats = await getUserStatsForAchievements ( session . id ) ;
161+ const achievements = userStats ? computeAchievements ( userStats ) : [ ] ;
212162
213- const hasNightOwl = attempts . some ( a => {
214- if ( ! a . completedAt ) return false ;
215- const hour = new Date ( a . completedAt ) . getHours ( ) ;
216- return hour >= 0 && hour < 5 ;
217- } ) ;
218-
219- const achievements = computeAchievements ( {
220- totalAttempts,
221- averageScore,
222- perfectScores,
223- highScores,
224- isSponsor : ! ! everSponsor ,
225- uniqueQuizzes,
226- totalPoints : user . points ,
227- topLeaderboard : false ,
228- hasStarredRepo,
229- sponsorCount : matchedSponsor ? 1 : 0 ,
230- hasNightOwl,
231- } ) ;
163+ const isMatchedSponsor = userStats ? userStats . sponsorCount > 0 : false ;
232164
233165 const outlineBtnStyles =
234- 'inline-flex items-center justify-center rounded-full border border-gray-200 dark:border-white/10 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm px-6 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 transition-colors hover:bg-white hover:text-(--accent-primary) dark:hover:bg-neutral-800 dark:hover:text-(--accent-primary)' ;
166+ 'inline-flex items-center justify-center rounded-full border border-gray-200/50 bg-white/10 px-6 py-2.5 text-sm font-semibold tracking-wide text-gray-700 backdrop-blur-md transition-all hover:-translate-y-0.5 hover:bg-white/20 hover:shadow-md hover:border-gray-300 dark:border-white/10 dark:bg-neutral-900/40 dark:text-gray-200 dark:hover:bg-neutral-800/80 dark:hover:border-white/20' ;
167+
168+ const sponsorBtnStyles =
169+ 'group relative inline-flex items-center justify-center gap-2 rounded-full border border-(--accent-primary)/30 bg-(--accent-primary)/10 px-6 py-2.5 text-sm font-semibold tracking-wide text-(--accent-primary) backdrop-blur-md transition-all hover:-translate-y-0.5 hover:bg-(--accent-primary)/20 hover:shadow-[0_4px_12px_rgba(var(--accent-primary-rgb),0.2)] hover:border-(--accent-primary)/50 dark:border-(--accent-primary)/20 dark:bg-(--accent-primary)/5 dark:hover:bg-(--accent-primary)/20 dark:hover:border-(--accent-primary)/40 dark:hover:shadow-[0_4px_15px_rgba(var(--accent-primary-rgb),0.3)] overflow-hidden' ;
235170
236171 return (
237172 < div className = "min-h-screen" >
@@ -253,19 +188,22 @@ export default async function DashboardPage({
253188 href = "#feedback"
254189 className = { `group flex items-center gap-2 ${ outlineBtnStyles } ` }
255190 >
256- < MessageSquare className = "h-4 w-4 transition-transform group-hover:-translate-y-0.5" />
191+ < MessageSquare className = "h-4 w-4 transition-transform group-hover:-translate-y-0.5 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white " />
257192 { t ( 'supportLink' ) }
258193 </ a >
259194 < a
260195 href = "https://github.com/sponsors/DevLoversTeam"
261196 target = "_blank"
262197 rel = "noopener noreferrer"
263- className = "group inline-flex items-center justify-center gap-2 rounded-full border border-(--accent-primary) bg-(--accent-primary)/10 px-6 py-2 text-sm font-medium text-(--accent-primary) transition-colors hover:bg-(--accent-primary) hover:text-white dark:border-(--accent-primary)/50 dark:bg-(--accent-primary)/10 dark:text-(--accent-primary) dark:hover:bg-(--accent-primary) dark:hover:text-white"
198+ className = { sponsorBtnStyles }
264199 >
265- < Heart className = "h-4 w-4 transition-transform group-hover:scale-110" />
266- { ! ! matchedSponsor
267- ? t ( 'profile.supportAgain' )
268- : t ( 'profile.becomeSponsor' ) }
200+ { /* Subtle gradient glow background effect */ }
201+ < div className = "absolute inset-0 z-0 bg-linear-to-r from-transparent via-(--accent-primary)/10 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
202+
203+ < span className = "relative z-10 flex items-center gap-2" >
204+ < Heart className = "h-4 w-4 transition-transform group-hover:scale-110 group-hover:fill-(--accent-primary)/20" />
205+ { isMatchedSponsor ? t ( 'profile.supportAgain' ) : t ( 'profile.becomeSponsor' ) }
206+ </ span >
269207 </ a >
270208 </ div >
271209 </ header >
@@ -274,17 +212,13 @@ export default async function DashboardPage({
274212 < ProfileCard
275213 user = { userForDisplay }
276214 locale = { locale }
277- isSponsor = { ! ! matchedSponsor }
215+ isSponsor = { isMatchedSponsor }
278216 totalAttempts = { totalAttempts }
279217 globalRank = { globalRank }
280218 />
281- < div id = "stats" className = "grid scroll-mt-8 gap-8 lg:grid-cols-2" >
282- < StatsCard stats = { stats } attempts = { attempts } />
283- < ActivityHeatmapCard
284- attempts = { attempts }
285- locale = { locale }
286- currentStreak = { currentStreak }
287- />
219+ < div className = "grid gap-8 lg:grid-cols-2" >
220+ < StatsCard stats = { stats } attempts = { lastAttempts } />
221+ < ActivityHeatmapCard attempts = { attempts } locale = { locale } currentStreak = { currentStreak } />
288222 </ div >
289223 </ div >
290224 < div className = "mt-8" >
0 commit comments