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
6 changes: 4 additions & 2 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@
"processing": "Processing…",
"withdrawalSuccess": "Withdrawal successful! You received {amount} XLM.",
"errorNoSigners": "Please add at least one co-signer address.",
"errorInvalidThreshold": "Required signatures must be between 1 and the number of co-signers."
"errorInvalidThreshold": "Required signatures must be between 1 and the number of co-signers.",
"you": "You"
},
"Voting": {
"title": "Vote on this cause",
Expand Down Expand Up @@ -521,7 +522,8 @@
},
"CampaignMap": {
"emptyTitle": "No campaigns with location data",
"emptyBody": "Campaigns with coordinates will appear on this map. Switch to list view to browse all campaigns."
"emptyBody": "Campaigns with coordinates will appear on this map. Switch to list view to browse all campaigns.",
"loading": "Loading map…"
},
"Gamification": {
"level_Bronze": "Bronze",
Expand Down
6 changes: 4 additions & 2 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@
"processing": "Procesando…",
"withdrawalSuccess": "¡Retiro exitoso! Recibiste {amount} XLM.",
"errorNoSigners": "Por favor agrega al menos una dirección de co-firmante.",
"errorInvalidThreshold": "Las firmas requeridas deben estar entre 1 y el número de co-firmantes."
"errorInvalidThreshold": "Las firmas requeridas deben estar entre 1 y el número de co-firmantes.",
"you": "Tú"
},
"Voting": {
"title": "Vota por esta causa",
Expand Down Expand Up @@ -521,7 +522,8 @@
},
"CampaignMap": {
"emptyTitle": "No hay campañas con datos de ubicación",
"emptyBody": "Las campañas con coordenadas aparecerán en este mapa. Cambia a la vista de lista para ver todas las campañas."
"emptyBody": "Las campañas con coordenadas aparecerán en este mapa. Cambia a la vista de lista para ver todas las campañas.",
"loading": "Cargando mapa…"
},
"Gamification": {
"level_Bronze": "Bronce",
Expand Down
12 changes: 12 additions & 0 deletions src/components/AnimatedProgressFill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import { useEffect, useRef } from "react";
import { motion, useSpring, useTransform } from "framer-motion";
import { useReducedMotion } from "@/hooks/useReducedMotion";

export default function AnimatedProgressFill({ targetPct }: { targetPct: number }) {
const prefersReducedMotion = useReducedMotion();
const hasMountedRef = useRef(false);
const springPct = useSpring(targetPct, { stiffness: 120, damping: 20, mass: 0.6 });
const barWidth = useTransform(springPct, (v) => `${v}%`);
Expand All @@ -17,6 +19,16 @@ export default function AnimatedProgressFill({ targetPct }: { targetPct: number
springPct.set(targetPct);
}, [targetPct, springPct]);

if (prefersReducedMotion) {
return (
<div
aria-hidden="true"
className="bg-linear-to-r from-blue-500 to-purple-500 h-1.5 rounded-full"
style={{ width: `${targetPct}%` }}
/>
);
}

return (
<motion.div
aria-hidden="true"
Expand Down
29 changes: 28 additions & 1 deletion src/components/CampaignMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ export function filterByValidCoordinates(

interface CampaignMapProps {
campaigns: Campaign[];
isLoading?: boolean;
}

export default function CampaignMap({ campaigns }: CampaignMapProps) {
export default function CampaignMap({ campaigns, isLoading = false }: CampaignMapProps) {
const t = useTranslations("CampaignMap");
const validCampaigns = useMemo(() => filterByValidCoordinates(campaigns), [campaigns]);

Expand All @@ -76,6 +77,32 @@ export default function CampaignMap({ campaigns }: CampaignMapProps) {
return [latSum / validCampaigns.length, lngSum / validCampaigns.length];
}, [validCampaigns]);

if (isLoading) {
return (
<div className="rounded-xl overflow-hidden border border-zinc-200 dark:border-zinc-700">
<div className="h-[500px] w-full flex items-center justify-center bg-zinc-50 dark:bg-zinc-800">
<div className="flex flex-col items-center gap-3">
<svg
className="w-8 h-8 text-blue-500 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span className="text-sm text-zinc-500 dark:text-zinc-400">{t("loading")}</span>
</div>
</div>
</div>
);
}

if (validCampaigns.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] p-6 text-center rounded-xl bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700">
Expand Down
87 changes: 51 additions & 36 deletions src/components/MultiSigWithdrawalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,43 +299,58 @@ export default function MultiSigWithdrawalPanel({
</div>

<ul className="space-y-1.5">
{activeProposal.signers.map((signer) => (
<li key={signer.address} className="flex items-center gap-2 text-xs">
{signer.signedAt ? (
<svg
className="w-3.5 h-3.5 text-green-500 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
className="w-3.5 h-3.5 text-zinc-300 dark:text-zinc-600 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16z"
clipRule="evenodd"
/>
</svg>
)}
<span className="font-mono text-zinc-600 dark:text-zinc-400 break-all">
{signer.address.slice(0, 8)}…{signer.address.slice(-6)}
</span>
{signer.signedAt && (
<span className="text-zinc-400 dark:text-zinc-500">
{new Date(signer.signedAt * 1000).toLocaleDateString()}
{activeProposal.signers.map((signer) => {
const isMe = isSameAddress(signer.address, walletAddress);
return (
<li
key={signer.address}
className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${
isMe
? "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
: ""
}`}
>
{signer.signedAt ? (
<svg
className="w-3.5 h-3.5 text-green-500 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
className="w-3.5 h-3.5 text-zinc-300 dark:text-zinc-600 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16z"
clipRule="evenodd"
/>
</svg>
)}
<span className="font-mono text-zinc-600 dark:text-zinc-400 break-all">
{signer.address.slice(0, 8)}…{signer.address.slice(-6)}
</span>
)}
</li>
))}
{isMe && (
<span className="ml-auto text-xs font-medium text-blue-600 dark:text-blue-400 shrink-0">
{t("you")}
</span>
)}
{signer.signedAt && (
<span className="text-zinc-400 dark:text-zinc-500 shrink-0">
{new Date(signer.signedAt * 1000).toLocaleDateString()}
</span>
)}
</li>
);
})}
</ul>

<div className="flex flex-col sm:flex-row gap-2">
Expand Down
Loading