(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1import React, { useState } from "react";
2import { Button } from "../ui";
3import { ExternalLink, Shield } from "lucide-react";
4import { addSkippedHostname } from "../../store/preferences";
5import { useTranslation } from "react-i18next";
6
7interface ExternalLinkModalProps {
8 isOpen: boolean;
9 onClose: () => void;
10 url: string | null;
11}
12
13export default function ExternalLinkModal({
14 isOpen,
15 onClose,
16 url,
17}: ExternalLinkModalProps) {
18 const { t } = useTranslation();
19 const [dontAskAgain, setDontAskAgain] = useState(false);
20
21 if (!isOpen || !url) return null;
22
23 const displayUrl = url.split("#:~:text=")[0];
24
25 const handleContinue = () => {
26 if (dontAskAgain && url) {
27 try {
28 const hostname = new URL(url).hostname;
29 addSkippedHostname(hostname);
30 } catch (e) {
31 console.error("Invalid URL", e);
32 }
33 }
34 window.open(url, "_blank", "noopener,noreferrer");
35 onClose();
36 };
37
38 const hostname = (() => {
39 try {
40 return new URL(url).hostname;
41 } catch {
42 return "this site";
43 }
44 })();
45
46 return (
47 <div
48 className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm animate-fade-in"
49 onClick={onClose}
50 >
51 <div
52 className="bg-white dark:bg-surface-900 rounded-xl shadow-2xl max-w-md w-full animate-scale-in ring-1 ring-surface-200 dark:ring-surface-700 overflow-hidden"
53 onClick={(e) => e.stopPropagation()}
54 >
55 <div className="px-6 pt-6 pb-4">
56 <div className="flex items-start gap-3">
57 <div className="w-9 h-9 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
58 <Shield size={18} />
59 </div>
60 <div className="min-w-0">
61 <h2 className="text-base font-semibold text-surface-900 dark:text-white">
62 {t("externalLink.title")}
63 </h2>
64 <p className="text-sm text-surface-500 dark:text-surface-400 mt-1">
65 {t("externalLink.message")}
66 </p>
67 </div>
68 </div>
69
70 <div className="mt-4 flex items-center gap-2 bg-surface-50 dark:bg-surface-800/60 border border-surface-200 dark:border-surface-700 rounded-lg px-3 py-2.5">
71 <ExternalLink
72 size={14}
73 className="text-surface-400 dark:text-surface-500 flex-shrink-0"
74 />
75 <span className="text-sm text-surface-700 dark:text-surface-300 break-all line-clamp-2">
76 {displayUrl}
77 </span>
78 </div>
79 </div>
80
81 <div className="px-6 pb-5 pt-2 flex flex-col gap-3">
82 <label className="flex items-center gap-2 cursor-pointer select-none group">
83 <input
84 type="checkbox"
85 checked={dontAskAgain}
86 onChange={(e) => setDontAskAgain(e.target.checked)}
87 className="rounded border-surface-300 dark:border-surface-600 text-primary-600 focus:ring-primary-500 w-3.5 h-3.5 cursor-pointer"
88 />
89 <span className="text-xs text-surface-500 dark:text-surface-400 group-hover:text-surface-600 dark:group-hover:text-surface-300 transition-colors">
90 {t("externalLink.alwaysAllow", { hostname })}
91 </span>
92 </label>
93
94 <div className="flex gap-2">
95 <Button
96 onClick={onClose}
97 variant="ghost"
98 className="flex-1 justify-center"
99 >
100 {t("externalLink.cancel")}
101 </Button>
102 <Button
103 onClick={handleContinue}
104 variant="primary"
105 className="flex-1 justify-center"
106 icon={<ExternalLink size={14} />}
107 >
108 {t("externalLink.open")}
109 </Button>
110 </div>
111 </div>
112 </div>
113 </div>
114 );
115}