feat: add lint

Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
2026-01-14 20:21:22 +08:00
parent 451793f6ce
commit d38b75fb66
14 changed files with 151 additions and 84 deletions

View File

@@ -1,6 +1,3 @@
// Simulate topic creation
import { client } from "./client"; // This won't work in node script easily due to frontend dependencies
// Let's use fetch directly against the server
async function run() {
console.log("Creating pending topic...");

View File

@@ -21,7 +21,7 @@ webhook.post("/:token/topic/:slug", async (c) => {
logger.warn({ token }, "[Webhook] Invalid personal token");
return c.json({ error: "Invalid personal token" }, 401);
}
let body;
let body: any;
try {
const rawBody = await c.req.text();
logger.debug({ bodyLength: rawBody.length }, "[Webhook] Received raw body");
@@ -248,14 +248,14 @@ webhook.post("/:token/dm", async (c) => {
return c.json({ error: "User has no Feishu ID linked" }, 400);
}
let body;
let body: any;
try {
const rawBody = await c.req.text();
if (!rawBody || rawBody.trim() === "") {
return c.json({ error: "Empty body" }, 400);
}
body = JSON.parse(rawBody);
} catch (e) {
} catch (_e) {
return c.json({ error: "Invalid JSON body" }, 400);
}

View File

@@ -46,6 +46,7 @@ function App() {
Please sign in with Feishu to continue
</p>
<button
type="button"
onClick={login}
className="w-full flex items-center justify-center bg-indigo-600 text-white py-3 px-4 rounded-lg hover:bg-indigo-700 transition"
>
@@ -73,6 +74,7 @@ function App() {
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
{user.isAdmin && (
<button
type="button"
onClick={() => setActiveTab("admin")}
className={`${
activeTab === "admin"
@@ -85,6 +87,7 @@ function App() {
</button>
)}
<button
type="button"
onClick={() => setActiveTab("topics")}
className={`${
activeTab === "topics"
@@ -97,6 +100,7 @@ function App() {
</button>
{user.isAdmin && (
<button
type="button"
onClick={() => setActiveTab("users")}
className={`${
activeTab === "users"
@@ -119,6 +123,7 @@ function App() {
)}
</div>
<button
type="button"
onClick={logout}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100"
>

View File

@@ -1,5 +1,5 @@
import { MessageCircle, Plus, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { client } from "../lib/client";
import Modal from "./Modal";
@@ -38,16 +38,7 @@ export default function GroupBindingsModal({
message: string;
} | null>(null);
useEffect(() => {
if (isOpen && topicId) {
fetchBindings();
fetchKnownGroups();
setStatus(null);
setSelectedChatId("");
}
}, [isOpen, topicId]);
const fetchBindings = async () => {
const fetchBindings = useCallback(async () => {
try {
const res = await client.api.topics[":id"].groups.$get(
{
@@ -62,20 +53,28 @@ export default function GroupBindingsModal({
} catch (err) {
console.error(err);
}
};
}, [topicId]);
const fetchKnownGroups = async () => {
const fetchKnownGroups = useCallback(async () => {
try {
const res = await client.api.groups.$get(undefined, {
init: { credentials: "include" },
});
const data = await res.json();
// Only verify uniqueness if needed, but here we just list what server returns
setKnownGroups(data as any);
} catch (err) {
console.error(err);
}
};
}, []);
useEffect(() => {
if (isOpen && topicId) {
fetchBindings();
fetchKnownGroups();
setStatus(null);
setSelectedChatId("");
}
}, [isOpen, topicId, fetchBindings, fetchKnownGroups]);
const handleBind = async () => {
if (!selectedChatId) return;
@@ -170,6 +169,7 @@ export default function GroupBindingsModal({
</span>
</div>
<button
type="button"
onClick={() => handleUnbind(binding.id)}
className="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove binding"
@@ -206,6 +206,7 @@ export default function GroupBindingsModal({
))}
</select>
<button
type="button"
onClick={handleBind}
disabled={!selectedChatId || loading}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"

View File

@@ -20,10 +20,13 @@ export default function Modal({
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div
className="absolute inset-0 bg-gray-500 opacity-75"
<button
type="button"
className="absolute inset-0 bg-gray-500 opacity-75 w-full h-full cursor-default"
onClick={onClose}
></div>
onKeyDown={(e) => e.key === "Escape" && onClose()}
aria-label="Close modal"
/>
</div>
<span
@@ -43,6 +46,7 @@ export default function Modal({
{title}
</h3>
<button
type="button"
onClick={onClose}
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none"
>

View File

@@ -1,4 +1,5 @@
import { hc } from "hono/client";
import type { AppType } from "../../../server/src/index";
// biome-ignore lint/suspicious/noExplicitAny: Hono client types can be complex
export const client = hc<AppType>("/") as any;

View File

@@ -8,10 +8,13 @@ import "./index.css";
// Simple routing based on pathname
const pathname = window.location.pathname;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<AuthProvider>
{pathname === "/auth/callback" ? <AuthCallback /> : <App />}
</AuthProvider>
</React.StrictMode>,
);
const rootElement = document.getElementById("root");
if (rootElement) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<AuthProvider>
{pathname === "/auth/callback" ? <AuthCallback /> : <App />}
</AuthProvider>
</React.StrictMode>,
);
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { client } from "../lib/client";
import SystemLoadView from "./SystemLoadView";
@@ -15,6 +15,7 @@ export default function AdminView() {
<div className="border-b border-gray-200 mb-6">
<nav className="-mb-px flex space-x-8">
<button
type="button"
onClick={() => setActiveTab("load")}
className={`${
activeTab === "load"
@@ -25,6 +26,7 @@ export default function AdminView() {
System Load
</button>
<button
type="button"
onClick={() => setActiveTab("requests")}
className={`${
activeTab === "requests"
@@ -35,6 +37,7 @@ export default function AdminView() {
Topic Requests
</button>
<button
type="button"
onClick={() => setActiveTab("topics")}
className={`${
activeTab === "topics"
@@ -59,7 +62,7 @@ function TopicsManagement() {
const [topics, setTopics] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const fetchAllTopics = async () => {
const fetchAllTopics = useCallback(async () => {
setLoading(true);
try {
const res = await client.api.topics.all.$get(undefined, {
@@ -72,11 +75,11 @@ function TopicsManagement() {
} finally {
setLoading(false);
}
};
}, []);
useEffect(() => {
fetchAllTopics();
}, []);
}, [fetchAllTopics]);
const handleDelete = async (id: string, name: string) => {
if (
@@ -160,6 +163,7 @@ function TopicsManagement() {
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
type="button"
onClick={() => handleDelete(topic.id, topic.name)}
className="text-red-600 hover:text-red-900"
>
@@ -178,7 +182,7 @@ function TopicRequestsList() {
const [requests, setRequests] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const fetchRequests = async () => {
const fetchRequests = useCallback(async () => {
setLoading(true);
try {
const res = await client.api.topics.requests.$get(undefined, {
@@ -191,11 +195,11 @@ function TopicRequestsList() {
} finally {
setLoading(false);
}
};
}, []);
useEffect(() => {
fetchRequests();
}, []);
}, [fetchRequests]);
const handleAction = async (
id: string,
@@ -259,18 +263,21 @@ function TopicRequestsList() {
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleAction(req.id, "approve")}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm font-medium shadow-sm transition-colors"
>
Approve
</button>
<button
type="button"
onClick={() => handleAction(req.id, "reject")}
className="px-4 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200 text-sm font-medium transition-colors"
>
Reject
</button>
<button
type="button"
onClick={() => handleAction(req.id, "delete", req.name)}
className="px-4 py-2 border border-gray-300 text-gray-600 rounded hover:bg-gray-50 text-sm font-medium transition-colors"
>

View File

@@ -60,7 +60,10 @@ export default function AuthCallback() {
</h2>
<p className="text-gray-700">{error}</p>
<button
onClick={() => (window.location.href = "/")}
type="button"
onClick={() => {
window.location.href = "/";
}}
className="mt-4 w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700"
>
Return to Home

View File

@@ -1,5 +1,5 @@
import { Activity, BarChart3, CheckCircle, Clock, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { client } from "../lib/client";
interface Stats {
@@ -24,7 +24,7 @@ export default function SystemLoadView() {
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
const fetchStats = async () => {
const fetchStats = useCallback(async () => {
try {
const res = await client.api.stats.$get(undefined, {
init: { credentials: "include" },
@@ -47,13 +47,13 @@ export default function SystemLoadView() {
} finally {
setLoading(false);
}
};
}, []);
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 10000); // 10s refresh for dynamic feel
return () => clearInterval(interval);
}, []);
}, [fetchStats]);
if (loading)
return (

View File

@@ -9,13 +9,13 @@ import {
UserPlus,
Users,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import GroupBindingsModal from "../components/GroupBindingsModal";
import Modal from "../components/Modal";
import { useAuth } from "../contexts/AuthContext";
import { client } from "../lib/client";
interface User {
interface TopicUser {
id: string;
name: string;
email?: string | null;
@@ -23,7 +23,7 @@ interface User {
interface Subscription {
userId: string;
user: User;
user: TopicUser;
}
interface Topic {
@@ -32,8 +32,8 @@ interface Topic {
slug: string;
description?: string;
subscriptions: Subscription[];
creator?: User;
approver?: User;
creator?: TopicUser;
approver?: TopicUser;
createdBy?: string;
}
@@ -41,7 +41,7 @@ export default function TopicsView() {
const { user: currentUser } = useAuth();
const [topics, setTopics] = useState<Topic[]>([]);
const [myRequests, setMyRequests] = useState<any[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [users, setUsers] = useState<TopicUser[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSubModalOpen, setIsSubModalOpen] = useState(false);
@@ -59,7 +59,7 @@ export default function TopicsView() {
message: string;
} | null>(null);
const fetchTopics = async () => {
const fetchTopics = useCallback(async () => {
setLoading(true);
try {
const res = await client.api.topics.$get(undefined, {
@@ -72,9 +72,9 @@ export default function TopicsView() {
} finally {
setLoading(false);
}
};
}, []);
const fetchMyRequests = async () => {
const fetchMyRequests = useCallback(async () => {
try {
const res = await client.api.topics["my-requests"].$get(undefined, {
init: { credentials: "include" },
@@ -84,19 +84,19 @@ export default function TopicsView() {
} catch (err) {
console.error(err);
}
};
}, []);
const fetchUsers = async () => {
const fetchUsers = useCallback(async () => {
try {
const res = await client.api.users.$get(undefined, {
init: { credentials: "include" },
});
const data = await res.json();
setUsers(data as unknown as User[]);
setUsers(data as unknown as TopicUser[]);
} catch (err) {
console.error(err);
}
};
}, []);
useEffect(() => {
fetchTopics();
@@ -104,7 +104,7 @@ export default function TopicsView() {
if (currentUser?.isAdmin) {
fetchUsers();
}
}, [currentUser]);
}, [currentUser, fetchMyRequests, fetchTopics, fetchUsers]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -314,6 +314,7 @@ export default function TopicsView() {
Inbox Webhook URL
</span>
<button
type="button"
onClick={() =>
copyToClipboard(getDmWebhookUrl(), "personal-dm")
}
@@ -357,6 +358,7 @@ export default function TopicsView() {
<div className="flex gap-2">
{currentUser && (
<button
type="button"
onClick={() => setIsModalOpen(true)}
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 flex items-center"
>
@@ -380,6 +382,7 @@ export default function TopicsView() {
</p>
<div className="flex items-center space-x-2">
<button
type="button"
onClick={() => handleSelfSubscribe(topic)}
className={`inline-flex items-center px-3 py-1 border text-xs font-medium rounded-md ${
isSubscribed(topic)
@@ -405,6 +408,7 @@ export default function TopicsView() {
<>
{currentUser.isAdmin && (
<button
type="button"
onClick={() => handleSubscriptionClick(topic)}
className="text-gray-400 hover:text-gray-500"
title="Manage Subscriptions"
@@ -413,6 +417,7 @@ export default function TopicsView() {
</button>
)}
<button
type="button"
onClick={() => handleGroupClick(topic)}
className="text-gray-400 hover:text-gray-500"
title="Manage Group Chats"
@@ -465,6 +470,7 @@ export default function TopicsView() {
Your Personal Webhook
</span>
<button
type="button"
onClick={() =>
copyToClipboard(
getWebhookUrl(topic.slug),
@@ -584,10 +590,14 @@ export default function TopicsView() {
>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
<label
htmlFor="topic-name"
className="block text-sm font-medium text-gray-700"
>
Name
</label>
<input
id="topic-name"
type="text"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
@@ -598,10 +608,14 @@ export default function TopicsView() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<label
htmlFor="topic-slug"
className="block text-sm font-medium text-gray-700"
>
Slug (Unique ID)
</label>
<input
id="topic-slug"
type="text"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
@@ -612,10 +626,14 @@ export default function TopicsView() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<label
htmlFor="topic-description"
className="block text-sm font-medium text-gray-700"
>
Description
</label>
<textarea
id="topic-description"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
value={formData.description}
onChange={(e) =>
@@ -700,6 +718,7 @@ export default function TopicsView() {
</div>
<div className="mt-6 flex justify-end">
<button
type="button"
onClick={() => setIsSubModalOpen(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>

View File

@@ -1,5 +1,5 @@
import { Plus, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import Modal from "../components/Modal";
import { useAuth } from "../contexts/AuthContext";
import { client } from "../lib/client";
@@ -23,7 +23,7 @@ export default function UsersView() {
email: "",
});
const fetchUsers = async () => {
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
const res = await client.api.users.$get(undefined, {
@@ -36,11 +36,11 @@ export default function UsersView() {
console.error(err);
setLoading(false);
}
};
}, []);
useEffect(() => {
fetchUsers();
}, []);
}, [fetchUsers]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -89,6 +89,7 @@ export default function UsersView() {
<h2 className="text-2xl font-bold text-gray-900">Users</h2>
{currentUser?.isAdmin && (
<button
type="button"
onClick={() => setIsModalOpen(true)}
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 flex items-center"
>
@@ -131,6 +132,7 @@ export default function UsersView() {
{currentUser?.isAdmin && (
<div className="ml-4 flex items-center space-x-2">
<button
type="button"
onClick={() => handleDelete(user.id)}
className="text-red-600 hover:text-red-900 p-2"
>
@@ -157,10 +159,14 @@ export default function UsersView() {
>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
<label
htmlFor="user-name"
className="block text-sm font-medium text-gray-700"
>
Name
</label>
<input
id="user-name"
type="text"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
@@ -171,10 +177,14 @@ export default function UsersView() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<label
htmlFor="user-feishu"
className="block text-sm font-medium text-gray-700"
>
Feishu User ID
</label>
<input
id="user-feishu"
type="text"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
value={formData.feishuUserId}
@@ -184,10 +194,14 @@ export default function UsersView() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<label
htmlFor="user-email"
className="block text-sm font-medium text-gray-700"
>
Email
</label>
<input
id="user-email"
type="email"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"
value={formData.email}

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import react from "@vitejs/plugin-react";
import path from "path";
import { defineConfig } from "vite";
// https://vitejs.dev/config/