mirror of
https://github.com/d0zingcat/alert-message-center.git
synced 2026-05-13 15:09:19 +00:00
@@ -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...");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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/
|
||||
|
||||
45
biome.json
45
biome.json
@@ -17,27 +17,40 @@
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"useButtonType": "off",
|
||||
"noStaticElementInteractions": "off",
|
||||
"useKeyWithClickEvents": "off",
|
||||
"noSvgWithoutTitle": "off",
|
||||
"noLabelWithoutControl": "off"
|
||||
"useButtonType": "error",
|
||||
"noStaticElementInteractions": "error",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"noSvgWithoutTitle": "warn",
|
||||
"noLabelWithoutControl": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAssignInExpressions": "off",
|
||||
"noExplicitAny": "off",
|
||||
"noImplicitAnyLet": "off",
|
||||
"noRedeclare": "off",
|
||||
"noUnknownAtRules": "off"
|
||||
"noAssignInExpressions": "error",
|
||||
"noExplicitAny": "warn",
|
||||
"noImplicitAnyLet": "error",
|
||||
"noRedeclare": "error",
|
||||
"noUnknownAtRules": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"ignore": [
|
||||
"tailwind",
|
||||
"layer",
|
||||
"apply",
|
||||
"variants",
|
||||
"responsive",
|
||||
"screen",
|
||||
"config"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"useNodejsImportProtocol": "off"
|
||||
"noNonNullAssertion": "warn",
|
||||
"useNodejsImportProtocol": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "off",
|
||||
"noUnusedVariables": "off",
|
||||
"noUnusedImports": "off"
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedImports": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -59,4 +72,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user