@@ -14,10 +14,9 @@ import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground
1414import { getUserLastAttemptPerQuiz , getUserQuizStats } from '@/db/queries/quizzes/quiz' ;
1515import { getUserProfile , getUserGlobalRank } from '@/db/queries/users' ;
1616import { redirect } from '@/i18n/routing' ;
17- import { getSponsors , getAllSponsors } from '@/lib/about/github-sponsors' ;
1817import { getCurrentUser } from '@/lib/auth' ;
1918import { computeAchievements } from '@/lib/achievements' ;
20- import { checkHasStarredRepo , resolveGitHubLogin } from '@/lib/github-stars ' ;
19+ import { getUserStatsForAchievements } from '@/lib/user-stats ' ;
2120
2221export async function generateMetadata ( {
2322 params,
@@ -53,59 +52,16 @@ export default async function DashboardPage({
5352
5453 const t = await getTranslations ( 'dashboard' ) ;
5554
56- // Active sponsors — used for the sponsor badge / button display in the UI
57- const sponsors = await getSponsors ( ) ;
58- // All-time sponsors (active + past) — used for the Supporter achievement check
59- const allSponsors = await getAllSponsors ( ) ;
60-
61- const userEmail = user . email . toLowerCase ( ) ;
62- const userName = ( user . name ?? '' ) . toLowerCase ( ) ;
63- const userImage = user . image ?? '' ;
64-
65- function findSponsor ( list : typeof sponsors ) {
66- return list . find ( s => {
67- if ( s . email && s . email . toLowerCase ( ) === userEmail ) return true ;
68- if ( userName && s . login && s . login . toLowerCase ( ) === userName ) return true ;
69- if ( userName && s . name && s . name . toLowerCase ( ) === userName ) return true ;
70- if (
71- userImage &&
72- s . avatarUrl &&
73- s . avatarUrl . trim ( ) . length > 0 &&
74- userImage . includes ( s . avatarUrl . split ( '?' ) [ 0 ] )
75- ) return true ;
76- return false ;
77- } ) ;
78- }
79-
80- const matchedSponsor = findSponsor ( sponsors ) ; // active — for UI display
81- const everSponsor = findSponsor ( allSponsors ) ; // all-time — for achievements
82-
83- // Determine the GitHub login to check against the stargazers list.
84- // Priority:
85- // 1. Matched sponsor login (most reliable — org PAT already resolved it)
86- // 2. For GitHub-OAuth users: resolve login from numeric providerId
87- // 3. user.name as last resort (may be a display name, not a login!)
88- let githubLogin = matchedSponsor ?. login || '' ;
89- if ( ! githubLogin && user . provider === 'github' && user . providerId ) {
90- githubLogin = ( await resolveGitHubLogin ( user . providerId ) ) ?? user . name ?? '' ;
91- } else if ( ! githubLogin ) {
92- githubLogin = user . name ?? '' ;
93- }
94-
95- const hasStarredRepo = githubLogin
96- ? await checkHasStarredRepo ( githubLogin )
97- : false ;
98-
9955 const attempts = await getUserQuizStats ( session . id ) ;
10056 const lastAttempts = await getUserLastAttemptPerQuiz ( session . id , locale ) ;
10157
10258 const totalAttempts = attempts . length ;
10359
10460 const averageScore =
105- totalAttempts > 0
61+ lastAttempts . length > 0
10662 ? Math . round (
107- attempts . reduce ( ( acc , curr ) => acc + Number ( curr . percentage ) , 0 ) /
108- totalAttempts
63+ lastAttempts . reduce ( ( acc , curr ) => acc + Number ( curr . percentage ) , 0 ) /
64+ lastAttempts . length
10965 )
11066 : 0 ;
11167
@@ -184,33 +140,16 @@ export default async function DashboardPage({
184140 trendPercentage,
185141 } ;
186142
187- const perfectScores = attempts . filter ( ( a ) => Number ( a . percentage ) === 100 ) . length ;
188- const highScores = attempts . filter ( ( a ) => Number ( a . percentage ) >= 90 ) . length ;
189- const uniqueQuizzes = lastAttempts . length ;
190-
191- // Night Owl: any attempt completed between 00:00 and 05:00 local time
192- const hasNightOwl = attempts . some ( ( a ) => {
193- if ( ! a . completedAt ) return false ;
194- const hour = new Date ( a . completedAt ) . getHours ( ) ;
195- return hour >= 0 && hour < 5 ;
196- } ) ;
143+ const userStats = await getUserStatsForAchievements ( session . id ) ;
144+ const achievements = userStats ? computeAchievements ( userStats ) : [ ] ;
197145
198- const achievements = computeAchievements ( {
199- totalAttempts,
200- averageScore,
201- perfectScores,
202- highScores,
203- isSponsor : ! ! everSponsor ,
204- uniqueQuizzes,
205- totalPoints : user . points ,
206- topLeaderboard : false ,
207- hasStarredRepo,
208- sponsorCount : matchedSponsor ? 1 : 0 , // TODO: wire to actual sponsorship history count
209- hasNightOwl,
210- } ) ;
146+ const isMatchedSponsor = userStats ? userStats . sponsorCount > 0 : false ;
211147
212148 const outlineBtnStyles =
213- '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)' ;
149+ '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' ;
150+
151+ const sponsorBtnStyles =
152+ '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' ;
214153
215154 return (
216155 < div className = "min-h-screen" >
@@ -234,17 +173,22 @@ export default async function DashboardPage({
234173 href = "#feedback"
235174 className = { `group flex items-center gap-2 ${ outlineBtnStyles } ` }
236175 >
237- < MessageSquare className = "h-4 w-4 transition-transform group-hover:-translate-y-0.5" />
176+ < 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 " />
238177 { t ( 'supportLink' ) }
239178 </ a >
240179 < a
241180 href = "https://github.com/sponsors/DevLoversTeam"
242181 target = "_blank"
243182 rel = "noopener noreferrer"
244- 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"
183+ className = { sponsorBtnStyles }
245184 >
246- < Heart className = "h-4 w-4 transition-transform group-hover:scale-110" />
247- { ! ! matchedSponsor ? t ( 'profile.supportAgain' ) : t ( 'profile.becomeSponsor' ) }
185+ { /* Subtle gradient glow background effect */ }
186+ < 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" />
187+
188+ < span className = "relative z-10 flex items-center gap-2" >
189+ < Heart className = "h-4 w-4 transition-transform group-hover:scale-110 group-hover:fill-(--accent-primary)/20" />
190+ { isMatchedSponsor ? t ( 'profile.supportAgain' ) : t ( 'profile.becomeSponsor' ) }
191+ </ span >
248192 </ a >
249193 </ div >
250194 </ header >
@@ -253,12 +197,12 @@ export default async function DashboardPage({
253197 < ProfileCard
254198 user = { userForDisplay }
255199 locale = { locale }
256- isSponsor = { ! ! matchedSponsor }
200+ isSponsor = { isMatchedSponsor }
257201 totalAttempts = { totalAttempts }
258202 globalRank = { globalRank }
259203 />
260204 < div className = "grid gap-8 lg:grid-cols-2" >
261- < StatsCard stats = { stats } attempts = { attempts } />
205+ < StatsCard stats = { stats } attempts = { lastAttempts } />
262206 < ActivityHeatmapCard attempts = { attempts } locale = { locale } currentStreak = { currentStreak } />
263207 </ div >
264208 </ div >
0 commit comments