Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bceb8c4
draft setup of mirage server, start unit testing hooks
solar464 Apr 7, 2026
3caf3b2
Merge branch 'main' of github.com:Progressive-Victory/the-website int…
solar464 Apr 24, 2026
d585de2
Dev - Main (Again) (#405)
May 5, 2026
418cc23
unit tests for hooks
solar464 May 9, 2026
d1fc8be
Merge branch 'main' of github.com:Progressive-Victory/the-website int…
solar464 May 9, 2026
88081b6
Merge branch 'dev' of github.com:Progressive-Victory/the-website into…
solar464 May 9, 2026
5c9153e
Fixing issue 306 citizenship error message (#400)
MaxDobbs32 May 10, 2026
c856f58
Update .env.example
sH3llH0und May 11, 2026
6be6e80
if you are in the banned stage, you are shown the banned page (#411)
Joseph-Melberg May 22, 2026
7ce06cb
Bump next from 15.5.15 to 15.5.18 (#409)
dependabot[bot] May 23, 2026
6724758
Admin Panel Header (#379)
BGL-PV May 23, 2026
7b8f6d4
speed up tests drastically, add timing based tests, remove unnecessar…
solar464 May 25, 2026
7c158f6
Merge branch 'dev' of github.com:Progressive-Victory/the-website into…
solar464 May 25, 2026
d99e2ba
fix inview unit test
solar464 May 25, 2026
efcfa2b
simplify onGet mocking
solar464 May 25, 2026
502d3ad
fix build and lint errors
solar464 May 25, 2026
6113c54
Delete src/test/mockStaticData.ts
solar464 May 27, 2026
82f6be0
removed paginated user request on account page to fix 403 error (#421)
sH3llH0und May 27, 2026
92f7135
Bugfix/117/disable tweet touch animation (#408)
sylintBundy May 27, 2026
33bff7e
Enhancement/116 (#407)
sylintBundy May 27, 2026
1a8452b
Reworked Endorsement Page (#422)
BGL-PV Jun 1, 2026
dab9b80
Merge branch 'main' into dev
sH3llH0und Jun 1, 2026
ffcc81c
Adjusting Candidate data (#427)
BGL-PV Jun 2, 2026
d0039a6
Merge branch 'main' into dev
sH3llH0und Jun 2, 2026
2bb04b2
Fundraising Panel V2 (#415)
BGL-PV Jun 3, 2026
f34b755
Merge branch 'dev' of github.com:Progressive-Victory/the-website into…
solar464 Jun 6, 2026
72f55c8
fix css util import
solar464 Jun 6, 2026
b399d9f
fix css util import to match status quo
solar464 Jun 6, 2026
65d8679
move static mock data to typescript so IDE type checking applies
solar464 Jun 6, 2026
8ec059b
document hydration hook unit test
solar464 Jun 6, 2026
1d0068d
Bump vitest from 4.0.18 to 4.1.0 (#426)
dependabot[bot] Jun 9, 2026
57ffe71
LNorman8/feature/433/Playwright Bump (#434)
LNorman8 Jun 9, 2026
a1bd762
Added the community event calender into the embedded page, related to…
naidneelttil Jun 9, 2026
b34ec24
Feature/twilio (#431)
AmberLehmann Jun 9, 2026
9653183
feature: added recently edited params to sort fields in admin panel f…
shawncasaus Jun 9, 2026
fe68c54
Merge branch 'dev' into feature/380/unit_testing_framework
solar464 Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@next/eslint-plugin-next": "^15.5.9",
"@playwright/test": "^1.57.0",
"@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
Expand All @@ -73,6 +73,8 @@
"globals": "^16.5.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"miragejs": "^0.1.48",
"pretender": "^3.4.7",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^3.4.19",
Expand All @@ -81,7 +83,7 @@
"typescript-eslint": "^8.53.1",
"vite-plugin-node-polyfills": "^0.24.0",
"vite-tsconfig-paths": "^6.0.4",
"vitest": "^4.0.18"
"vitest": "^4.1.0"
},
"pnpm": {
"overrides": {
Expand Down
779 changes: 414 additions & 365 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/app/QueryClientWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export default function QueryClientWrapper({
</QueryClientProvider>
)
}

export function clearQueryClient() {
queryClient.clear()
}
336 changes: 336 additions & 0 deletions src/app/admin/panels/fundraising/fundraising.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
import type { ChartGranularityMode } from '@/components/common/charts/timeBuckets'

export const PRESETS = [
'All Time',
'Year To Date',
'Month To Date',
'Last Month',
'Week To Date',
'Last 7 Days',
'Today',
'Yesterday',
] as const

export type Preset = (typeof PRESETS)[number]

export const CHART_GRANULARITY_LABELS: Record<ChartGranularityMode, string> = {
auto: 'Auto',
year: 'Years',
quarter: 'Quarters',
month: 'Months',
week: 'Weeks',
day: 'Days',
hour: 'Hours',
}

export function formatCurrency(value?: number) {
if (value == null || !Number.isFinite(value)) return '—'
return value.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}

export function formatCount(value?: number) {
if (value == null || !Number.isFinite(value)) return '—'
return value.toLocaleString('en-US')
}

export function formatCurrencyAxis(value: number, step: number): string {
if (!Number.isFinite(value)) return ''
let digits = 0
if (step < 1) digits = 2
else if (step < 10 && !Number.isInteger(step)) digits = 1

if (value >= 1_000_000) {
return `$${(value / 1_000_000).toLocaleString('en-US', {
maximumFractionDigits: 1,
})}M`
}

if (value >= 10_000) {
return `$${Math.round(value / 1000).toLocaleString('en-US')}k`
}

return value.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: digits,
maximumFractionDigits: digits,
})
}

export function formatCountAxis(value: number): string {
if (!Number.isFinite(value)) return ''
const rounded = Math.round(value)
if (rounded >= 1_000_000) {
return `${(rounded / 1_000_000).toLocaleString('en-US', {
maximumFractionDigits: 1,
})}M`
}
if (rounded >= 10_000) {
return `${Math.round(rounded / 1000).toLocaleString('en-US')}k`
}
return rounded.toLocaleString('en-US')
}

export function formatDonationCountLabel(value?: number) {
return `${formatCount(value)} ${value === 1 ? 'donation' : 'donations'}`
}

export function startOfDayISO(d: Date): string {
return new Date(
d.getFullYear(),
d.getMonth(),
d.getDate(),
0,
0,
0
).toISOString()
}

export function endOfDayISO(d: Date): string {
return new Date(
d.getFullYear(),
d.getMonth(),
d.getDate(),
23,
59,
59
).toISOString()
}

export function addDays(date: Date, days: number) {
const d = new Date(date)
d.setDate(d.getDate() + days)
return d
}

export function getPresetRange(preset: Preset): [string, string] {
const today = new Date()

switch (preset) {
case 'All Time':
return ['', '']
case 'Year To Date':
return [
startOfDayISO(new Date(today.getFullYear(), 0, 1)),
endOfDayISO(today),
]
case 'Month To Date':
return [
startOfDayISO(
new Date(today.getFullYear(), today.getMonth(), 1)
),
endOfDayISO(today),
]
case 'Last Month': {
const start = new Date(today.getFullYear(), today.getMonth() - 1, 1)
const end = new Date(today.getFullYear(), today.getMonth(), 0)
return [startOfDayISO(start), endOfDayISO(end)]
}
case 'Week To Date': {
const day = today.getDay()
const diff = day === 0 ? 6 : day - 1
const start = new Date(today)
start.setDate(today.getDate() - diff)
return [startOfDayISO(start), endOfDayISO(today)]
}
case 'Last 7 Days': {
const start = new Date(today)
start.setDate(today.getDate() - 6)
return [startOfDayISO(start), endOfDayISO(today)]
}
case 'Today':
return [startOfDayISO(today), endOfDayISO(today)]
case 'Yesterday': {
const yesterday = new Date(today)
yesterday.setDate(today.getDate() - 1)
return [startOfDayISO(yesterday), endOfDayISO(yesterday)]
}
}
}

export function getResolvedPresetRange(
preset: Preset,
allTimeFirstIso?: string
): [string, string] {
let [startIso, endIso] = getPresetRange(preset)

if (preset === 'All Time') {
const today = new Date()
endIso = endOfDayISO(today)
startIso = allTimeFirstIso
? startOfDayISO(new Date(allTimeFirstIso))
: startOfDayISO(today)
}

return [startIso, endIso]
}

export function inferPresetFromRange(
startIso: string,
endIso: string,
allTimeFirstIso?: string
): Preset | null {
if (!startIso || !endIso) return null

for (const preset of PRESETS) {
const [presetStart, presetEnd] = getResolvedPresetRange(
preset,
allTimeFirstIso
)
if (startIso === presetStart && endIso === presetEnd) {
return preset
}
}

return null
}

export function isXToDatePreset(
preset: Preset | null
): preset is 'Year To Date' | 'Month To Date' | 'Week To Date' | 'Today' {
return (
preset === 'Year To Date' ||
preset === 'Month To Date' ||
preset === 'Week To Date' ||
preset === 'Today'
)
}

export function getXToDatePeriodName(
preset: Preset | null,
granularityMode: ChartGranularityMode
): string {
if (preset === 'Year To Date') return 'Year'
if (preset === 'Month To Date') return 'Month'
if (preset === 'Week To Date') return 'Week'
if (preset === 'Today' && granularityMode === 'hour') return 'Hour'
return 'Date'
}

export function getExtendedEndForXToDatePreset(
preset: 'Year To Date' | 'Month To Date' | 'Week To Date' | 'Today',
startIso: string,
endIso: string
): string {
if (!startIso || !endIso) return endIso

if (preset === 'Today') {
const d = new Date(startIso)
return endOfDayISO(d)
}

if (preset === 'Year To Date') {
const d = new Date(startIso)
return endOfDayISO(new Date(d.getFullYear(), 11, 31))
}

if (preset === 'Month To Date') {
const d = new Date(startIso)
return endOfDayISO(new Date(d.getFullYear(), d.getMonth() + 1, 0))
}

const start = new Date(startIso)
const weekEnd = addDays(
new Date(start.getFullYear(), start.getMonth(), start.getDate()),
6
)
return endOfDayISO(weekEnd)
}

export function formatRangeDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}

export function isSameDay(a: Date, b: Date) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
)
}

type RangeKind = 'preset' | 'all' | 'both' | 'from' | 'until'

function getRangeKind(
committedPreset: Preset | null,
startDate: string,
endDate: string
): RangeKind {
switch (true) {
case !!committedPreset:
return 'preset'
case !!(startDate && endDate):
return 'both'
case !!startDate:
return 'from'
case !!endDate:
return 'until'
default:
return 'all'
}
}

export function getSelectedRangeLabel(
committedPreset: Preset | null,
startDate: string,
endDate: string
) {
const kind = getRangeKind(committedPreset, startDate, endDate)
switch (kind) {
case 'preset':
return committedPreset!
case 'both':
return isSameDay(new Date(startDate), new Date(endDate))
? formatRangeDate(startDate)
: `${formatRangeDate(startDate)} - ${formatRangeDate(endDate)}`
case 'from':
return `From ${formatRangeDate(startDate)}`
case 'until':
return `Until ${formatRangeDate(endDate)}`
case 'all':
return 'All Time'
default: {
const _exhaustive: never = kind
return _exhaustive
}
}
}

export function getRaisedKickerLabel(
committedPreset: Preset | null,
startDate: string,
endDate: string
) {
const kind = getRangeKind(committedPreset, startDate, endDate)
switch (kind) {
case 'preset':
return `Total Raised ${committedPreset!}`
case 'both':
return `Total Raised ${formatRangeDate(startDate)} - ${formatRangeDate(endDate)}`
case 'from':
return `Total Raised From ${formatRangeDate(startDate)}`
case 'until':
return `Total Raised Until ${formatRangeDate(endDate)}`
case 'all':
return 'Total Raised Custom Range'
default: {
const _exhaustive: never = kind
return _exhaustive
}
}
}

export function getPercentage(part: number, total: number): number | null {
if (!Number.isFinite(total) || total <= 0) return null
const pct = Math.round((part / total) * 100)
return Number.isFinite(pct) ? pct : null
}
Loading
Loading