Skip to content

Commit 2d07c50

Browse files
committed
feat: polish github goal progress ui
1 parent e35ad73 commit 2d07c50

File tree

6 files changed

+114
-35
lines changed

6 files changed

+114
-35
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1682,10 +1682,13 @@
16821682
},
16831683
"goal": {
16841684
"currentGoal": "Current Goal",
1685+
"progress": "Progress",
16851686
"of": "of",
16861687
"remaining": "Remaining",
16871688
"contribute": "Contribute",
16881689
"completed": "Completed",
1689-
"pending": "In Progress"
1690+
"pending": "In Progress",
1691+
"githubStarsUnit": "stars",
1692+
"githubProgress": "Star progress"
16901693
}
16911694
}

dashboard/public/statics/locales/fa.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1661,10 +1661,13 @@
16611661
},
16621662
"goal": {
16631663
"currentGoal": "هدف فعلی",
1664+
"progress": "پیشرفت",
16641665
"of": "از",
16651666
"remaining": "باقیمانده",
16661667
"contribute": "مشارکت کنید",
16671668
"completed": "تکمیل شده",
1668-
"pending": "در حال انجام"
1669+
"pending": "در حال انجام",
1670+
"githubStarsUnit": "ستاره",
1671+
"githubProgress": "پیشرفت ستاره‌ها"
16691672
}
16701673
}

dashboard/public/statics/locales/ru.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1664,11 +1664,14 @@
16641664
},
16651665
"goal": {
16661666
"currentGoal": "Текущая цель",
1667+
"progress": "Прогресс",
16671668
"of": "из",
16681669
"remaining": "Осталось",
16691670
"contribute": "Поддержать",
16701671
"completed": "Завершено",
1671-
"pending": "В процессе"
1672+
"pending": "В процессе",
1673+
"githubStarsUnit": "звёзд",
1674+
"githubProgress": "Прогресс по звёздам"
16721675
},
16731676
"validation": {
16741677
"generic": "{{field}} недействительно",

dashboard/public/statics/locales/zh.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1703,11 +1703,14 @@
17031703
},
17041704
"goal": {
17051705
"currentGoal": "当前目标",
1706+
"progress": "进度",
17061707
"of": "",
17071708
"remaining": "剩余",
17081709
"contribute": "贡献",
17091710
"completed": "已完成",
1710-
"pending": "进行中"
1711+
"pending": "进行中",
1712+
"githubStarsUnit": "星标",
1713+
"githubProgress": "星标进度"
17111714
},
17121715
"validation": {
17131716
"generic": "{{field}} 无效",

dashboard/src/components/goal-progress.tsx

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Card, CardContent } from '@/components/ui/card'
33
import { Button } from '@/components/ui/button'
44
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
55
import { useAllGoals } from '@/hooks/use-goal'
6-
import { Target, TrendingUp, Heart } from 'lucide-react'
6+
import { Target, TrendingUp, Heart, Star, Github } from 'lucide-react'
77
import { useTranslation } from 'react-i18next'
88
import { Skeleton } from '@/components/ui/skeleton'
99
import { cn } from '@/lib/utils'
@@ -136,12 +136,48 @@ export function GoalProgress() {
136136
}
137137

138138
const currentGoal = pendingGoals[currentGoalIndex]
139-
const progress = Math.min((currentGoal.paid_amount / currentGoal.price) * 100, 100)
140-
const remaining = Math.max(currentGoal.price - currentGoal.paid_amount, 0)
139+
const isGithubGoal = currentGoal.type === 'github_stars'
140+
const unitLabel = isGithubGoal ? t('goal.githubStarsUnit', { defaultValue: 'stars' }) : ''
141+
const goalTarget = currentGoal.price || 0
142+
const goalCurrent = currentGoal.paid_amount || 0
143+
const progress = Math.min(goalTarget > 0 ? (goalCurrent / goalTarget) * 100 : 0, 100)
144+
const remaining = Math.max(goalTarget - goalCurrent, 0)
145+
const formattedCurrent = isGithubGoal
146+
? `${Math.round(goalCurrent).toLocaleString()} ${unitLabel}`
147+
: `$${goalCurrent.toLocaleString()}`
148+
const formattedTarget = isGithubGoal
149+
? `${Math.round(goalTarget).toLocaleString()} ${unitLabel}`
150+
: `$${goalTarget.toLocaleString()}`
151+
const formattedRemaining = isGithubGoal
152+
? `${Math.max(Math.round(remaining), 0).toLocaleString()} ${unitLabel}`
153+
: `$${remaining.toLocaleString()}`
154+
const progressLabel = isGithubGoal
155+
? t('goal.githubProgress', { defaultValue: 'Star progress' })
156+
: t('goal.progress', { defaultValue: 'Progress' })
157+
const remainingLabel = t('goal.remaining')
158+
const ctaLabel = isGithubGoal
159+
? t('goal.starOnGitHub', { defaultValue: 'Star on GitHub' })
160+
: t('goal.contribute')
161+
const ctaHref =
162+
isGithubGoal && currentGoal.repo_owner && currentGoal.repo_name
163+
? `https://github.com/${currentGoal.repo_owner}/${currentGoal.repo_name}`
164+
: 'https://donate.pasarguard.org'
165+
const CtaIcon = isGithubGoal ? Star : Target
166+
const BadgeIcon = isGithubGoal ? Star : TrendingUp
167+
const badgeClasses = isGithubGoal
168+
? 'bg-amber-500/20 text-amber-700 dark:text-amber-400'
169+
: 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-400'
170+
const repoInfoAvailable = Boolean(isGithubGoal && currentGoal.repo_owner && currentGoal.repo_name)
171+
const repoIdentifier =
172+
currentGoal.repo_owner && currentGoal.repo_name
173+
? `${currentGoal.repo_owner}/${currentGoal.repo_name}`
174+
: 'owner/repo'
175+
const showRemaining = remaining > 0
141176

142177
// Collapsed state (desktop only) - simple donate button with popover
143178
// On mobile, always use expanded UI since there's no collapsed sidebar concept
144179
if (state === 'collapsed' && !isMobile) {
180+
const SummaryIcon = isGithubGoal ? Star : Heart
145181
return (
146182
<div className="mx-2 mb-2">
147183
<Popover>
@@ -151,48 +187,52 @@ export function GoalProgress() {
151187
size="icon"
152188
className="h-8 w-8 rounded-md"
153189
>
154-
<Heart className="h-4 w-4 text-primary" />
190+
<SummaryIcon className="h-4 w-4 text-primary" />
155191
</Button>
156192
</PopoverTrigger>
157193
<PopoverContent className="w-80 p-4" side="right" align="start">
158194
<div className="space-y-3">
159195
<div className="flex items-center gap-2">
160-
<Heart className="h-4 w-4 text-primary" />
196+
<SummaryIcon className="h-4 w-4 text-primary" />
161197
<span className="font-semibold text-sm">{currentGoal.name}</span>
162198
</div>
163199

164200
<div className="space-y-2">
165201
<div className="flex items-center justify-between text-xs">
166-
<span className="text-muted-foreground">Progress</span>
202+
<span className="text-muted-foreground">{progressLabel}</span>
167203
<span className="font-medium">{progress.toFixed(0)}%</span>
168204
</div>
169205
<Progress value={progress} className="h-2" />
170206
<div className="flex items-center justify-between text-xs">
171-
<span className="font-medium text-primary">${currentGoal.paid_amount.toLocaleString()}</span>
207+
<span className="font-medium text-primary">{formattedCurrent}</span>
172208
<span className="text-muted-foreground">
173-
{t('goal.of')} ${currentGoal.price.toLocaleString()}
209+
{t('goal.of')} {formattedTarget}
174210
</span>
175211
</div>
176212
</div>
177213

178-
{remaining > 0 && (
179-
<div className="rounded-md bg-muted/50 px-3 py-2">
180-
<div className="flex items-center justify-between text-xs">
181-
<span className="text-muted-foreground">{t('goal.remaining')}</span>
182-
<span className="font-semibold">${remaining.toLocaleString()}</span>
183-
</div>
214+
<div className="min-h-[32px]">
215+
<div
216+
className={cn(
217+
'flex items-center justify-between rounded-md px-3 py-2 text-xs transition-opacity',
218+
showRemaining ? 'bg-muted/50 opacity-100' : 'opacity-0'
219+
)}
220+
aria-hidden={!showRemaining}
221+
>
222+
<span className="text-muted-foreground">{remainingLabel}</span>
223+
<span className="font-semibold">{formattedRemaining}</span>
184224
</div>
185-
)}
225+
</div>
186226

187227
<Button asChild className="w-full">
188228
<a
189-
href="https://donate.pasarguard.org"
229+
href={ctaHref}
190230
target="_blank"
191231
rel="noopener noreferrer"
192232
className="flex items-center justify-center gap-2"
193233
>
194-
<Target className="h-4 w-4" />
195-
{t('goal.contribute')}
234+
<CtaIcon className="h-4 w-4" />
235+
{ctaLabel}
196236
</a>
197237
</Button>
198238
</div>
@@ -230,10 +270,22 @@ export function GoalProgress() {
230270
{t('goal.currentGoal')} ({currentGoalIndex + 1}/{pendingGoals.length})
231271
</span>
232272
<span className="line-clamp-1 text-sm font-semibold leading-tight">{currentGoal.name}</span>
273+
<div className="mt-1 h-4">
274+
<div
275+
className={cn(
276+
'flex items-center gap-1 text-[11px] text-muted-foreground transition-opacity',
277+
repoInfoAvailable ? 'opacity-100' : 'opacity-0'
278+
)}
279+
aria-hidden={!repoInfoAvailable}
280+
>
281+
<Github className="h-3 w-3" aria-hidden />
282+
<span className="truncate">{repoIdentifier}</span>
283+
</div>
284+
</div>
233285
</div>
234286
</div>
235-
<div className="flex items-center gap-1 rounded-full bg-amber-500/20 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-400">
236-
<TrendingUp className="h-3 w-3" />
287+
<div className={cn('flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium', badgeClasses)}>
288+
<BadgeIcon className="h-3 w-3" />
237289
{progress.toFixed(0)}%
238290
</div>
239291
</div>
@@ -242,33 +294,43 @@ export function GoalProgress() {
242294
<div className="space-y-1">
243295
<Progress value={progress} className="h-2" />
244296
<div className="flex items-center justify-between text-xs">
245-
<span className="font-medium text-primary">${currentGoal.paid_amount.toLocaleString()}</span>
297+
<span className="font-medium text-primary">{formattedCurrent}</span>
246298
<span className="text-muted-foreground">
247-
{t('goal.of')} ${currentGoal.price.toLocaleString()}
299+
{t('goal.of')} {formattedTarget}
248300
</span>
249301
</div>
250302
</div>
251303

252304
{/* Details */}
253-
{currentGoal.detail && <p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">{currentGoal.detail}</p>}
305+
<div className="min-h-[38px]">
306+
{currentGoal.detail && (
307+
<p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">{currentGoal.detail}</p>
308+
)}
309+
</div>
254310

255311
{/* Remaining */}
256-
{remaining > 0 && (
257-
<div className="flex items-center justify-between rounded-md bg-background/50 px-2 py-1.5">
258-
<span className="text-xs font-medium text-muted-foreground">{t('goal.remaining')}</span>
259-
<span className="text-xs font-semibold text-foreground">${remaining.toLocaleString()}</span>
312+
<div className="min-h-[32px]">
313+
<div
314+
className={cn(
315+
'flex items-center justify-between rounded-md px-2 py-1.5 text-xs transition-opacity',
316+
showRemaining ? 'bg-background/50 opacity-100' : 'opacity-0'
317+
)}
318+
aria-hidden={!showRemaining}
319+
>
320+
<span className="font-medium text-muted-foreground">{remainingLabel}</span>
321+
<span className="font-semibold text-foreground">{formattedRemaining}</span>
260322
</div>
261-
)}
323+
</div>
262324

263325
{/* CTA Button */}
264326
<a
265-
href="https://donate.pasarguard.org"
327+
href={ctaHref}
266328
target="_blank"
267329
rel="noopener noreferrer"
268330
className="flex w-full items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-xs font-semibold text-primary-foreground transition-all hover:bg-primary/90 hover:shadow-md"
269331
>
270-
<Target className="h-3.5 w-3.5" />
271-
{t('goal.contribute')}
332+
<CtaIcon className="h-3.5 w-3.5" />
333+
{ctaLabel}
272334
</a>
273335
</div>
274336
</CardContent>

dashboard/src/hooks/use-goal.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { useQuery } from '@tanstack/react-query'
22
import { $fetch } from '@/service/http'
33

4+
type GoalType = 'donation' | 'github_stars' | (string & {})
5+
46
interface Goal {
57
id: number
68
name: string
79
detail: string
810
price: number
911
paid_amount: number
1012
status: 'pending' | 'completed' | 'cancelled'
13+
type: GoalType
14+
repo_owner?: string
15+
repo_name?: string
1116
created_at: string
1217
updated_at: string
1318
}

0 commit comments

Comments
 (0)