Skip to content
Merged
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
9 changes: 9 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@

[[plugins]]
package = "@netlify/plugin-nextjs"

[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://horizon.stellar.org https://horizon-testnet.stellar.org https://soroban.stellar.org https://soroban-testnet.stellar.org https://app.posthog.com https://us.i.posthog.com; img-src 'self' data: blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
37 changes: 34 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions src/__tests__/format.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
safeBigInt,
stroopsToDisplay,
displayToStroops,
formatUSDC,
Expand Down Expand Up @@ -139,3 +140,37 @@ describe('estimatePremium', () => {
expect(estimatePremium('0.05', 100)).toBe('0.00');
});
});

describe('safeBigInt', () => {
it('converts valid numeric strings', () => {
expect(safeBigInt('10000000')).toBe(10000000n);
expect(safeBigInt('0')).toBe(0n);
expect(safeBigInt('-42')).toBe(-42n);
});

it('passes through bigint values', () => {
expect(safeBigInt(123n)).toBe(123n);
});

it('returns 0n for empty string', () => {
expect(safeBigInt('')).toBe(0n);
});

it('returns 0n for whitespace', () => {
expect(safeBigInt(' ')).toBe(0n);
});

it('returns 0n for null', () => {
expect(safeBigInt(null)).toBe(0n);
});

it('returns 0n for undefined', () => {
expect(safeBigInt(undefined)).toBe(0n);
});

it('returns 0n for non-numeric strings', () => {
expect(safeBigInt('N/A')).toBe(0n);
expect(safeBigInt('abc')).toBe(0n);
expect(safeBigInt('12.5')).toBe(0n);
});
});
8 changes: 5 additions & 3 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ConnectWalletPrompt } from "@/components/ConnectWalletPrompt";
import { PolicyCard } from "@/components/PolicyCard";
import { SkeletonCard } from "@/components/Skeleton";
import { EmptyState } from "@/components/EmptyState";
import { formatUSDC } from "@/lib/format";
import { formatUSDC, safeBigInt } from "@/lib/format";
import { StatsCard } from "@/components/StatsCard";
import { WalletAddressDisplay } from "@/components/WalletAddressDisplay";
import Link from "next/link";
Expand All @@ -24,8 +24,8 @@ export default function DashboardPage() {
}

const active = policies.filter((p) => p.status === "Active");
const totalCov = active.reduce((sum, p) => sum + BigInt(p.coverage), 0n);
const totalPaid = active.reduce((sum, p) => sum + BigInt(p.premiumPaid), 0n);
const totalCov = active.reduce((sum, p) => sum + safeBigInt(p.coverage), 0n);
const totalPaid = active.reduce((sum, p) => sum + safeBigInt(p.premiumPaid), 0n);

return (
<main className="mx-auto max-w-7xl px-6 py-12">
Expand Down Expand Up @@ -73,6 +73,8 @@ export default function DashboardPage() {
<SkeletonCard key={i} />
))}
</div>
) : error ? (
<p className="text-sm text-gray-400">Unable to load policies — {error}</p>
) : active.length === 0 ? (
<EmptyState
icon="🛡️"
Expand Down
19 changes: 14 additions & 5 deletions src/components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { PolicyStatus, ClaimStatus } from '@/types';
import { STATUS_COLOURS } from '@/lib/constants';

type BadgeLabel = PolicyStatus | ClaimStatus | string;

interface BadgeProps {
label: string;
variant?: string;
label: BadgeLabel;
variant?: ColourName;
className?: string;
}

const COLOUR_CLASSES: Record<string, string> = {
type ColourName = 'emerald' | 'sky' | 'amber' | 'red' | 'gray' | 'purple' | 'teal';

const COLOUR_CLASSES: Record<ColourName, string> = {
emerald: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
sky: 'bg-sky-500/10 text-sky-400 border-sky-500/20',
amber: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
Expand All @@ -16,9 +21,13 @@ const COLOUR_CLASSES: Record<string, string> = {
teal: 'bg-teal-500/10 text-teal-400 border-teal-500/20',
};

function isStatusKey(key: string): key is PolicyStatus | ClaimStatus {
return key in STATUS_COLOURS;
}

export function Badge({ label, variant, className }: BadgeProps) {
const colour = variant ?? STATUS_COLOURS[label] ?? 'gray';
const colourClass = COLOUR_CLASSES[colour] ?? COLOUR_CLASSES.gray;
const colour: ColourName = (variant ?? (isStatusKey(label) ? STATUS_COLOURS[label] : undefined) ?? 'gray') as ColourName;
const colourClass = COLOUR_CLASSES[colour];
return (
<span
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest ${colourClass} ${className ?? ''}`}
Expand Down
8 changes: 5 additions & 3 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,25 @@ export const ORACLE_CONTRACT_ID =
export const CLAIMS_CONTRACT_ID =
process.env.NEXT_PUBLIC_CLAIMS_CONTRACT_ID ?? '';

export const CATEGORY_LABELS: Record<string, string> = {
import type { Category, PolicyStatus, ClaimStatus } from '@/types';

export const CATEGORY_LABELS: Record<Category, string> = {
crop: 'Crop Insurance',
flight: 'Flight Delay',
disaster: 'Natural Disaster',
health: 'Health',
defi: 'DeFi Cover',
};

export const CATEGORY_ICONS: Record<string, string> = {
export const CATEGORY_ICONS: Record<Category, string> = {
crop: '🌾',
flight: '✈️',
disaster: '🌪️',
health: '🏥',
defi: '🔐',
};

export const STATUS_COLOURS: Record<string, string> = {
export const STATUS_COLOURS: Record<PolicyStatus | ClaimStatus, string> = {
Active: 'emerald',
Expired: 'gray',
Claimed: 'sky',
Expand Down
8 changes: 8 additions & 0 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { STROOPS_PER_UNIT } from './constants';

export function safeBigInt(value: string | bigint | null | undefined): bigint {
if (value === null || value === undefined) return 0n;
if (typeof value === 'bigint') return value;
const trimmed = value.trim();
if (trimmed === '' || !/^-?\d+$/.test(trimmed)) return 0n;
return BigInt(trimmed);
}

export function stroopsToDisplay(stroops: string | bigint | null | undefined, decimals = 2): string {
if (stroops === null || stroops === undefined) return '—';
if (typeof stroops === 'string') {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.tsbuildinfo

Large diffs are not rendered by default.

Loading