File size: 4,867 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | "use client";
import { useEffect, useState, useRef } from "react";
import { createPortal } from "react-dom";
import PropTypes from "prop-types";
import { GITHUB_CONFIG } from "@/shared/constants/config";
export default function DonateModal({ isOpen, onClose }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const modalRef = useRef(null);
useEffect(() => {
if (!isOpen || data) return;
setLoading(true);
setError("");
fetch(GITHUB_CONFIG.donateUrl, { cache: "no-store" })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((json) => setData(json))
.catch((err) => setError(err.message || "Failed to load"))
.finally(() => setLoading(false));
}, [isOpen, data]);
useEffect(() => {
const handleClickOutside = (e) => {
if (modalRef.current && !modalRef.current.contains(e.target)) onClose();
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen, onClose]);
if (!isOpen || typeof document === "undefined") return null;
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" onClick={onClose} />
<div
ref={modalRef}
className="relative w-full bg-surface border border-black/10 dark:border-white/10 rounded-xl shadow-2xl animate-in fade-in zoom-in-95 duration-200 max-w-3xl flex flex-col max-h-[85vh]"
>
<div className="flex items-center justify-between p-3 border-b border-black/5 dark:border-white/5">
<h2 className="text-lg font-semibold text-text-main flex items-center gap-2">
<span className="material-symbols-outlined text-pink-500">volunteer_activism</span>
{data?.title || "Support 9Router"}
</h2>
<button
onClick={onClose}
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
aria-label="Close"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<div className="p-6 overflow-y-auto flex-1">
{loading && (
<div className="flex items-center justify-center py-10 text-text-muted">
<span className="material-symbols-outlined animate-spin mr-2">progress_activity</span>
Loading...
</div>
)}
{error && (
<div className="text-red-500 py-4">Failed to load donate info: {error}</div>
)}
{!loading && !error && data && (
<>
{data.message && (
<p className="text-text-muted text-sm mb-6 text-center">{data.message}</p>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{data.channels?.map((ch) => (
<DonateChannelCard key={ch.id} channel={ch} />
))}
</div>
</>
)}
</div>
</div>
</div>,
document.body
);
}
function DonateChannelCard({ channel }) {
const { label, description, icon, color, url, qr } = channel;
const content = (
<>
<div
className="w-12 h-12 rounded-full flex items-center justify-center mb-3"
style={{ backgroundColor: `${color}20`, color }}
>
<span className="material-symbols-outlined text-[26px]">{icon}</span>
</div>
<div className="font-semibold text-text-main mb-1">{label}</div>
{description && (
<div className="text-xs text-text-muted mb-3 text-center">{description}</div>
)}
{qr && (
<img
src={qr}
alt={`${label} QR`}
className="w-full max-w-[180px] aspect-square object-contain rounded-lg bg-white p-1"
/>
)}
</>
);
return (
<div className="flex flex-col items-center p-4 rounded-xl border border-black/10 dark:border-white/10 bg-surface/50 hover:border-pink-500/40 transition-colors">
{content}
{url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="mt-3 inline-flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm font-medium text-white hover:opacity-90 transition-opacity"
style={{ backgroundColor: color }}
>
Open
<span className="material-symbols-outlined text-[16px]">open_in_new</span>
</a>
)}
</div>
);
}
DonateModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};
|