Alternative web application for the
pdsadmin command
1import type { MouseEvent } from "react";
2import { useState } from "react";
3import { useTranslation } from "react-i18next";
4
5import { useOpenModal } from "../../atoms/modal";
6import { usePDS, usePDSHostname } from "../../atoms/pds";
7import { cn } from "../../utils/cn";
8
9type PDSButtonProps = {
10 type: "request-crawl" | "logout" | "create-account";
11 iconClassName: string;
12 children: string;
13};
14
15function PDSButton({ type, iconClassName, children }: PDSButtonProps) {
16 const openModal = useOpenModal();
17 return (
18 <button
19 className="btn btn-ghost h-12 shadow"
20 onClick={() => openModal({ type })}
21 data-testid={`${type}-button`}
22 >
23 <span className={cn("size-4", iconClassName)}></span>
24 <span className="truncate">{children}</span>
25 </button>
26 );
27}
28
29function CreateInviteCodeButton() {
30 const pds = usePDS();
31 const openModal = useOpenModal();
32 const [loading, setLoading] = useState(false);
33 const { t } = useTranslation();
34
35 const handleCreateInviteCode = async (e: MouseEvent) => {
36 e.stopPropagation();
37 setLoading(true);
38 try {
39 const code = await pds.createInviteCode();
40 openModal({ type: "invite-code", code });
41 } catch (error) {
42 alert(t("pds-menu.alerts.error", { message: String(error) }));
43 } finally {
44 setLoading(false);
45 }
46 };
47
48 return (
49 <div
50 // button要素を使うとドロップダウンが閉じてしまうのでdivを使う
51 role="button"
52 className={cn("btn btn-ghost h-12 shadow", {
53 "pointer-events-none": loading,
54 "pointer-events-auto": !loading,
55 })}
56 onClick={handleCreateInviteCode}
57 data-testid="create-invite-code-button"
58 >
59 {loading && (
60 <div className="loading loading-spinner loading-sm absolute"></div>
61 )}
62 <span className="i-lucide-ticket size-4"></span>
63 <span className={cn("truncate", loading && "opacity-0")}>
64 {t("pds-menu.buttons.create-invite-code")}
65 </span>
66 </div>
67 );
68}
69
70export function PDSMenu() {
71 const pdsHostname = usePDSHostname();
72 const { t } = useTranslation();
73
74 return (
75 <div className="card rounded-box bg-base-100 p-2 shadow-md">
76 <div className="flex justify-center py-4">
77 PDS:
78 <span className="ml-2 font-bold">{pdsHostname}</span>
79 </div>
80 <div className="grid grid-cols-2 grid-rows-2 gap-2">
81 <PDSButton type="create-account" iconClassName="i-lucide-user-plus">
82 {t("pds-menu.buttons.create-account")}
83 </PDSButton>
84 <CreateInviteCodeButton />
85 <PDSButton type="request-crawl" iconClassName="i-lucide-refresh-cw">
86 {t("pds-menu.buttons.request-crawl")}
87 </PDSButton>
88 <PDSButton type="logout" iconClassName="i-lucide-log-out">
89 {t("pds-menu.buttons.sign-out")}
90 </PDSButton>
91 </div>
92 </div>
93 );
94}