fix: lint

Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
2026-02-09 15:09:26 +08:00
parent 25b0ab7fea
commit 307c007c3f
7 changed files with 377 additions and 317 deletions

View File

@@ -1,194 +1,194 @@
import {
AlertCircle,
CheckCircle2,
Loader2,
Paperclip,
Send,
X,
AlertCircle,
CheckCircle2,
Loader2,
Paperclip,
Send,
X,
} from "lucide-react";
import { useRef, useState } from "react";
interface SendAlertFormProps {
webhookUrl: string;
onSuccess?: () => void;
placeholder?: string;
title?: string;
webhookUrl: string;
onSuccess?: () => void;
placeholder?: string;
title?: string;
}
export default function SendAlertForm({
webhookUrl,
onSuccess,
placeholder = "Type your message here...",
title = "Send Quick Alert",
webhookUrl,
onSuccess,
placeholder = "Type your message here...",
title = "Send Quick Alert",
}: SendAlertFormProps) {
const [content, setContent] = useState("");
const [file, setFile] = useState<File | null>(null);
const [isSending, setIsSending] = useState(false);
const [status, setStatus] = useState<{
type: "success" | "error";
message: string;
} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [content, setContent] = useState("");
const [file, setFile] = useState<File | null>(null);
const [isSending, setIsSending] = useState(false);
const [status, setStatus] = useState<{
type: "success" | "error";
message: string;
} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
setStatus(null);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files?.[0]) {
setFile(e.target.files[0]);
setStatus(null);
}
};
const removeFile = () => {
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const removeFile = () => {
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim() && !file) return;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim() && !file) return;
setIsSending(true);
setStatus(null);
setIsSending(true);
setStatus(null);
try {
const formData = new FormData();
if (content.trim()) {
// We send content as a stringified JSON if it's elaborate,
// but for "Pure Proxy", simple text is just a field.
// Our backend getRequestBody handles both.
formData.append("content", JSON.stringify({ text: content }));
formData.append("msg_type", "text");
}
try {
const formData = new FormData();
if (content.trim()) {
// We send content as a stringified JSON if it's elaborate,
// but for "Pure Proxy", simple text is just a field.
// Our backend getRequestBody handles both.
formData.append("content", JSON.stringify({ text: content }));
formData.append("msg_type", "text");
}
if (file) {
const isImage = file.type.startsWith("image/");
formData.append(isImage ? "image" : "file", file);
if (!content.trim()) {
formData.append("msg_type", isImage ? "image" : "file");
}
}
if (file) {
const isImage = file.type.startsWith("image/");
formData.append(isImage ? "image" : "file", file);
if (!content.trim()) {
formData.append("msg_type", isImage ? "image" : "file");
}
}
const response = await fetch(webhookUrl, {
method: "POST",
body: formData,
});
const response = await fetch(webhookUrl, {
method: "POST",
body: formData,
});
if (response.ok) {
setStatus({ type: "success", message: "Alert sent successfully!" });
setContent("");
setFile(null);
if (fileInputRef.current) fileInputRef.current.value = "";
onSuccess?.();
setTimeout(() => setStatus(null), 3000);
} else {
const error = await response.json();
setStatus({
type: "error",
message: error.error || "Failed to send alert",
});
}
} catch (err) {
setStatus({ type: "error", message: "Network error. Please try again." });
} finally {
setIsSending(false);
}
};
if (response.ok) {
setStatus({ type: "success", message: "Alert sent successfully!" });
setContent("");
setFile(null);
if (fileInputRef.current) fileInputRef.current.value = "";
onSuccess?.();
setTimeout(() => setStatus(null), 3000);
} else {
const error = await response.json();
setStatus({
type: "error",
message: error.error || "Failed to send alert",
});
}
} catch (_err) {
setStatus({ type: "error", message: "Network error. Please try again." });
} finally {
setIsSending(false);
}
};
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
<h3 className="text-sm font-bold text-gray-700 uppercase tracking-tight">
{title}
</h3>
</div>
<form onSubmit={handleSubmit} className="p-5">
<div className="relative">
<textarea
className="w-full min-h-[100px] p-3 text-sm text-gray-800 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all placeholder:text-gray-400 resize-none"
placeholder={placeholder}
value={content}
onChange={(e) => setContent(e.target.value)}
disabled={isSending}
/>
</div>
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
<h3 className="text-sm font-bold text-gray-700 uppercase tracking-tight">
{title}
</h3>
</div>
<form onSubmit={handleSubmit} className="p-5">
<div className="relative">
<textarea
className="w-full min-h-[100px] p-3 text-sm text-gray-800 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all placeholder:text-gray-400 resize-none"
placeholder={placeholder}
value={content}
onChange={(e) => setContent(e.target.value)}
disabled={isSending}
/>
</div>
<div className="mt-4 space-y-3">
{file && (
<div className="flex items-center justify-between p-2 px-3 bg-indigo-50 rounded-lg border border-indigo-100 animate-in fade-in slide-in-from-top-2">
<div className="flex items-center">
<Paperclip className="w-4 h-4 text-indigo-500 mr-2" />
<span className="text-xs font-medium text-indigo-700 truncate max-w-[200px]">
{file.name}
</span>
<span className="ml-2 text-[10px] text-indigo-400">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<button
type="button"
onClick={removeFile}
className="text-indigo-400 hover:text-indigo-600 p-1 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
)}
<div className="mt-4 space-y-3">
{file && (
<div className="flex items-center justify-between p-2 px-3 bg-indigo-50 rounded-lg border border-indigo-100 animate-in fade-in slide-in-from-top-2">
<div className="flex items-center">
<Paperclip className="w-4 h-4 text-indigo-500 mr-2" />
<span className="text-xs font-medium text-indigo-700 truncate max-w-[200px]">
{file.name}
</span>
<span className="ml-2 text-[10px] text-indigo-400">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<button
type="button"
onClick={removeFile}
className="text-indigo-400 hover:text-indigo-600 p-1 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
)}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
id="alert-file-upload"
/>
<label
htmlFor="alert-file-upload"
className="cursor-pointer inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors border border-gray-200"
>
<Paperclip className="w-3.5 h-3.5 mr-1.5" />
Attach File
</label>
</div>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
id="alert-file-upload"
/>
<label
htmlFor="alert-file-upload"
className="cursor-pointer inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors border border-gray-200"
>
<Paperclip className="w-3.5 h-3.5 mr-1.5" />
Attach File
</label>
</div>
<button
type="submit"
disabled={isSending || (!content.trim() && !file)}
className="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-indigo-500/20"
>
{isSending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Send Alert
</>
)}
</button>
</div>
</div>
<button
type="submit"
disabled={isSending || (!content.trim() && !file)}
className="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-indigo-500/20"
>
{isSending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Send Alert
</>
)}
</button>
</div>
</div>
{status && (
<div
className={`mt-4 p-3 rounded-lg flex items-start animate-in fade-in zoom-in-95 ${status.type === "success"
? "bg-green-50 text-green-700 border border-green-100"
: "bg-red-50 text-red-700 border border-red-100"
}`}
>
{status.type === "success" ? (
<CheckCircle2 className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
)}
<span className="text-xs font-medium">{status.message}</span>
</div>
)}
</form>
</div>
);
{status && (
<div
className={`mt-4 p-3 rounded-lg flex items-start animate-in fade-in zoom-in-95 ${status.type === "success"
? "bg-green-50 text-green-700 border border-green-100"
: "bg-red-50 text-red-700 border border-red-100"
}`}
>
{status.type === "success" ? (
<CheckCircle2 className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
)}
<span className="text-xs font-medium">{status.message}</span>
</div>
)}
</form>
</div>
);
}

View File

@@ -4,13 +4,13 @@ import {
Globe,
Lock,
Plus,
Send,
Settings,
ShieldCheck,
User,
UserMinus,
UserPlus,
Users,
Send,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import GroupBindingsModal from "../components/GroupBindingsModal";
@@ -228,20 +228,20 @@ export default function TopicsView() {
const updatedSubs = isSubscribed
? t.subscriptions.filter((s) => s.userId !== userId)
: [
...t.subscriptions,
{
userId,
user:
users.find((u) => u.id === userId) ||
(currentUser
? {
id: currentUser.id,
name: currentUser.name,
email: currentUser.email,
}
: { id: "unknown", name: "Unknown" }),
},
];
...t.subscriptions,
{
userId,
user:
users.find((u) => u.id === userId) ||
(currentUser
? {
id: currentUser.id,
name: currentUser.name,
email: currentUser.email,
}
: { id: "unknown", name: "Unknown" }),
},
];
return { ...t, subscriptions: updatedSubs };
}
return t;
@@ -253,20 +253,20 @@ export default function TopicsView() {
const updatedSubs = isSubscribed
? selectedTopic.subscriptions.filter((s) => s.userId !== userId)
: [
...selectedTopic.subscriptions,
{
userId,
user:
users.find((u) => u.id === userId) ||
(currentUser
? {
id: currentUser.id,
name: currentUser.name,
email: currentUser.email,
}
: { id: "unknown", name: "Unknown" }),
},
];
...selectedTopic.subscriptions,
{
userId,
user:
users.find((u) => u.id === userId) ||
(currentUser
? {
id: currentUser.id,
name: currentUser.name,
email: currentUser.email,
}
: { id: "unknown", name: "Unknown" }),
},
];
setSelectedTopic({ ...selectedTopic, subscriptions: updatedSubs });
}
@@ -421,7 +421,9 @@ export default function TopicsView() {
className="inline-flex items-center text-sm font-bold text-white hover:text-indigo-200 transition-colors"
>
<Send className="w-4 h-4 mr-2" />
{showPersonalSend ? "Hide Send Form" : "Send Quick Message to Myself"}
{showPersonalSend
? "Hide Send Form"
: "Send Quick Message to Myself"}
</button>
{showPersonalSend && (
@@ -479,10 +481,11 @@ export default function TopicsView() {
<button
type="button"
onClick={() => handleSelfSubscribe(topic)}
className={`inline-flex items-center px-3 py-1 border text-xs font-medium rounded-md ${isSubscribedToTopic(topic)
? "border-red-300 text-red-700 bg-red-50 hover:bg-red-100"
: "border-green-300 text-green-700 bg-green-50 hover:bg-green-100"
}`}
className={`inline-flex items-center px-3 py-1 border text-xs font-medium rounded-md ${
isSubscribedToTopic(topic)
? "border-red-300 text-red-700 bg-red-50 hover:bg-red-100"
: "border-green-300 text-green-700 bg-green-50 hover:bg-green-100"
}`}
>
{isSubscribedToTopic(topic) ? (
<>
@@ -595,13 +598,16 @@ export default function TopicsView() {
type="button"
onClick={() =>
setActiveSendTopic(
activeSendTopic === topic.id ? null : topic.id,
activeSendTopic === topic.id
? null
: topic.id,
)
}
className={`flex items-center text-xs font-semibold px-2 py-0.5 rounded border transition-all hover:shadow hover:translate-y-[-1px] ${activeSendTopic === topic.id
? "bg-indigo-600 text-white border-indigo-700 shadow-inner"
: "bg-white text-indigo-600 border-gray-200 shadow-sm"
}`}
className={`flex items-center text-xs font-semibold px-2 py-0.5 rounded border transition-all hover:shadow hover:translate-y-[-1px] ${
activeSendTopic === topic.id
? "bg-indigo-600 text-white border-indigo-700 shadow-inner"
: "bg-white text-indigo-600 border-gray-200 shadow-sm"
}`}
>
<Send className="w-3 h-3 mr-1" />
{activeSendTopic === topic.id
@@ -703,72 +709,71 @@ export default function TopicsView() {
</div>
</li>
)}
</ul >
</div >
</ul>
</div>
{
myRequests.length > 0 && (
<div className="mt-12">
<h3 className="text-lg font-bold text-gray-900 mb-4">My Requests</h3>
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{myRequests.map((req) => (
<li key={req.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-indigo-600 truncate">
{req.name}
</p>
<div className="flex items-center">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${req.status === "approved"
{myRequests.length > 0 && (
<div className="mt-12">
<h3 className="text-lg font-bold text-gray-900 mb-4">My Requests</h3>
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{myRequests.map((req) => (
<li key={req.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-indigo-600 truncate">
{req.name}
</p>
<div className="flex items-center">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
req.status === "approved"
? "bg-green-100 text-green-800"
: req.status === "rejected"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{req.status === "approved"
? "Approved"
: req.status === "rejected"
? "Rejected"
: "Pending"}
}`}
>
{req.status === "approved"
? "Approved"
: req.status === "rejected"
? "Rejected"
: "Pending"}
</span>
</div>
</div>
<div className="mt-2 text-sm text-gray-500">
<p>
Slug: <span className="font-mono">{req.slug}</span>
</p>
{req.description && (
<p className="mt-1">{req.description}</p>
)}
<p className="mt-1 text-xs text-gray-400">
Requested on:{" "}
{req.createdAt
? new Date(req.createdAt).toLocaleDateString()
: "Unknown"}
{req.approver && (
<span className="ml-2">
| Approved by: {req.approver.name}
</span>
</div>
</div>
<div className="mt-2 text-sm text-gray-500">
<p>
Slug: <span className="font-mono">{req.slug}</span>
</p>
{req.description && (
<p className="mt-1">{req.description}</p>
)}
<p className="mt-1 text-xs text-gray-400">
Requested on:{" "}
{req.createdAt
? new Date(req.createdAt).toLocaleDateString()
: "Unknown"}
{req.approver && (
<span className="ml-2">
| Approved by: {req.approver.name}
</span>
)}
</p>
</div>
</p>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
</li>
))}
</ul>
</div>
)
}
</div>
)}
< Modal
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={currentUser?.isAdmin ? "Add New Topic" : "Request New Topic"}
@@ -850,10 +855,11 @@ export default function TopicsView() {
</div>
{submitStatus && (
<div
className={`p-3 rounded-md text-sm ${submitStatus.type === "success"
? "bg-green-50 text-green-800"
: "bg-red-50 text-red-800"
}`}
className={`p-3 rounded-md text-sm ${
submitStatus.type === "success"
? "bg-green-50 text-green-800"
: "bg-red-50 text-red-800"
}`}
>
{submitStatus.message}
</div>
@@ -874,7 +880,7 @@ export default function TopicsView() {
</button>
</div>
</form>
</Modal >
</Modal>
<Modal
isOpen={isSubModalOpen}
@@ -934,16 +940,14 @@ export default function TopicsView() {
</div>
</Modal>
{
selectedTopic && (
<GroupBindingsModal
isOpen={isGroupModalOpen}
onClose={() => setIsGroupModalOpen(false)}
topicId={selectedTopic.id}
topicName={selectedTopic.name}
/>
)
}
</div >
{selectedTopic && (
<GroupBindingsModal
isOpen={isGroupModalOpen}
onClose={() => setIsGroupModalOpen(false)}
topicId={selectedTopic.id}
topicName={selectedTopic.name}
/>
)}
</div>
);
}