"use client"; import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { ShieldCheck, ShieldX, Loader2 } from "lucide-react"; import { countryCodeToEmoji } from "@/lib/utils"; import type { Dictionary } from "@/lib/dictionaries"; interface IPData { ip: string; country_name: string; country_code: string; region: string; city: string; org: string; asn: string; } interface IPInfoCardProps { dict: Dictionary; } export default function IPInfoCard({ dict }: IPInfoCardProps) { const [open, setOpen] = useState(false); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const ref = useRef(null); const [isSecure, setIsSecure] = useState(false); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setIsSecure(window.location.protocol === "https:"); }, []); useEffect(() => { function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } } if (open) document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [open]); // Fetch IP data when opened. The request is tiny, so it's allowed to finish // even if the popup closes — aborting it used to leave the card stuck on // the loading state forever. Failed lookups retry on the next open. const fetching = useRef(false); useEffect(() => { if (!open || data || fetching.current) return; fetching.current = true; setLoading(true); setError(false); fetch("https://ipapi.co/json/") .then((res) => { if (!res.ok) throw new Error("Failed"); return res.json(); }) .then((json) => { setData({ ip: json.ip, country_name: json.country_name, country_code: json.country_code, region: json.region, city: json.city, org: json.org || "Unknown", asn: json.asn || "", }); }) .catch(() => { setError(true); }) .finally(() => { fetching.current = false; setLoading(false); }); }, [open, data]); const flag = data ? countryCodeToEmoji(data.country_code) : "🌐"; return (
{open && ( {/* Header */}
{dict.ipInfo.title} Visitor: {flag} {data?.country_code || "??"}
{/* Body */}
{loading && (
{dict.ipInfo.loading}
)} {error && (
{dict.ipInfo.error}
)} {data && !loading && ( <>
{flag}
{data.country_name}
{data.ip}
{dict.ipInfo.connection} {isSecure ? dict.ipInfo.secure : dict.ipInfo.notSecure} {isSecure ? ( ) : ( )}
)}
)}
); } function InfoRow({ label, value, wrap, }: { label: string; value: string; wrap?: boolean; }) { return (
{label} {value}
); }