All the channel approval
This commit is contained in:
192
src/components/channel-users/channel-users.tsx
Normal file
192
src/components/channel-users/channel-users.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
/** Maps channel IDs to the instructions for finding the user ID. */
|
||||
const CHANNEL_ID_HELP: Record<string, string> = {
|
||||
telegram: "telegramIdHelp",
|
||||
discord: "discordIdHelp",
|
||||
email: "emailIdHelp",
|
||||
};
|
||||
|
||||
interface ChannelUsersProps {
|
||||
tenantName: string;
|
||||
/** Currently enabled channel packages (e.g. ["telegram", "discord"]) */
|
||||
enabledChannels: string[];
|
||||
/** Current channelUsers from the PiecedTenant spec */
|
||||
initialChannelUsers: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export function ChannelUsers({
|
||||
tenantName,
|
||||
enabledChannels,
|
||||
initialChannelUsers,
|
||||
}: ChannelUsersProps) {
|
||||
const t = useTranslations("channelUsers");
|
||||
const router = useRouter();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
||||
const [channelUsers, setChannelUsers] =
|
||||
useState<Record<string, string[]>>(initialChannelUsers);
|
||||
|
||||
const updateChannelUsers = useCallback(
|
||||
async (updated: Record<string, string[]>) => {
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch(`/api/tenants/${tenantName}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ channelUsers: updated }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Update failed");
|
||||
}
|
||||
setChannelUsers(updated);
|
||||
router.refresh();
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[tenantName, router]
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(
|
||||
(channel: string) => {
|
||||
const userId = inputValues[channel]?.trim();
|
||||
if (!userId) return;
|
||||
|
||||
const current = channelUsers[channel] || [];
|
||||
if (current.includes(userId)) {
|
||||
setError(t("alreadyAdded"));
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...channelUsers,
|
||||
[channel]: [...current, userId],
|
||||
};
|
||||
setInputValues((prev) => ({ ...prev, [channel]: "" }));
|
||||
updateChannelUsers(updated);
|
||||
},
|
||||
[channelUsers, inputValues, updateChannelUsers, t]
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(channel: string, userId: string) => {
|
||||
const current = channelUsers[channel] || [];
|
||||
const updated = {
|
||||
...channelUsers,
|
||||
[channel]: current.filter((id) => id !== userId),
|
||||
};
|
||||
updateChannelUsers(updated);
|
||||
},
|
||||
[channelUsers, updateChannelUsers]
|
||||
);
|
||||
|
||||
if (enabledChannels.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-1">
|
||||
{t("title")}
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted mb-4">{t("description")}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-xs text-red-400 bg-red-400/10 border border-red-400/20 rounded-lg px-3 py-2">
|
||||
{error}
|
||||
<button
|
||||
onClick={() => setError("")}
|
||||
className="ml-2 text-red-300 hover:text-red-200"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enabledChannels.map((channel) => {
|
||||
const users = channelUsers[channel] || [];
|
||||
const helpKey = CHANNEL_ID_HELP[channel];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={channel}
|
||||
className="bg-surface-2 border border-border rounded-lg p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-medium text-text-primary capitalize">
|
||||
{channel}
|
||||
</h4>
|
||||
<span className="text-xs text-text-muted tabular-nums">
|
||||
{users.length} {t("users")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{helpKey && (
|
||||
<p className="text-xs text-text-secondary bg-surface-1 border border-border rounded-lg p-3 mb-3 whitespace-pre-line">
|
||||
{t(helpKey)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Current users */}
|
||||
{users.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{users.map((userId) => (
|
||||
<span
|
||||
key={userId}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-mono bg-accent/10 text-accent border border-accent/20 rounded-full"
|
||||
>
|
||||
{userId}
|
||||
<button
|
||||
onClick={() => handleRemove(channel, userId)}
|
||||
disabled={saving}
|
||||
className="text-accent/60 hover:text-red-400 transition-colors disabled:opacity-50"
|
||||
title={t("remove")}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add user */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValues[channel] || ""}
|
||||
onChange={(e) =>
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
[channel]: e.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd(channel);
|
||||
}}
|
||||
placeholder={t("placeholder")}
|
||||
className="flex-1 px-3 py-2 bg-surface-1 border border-border rounded-lg text-sm text-text-primary font-mono placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleAdd(channel)}
|
||||
disabled={saving || !inputValues[channel]?.trim()}
|
||||
className="px-4 py-2 text-sm font-medium bg-accent text-white rounded-lg hover:bg-accent-dim transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? "…" : t("add")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user