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
44 changes: 22 additions & 22 deletions frontend/src/app/[locale]/admin/disputes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,28 +225,28 @@ export default function AdminDisputeDetailPage() {
onClose={() => setPendingAction(null)}
title={modalCopy.title}
size="md"
>
<div className="space-y-5">
<p className="text-sm text-zinc-600 dark:text-zinc-300">{modalCopy.description}</p>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => setPendingAction(null)}
className="rounded-lg border border-zinc-200 px-4 py-2 text-sm font-semibold text-zinc-700 transition hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
>
{t("modal.cancel")}
</button>
<button
type="button"
onClick={submitResolution}
disabled={resolveDispute.isPending}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{resolveDispute.isPending ? t("modal.submitting") : t("modal.submit")}
</button>
</div>
/>
<div className="space-y-5">
<p className="text-sm text-zinc-600 dark:text-zinc-300">{modalCopy.description}</p>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => setPendingAction(null)}
className="rounded-lg border border-zinc-200 px-4 py-2 text-sm font-semibold text-zinc-700 transition hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
>
{t("modal.cancel")}
</button>
<button
type="button"
onClick={submitResolution}
disabled={resolveDispute.isPending}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{resolveDispute.isPending ? t("modal.submitting") : t("modal.submit")}
</button>
</div>
</Modal>
</section>
</div>
</Modal>
</section >
);
}
38 changes: 33 additions & 5 deletions frontend/src/app/[locale]/request-loan/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,38 @@ const LoanApplicationWizard = dynamic(
{ ssr: false, loading: () => <WizardSkeleton /> },
);

// Score band thresholds
const EXCELLENT_SCORE_THRESHOLD = 750;
const GOOD_SCORE_THRESHOLD = 670;
const FAIR_SCORE_THRESHOLD = 580;
const MINIMUM_ELIGIBLE_SCORE_THRESHOLD = 500;

// Maximum loan amounts per score band
const EXCELLENT_SCORE_MAX_LOAN = 50_000;
const GOOD_SCORE_MAX_LOAN = 25_000;
const FAIR_SCORE_MAX_LOAN = 10_000;
const MINIMUM_SCORE_MAX_LOAN = 5_000;

// Warning range
const NEAR_MINIMUM_SCORE_DELTA = 40;

function getScoreBandMax(score: number): number {
if (score >= 750) return 50_000;
if (score >= 670) return 25_000;
if (score >= 580) return 10_000;
if (score >= 500) return 5_000;
if (score >= EXCELLENT_SCORE_THRESHOLD) {
return EXCELLENT_SCORE_MAX_LOAN;
}

if (score >= GOOD_SCORE_THRESHOLD) {
return GOOD_SCORE_MAX_LOAN;
}

if (score >= FAIR_SCORE_THRESHOLD) {
return FAIR_SCORE_MAX_LOAN;
}

if (score >= MINIMUM_ELIGIBLE_SCORE_THRESHOLD) {
return MINIMUM_SCORE_MAX_LOAN;
}

return 0;
}

Expand Down Expand Up @@ -83,7 +110,8 @@ export default function RequestLoanPage() {
minScoreConfig?.maxAmount ?? Number.POSITIVE_INFINITY,
);
const scoreDelta = resolvedCreditScore - minimumScore;
const isCloseToMinimum = scoreDelta >= 0 && scoreDelta <= 40;
const isCloseToMinimum =
scoreDelta >= 0 && scoreDelta <= NEAR_MINIMUM_SCORE_DELTA;
const isIneligible = isWalletConnected && !isLoadingConfig && !isLoadingScore && scoreDelta < 0;
const isCheckingEligibility =
isWalletConnected && (isLoadingConfig || (isLoadingScore && !!borrowerAddress));
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/app/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
ariaLabel?: string;
children: React.ReactNode;
className?: string;
size?: "sm" | "md" | "lg" | "xl";
Expand All @@ -29,6 +30,7 @@ const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
ariaLabel,
children,
className,
size = "lg",
Expand Down Expand Up @@ -67,6 +69,7 @@ const Modal: React.FC<ModalProps> = ({
role="dialog"
aria-modal="true"
aria-labelledby={title ? titleId : undefined}
aria-label={!title ? ariaLabel : undefined}
tabIndex={-1}
className={cn(
"relative w-full overflow-hidden rounded-2xl bg-white shadow-2xl dark:bg-zinc-950 dark:border dark:border-zinc-800 focus:outline-none",
Expand Down
24 changes: 13 additions & 11 deletions frontend/src/app/utils/soundManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class SoundManager {
}

private initializeSounds() {
// Using data URIs for simple sound effects to avoid external dependencies
// In production, replace with actual audio files
// Placeholder silent sound effects.
// Replace with real audio assets or generated tones in production.

const soundEffects: Record<SoundEffect, string> = {
// Level up - triumphant sound
Expand Down Expand Up @@ -65,15 +65,17 @@ class SoundManager {
}

/**
* Generate a simple tone using Web Audio API
* This creates a data URI that can be used as an audio source
*/
private generateTone(_frequencies: number[], _duration: number): string {
// For now, return empty data URI
// In a real implementation, you would generate actual audio data
// or use external audio files
return "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=";
}
* Placeholder sound generator.
*
* Gamification sound effects are currently disabled and all generated
* clips are intentionally silent placeholders until real audio assets
* or Web Audio API tone generation is implemented.
*
* The frequency and duration parameters are currently unused.
*/
private generateTone(_frequencies: number[], _duration: number): string {
return "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=";
}

/**
* Play a sound effect
Expand Down
Loading