Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions frontend/src/__tests__/utils/dateHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import { formatDate, getRemainingDays } from '../../utils/dateHelpers';

describe('formatDate', () => {
it('returns N/A for empty string', () => {
expect(formatDate('')).toBe('N/A');
});

it('formats a date-only string (YYYY-MM-DD) correctly', () => {
const result = formatDate('2024-01-15');
expect(result).toBe('Jan 15, 2024');
});

it('formats an ISO datetime string correctly', () => {
const result = formatDate('2024-06-01T10:00:00Z');
expect(result).toContain('2024');
expect(result).toContain('Jun');
});

it('returns the original string for an invalid date', () => {
expect(formatDate('not-a-date')).toBe('not-a-date');
});

it('handles single-digit month and day', () => {
const result = formatDate('2024-03-05');
expect(result).toBe('Mar 5, 2024');
});

it('handles Dec 31 edge case', () => {
const result = formatDate('2024-12-31');
expect(result).toBe('Dec 31, 2024');
});
});

describe('getRemainingDays', () => {
it('returns 0 for an invalid date string', () => {
expect(getRemainingDays('garbage')).toBe(0);
});

it('returns 0 for today', () => {
const today = new Date();
expect(getRemainingDays(today)).toBe(0);
});

it('returns a positive number for a future date', () => {
const future = new Date();
future.setDate(future.getDate() + 10);
expect(getRemainingDays(future)).toBe(10);
});

it('returns a negative number for a past date', () => {
const past = new Date();
past.setDate(past.getDate() - 5);
expect(getRemainingDays(past)).toBe(-5);
});

it('accepts a date string', () => {
const future = new Date();
future.setDate(future.getDate() + 3);
const iso = future.toISOString();
expect(getRemainingDays(iso)).toBe(3);
});

it('uses day-level precision, not time-level', () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
expect(getRemainingDays(tomorrow)).toBe(1);
});
});
1 change: 1 addition & 0 deletions frontend/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const Avatar: React.FC<AvatarProps> = ({
<img
src={avatarUrl}
alt={name}
loading="lazy"
className="w-full h-full object-cover"
onError={() => {
setHasImageError(true);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ContractErrorPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
box-shadow: var(--shadow-md);
backdrop-filter: blur(10px);
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/FeeEstimationPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s ease;
}

.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
box-shadow: var(--shadow-md);
}

.cardTitle {
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
--font-head: 'Syne', sans-serif;
--font-body: 'Inter', sans-serif;
--font-mono: 'DM Mono', monospace;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.16);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.24);
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.18);
--shadow-card-hover: 0 6px 20px rgba(0, 0, 0, 0.28);
color-scheme: dark;
}

Expand All @@ -44,6 +49,11 @@
--font-head: 'Syne', sans-serif;
--font-body: 'Inter', sans-serif;
--font-mono: 'DM Mono', monospace;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-card-hover: 0 6px 20px rgba(0, 0, 0, 0.1);
color-scheme: light;
}

Expand Down Expand Up @@ -121,13 +131,16 @@ h6 {
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px;
box-shadow: var(--shadow-card);
transition:
transform 0.2s ease,
border-color 0.2s ease;
border-color 0.2s ease,
box-shadow 0.2s ease;
}

.card:hover {
border-color: var(--border-hi);
box-shadow: var(--shadow-card-hover);
transform: translateY(-2px);
}

Expand Down
20 changes: 1 addition & 19 deletions frontend/src/pages/PayrollScheduler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createClaimableBalanceTransaction, generateWallet } from '../services/s
import { ContractErrorPanel } from '../components/ContractErrorPanel';
import { HelpLink } from '../components/HelpLink';
import { parseContractError, type ContractErrorDetail } from '../utils/contractErrorParser';
import { formatDate } from '../utils/dateHelpers';

interface PayrollFormState {
employeeName: string;
Expand Down Expand Up @@ -111,25 +112,6 @@ function computeNextRunDate(config: SchedulingConfig, from: Date = new Date()):
return first;
}

const formatDate = (dateString: string) => {
if (!dateString) return 'N/A';

const dateOnlyMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateString);
const date = dateOnlyMatch
? new Date(
Number.parseInt(dateOnlyMatch[1], 10),
Number.parseInt(dateOnlyMatch[2], 10) - 1,
Number.parseInt(dateOnlyMatch[3], 10)
)
: new Date(dateString);

if (isNaN(date.getTime())) return dateString;
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
};

interface PendingClaim {
id: string;
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/providers/WalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
>
<div className="flex items-center gap-3">
{wallet.icon ? (
<img src={wallet.icon} alt={wallet.name} className="h-6 w-6 rounded" />
<img
src={wallet.icon}
alt={wallet.name}
loading="lazy"
className="h-6 w-6 rounded"
/>
) : (
<div className="h-6 w-6 rounded bg-white/10" />
)}
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/utils/dateHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function formatDate(dateString: string): string {
if (!dateString) return 'N/A';

const dateOnlyMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateString);
const date = dateOnlyMatch
? new Date(
Number.parseInt(dateOnlyMatch[1], 10),
Number.parseInt(dateOnlyMatch[2], 10) - 1,
Number.parseInt(dateOnlyMatch[3], 10)
)
: new Date(dateString);

if (isNaN(date.getTime())) return dateString;
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}

export function getRemainingDays(targetDate: string | Date): number {
const target = typeof targetDate === 'string' ? new Date(targetDate) : targetDate;
if (isNaN(target.getTime())) return 0;

const now = new Date();
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const startOfTarget = new Date(target.getFullYear(), target.getMonth(), target.getDate());

const diffMs = startOfTarget.getTime() - startOfToday.getTime();
return Math.ceil(diffMs / (1000 * 60 * 60 * 24));
}
11 changes: 8 additions & 3 deletions src/pages/PayrollScheduler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ export default function PayrollScheduler() {
return (
<div className="p-12 max-w-4xl mx-auto space-y-8 min-h-screen">
<div>
<h1 className="text-3xl font-bold mb-2">Payroll <span className="text-accent">Scheduler</span></h1>
<p className="text-gray-500">Automate recurring payments to your workforce.</p>
<h1 className="text-3xl font-bold mb-2">
Payroll <span className="text-accent">Scheduler</span>
</h1>
<p className="text-gray-500">
Automate recurring payments to your workforce.
</p>
</div>

<Card className="p-12 border-dashed border-2 flex flex-col items-center justify-center text-center space-y-4">
Expand All @@ -16,7 +20,8 @@ export default function PayrollScheduler() {
<div>
<h3 className="text-xl font-bold">No Active Schedules</h3>
<p className="text-gray-400 max-w-sm mx-auto">
You haven't set up any automated payroll schedules yet. Connect your wallet to get started.
You haven't set up any automated payroll schedules yet. Connect your
wallet to get started.
</p>
</div>
<Button variant="primary" size="md" className="!bg-accent">
Expand Down
Loading