Files
smart-home/src/features/topics/index.tsx

192 lines
6.5 KiB
TypeScript

import { PlusIcon } from "lucide-react";
import { useState, type FormEvent } from "react";
import { cn } from "../../utils/classname";
import StateSection from "./sections/state";
import CommandSection from "./sections/command";
import { useCreateTopic } from "./hooks/mutations";
import { getMerchant } from "../../utils/storage";
import { ENV } from "../../constants/env";
export default function TopicsFeature() {
const [tab, setTab] = useState("state");
const [isOpen, setIsOpen] = useState(false);
const [form, setForm] = useState<TopicPayload>({
topic: "",
type: "state-reply",
});
const [error, setError] = useState("");
const createTopic = useCreateTopic();
const merchant = getMerchant();
const expectedCredential = ENV.credential?.trim();
const hasCredential = Boolean(merchant?.credential?.trim());
const isCredentialMatch =
Boolean(expectedCredential) &&
merchant?.credential?.trim() === expectedCredential;
const isMerchantSetup =
Boolean(merchant?.merchantName?.trim()) &&
Boolean(merchant?.tower?.trim()) &&
Boolean(merchant?.floor?.trim()) &&
Boolean(merchant?.unit?.trim());
const isReady = isMerchantSetup && hasCredential && isCredentialMatch;
const handleOpen = () => {
setError("");
setForm({
topic: "",
type: tab === "state" ? "state-reply" : "commands",
});
setIsOpen(true);
};
const handleClose = () => {
if (createTopic.isPending) return;
setIsOpen(false);
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const topic = form.topic.trim();
if (!topic) {
setError("Topic wajib diisi.");
return;
}
createTopic.mutate(
{ topic, type: form.type },
{
onSuccess: () => {
setIsOpen(false);
setForm({
topic: "",
type: tab === "state" ? "state-reply" : "commands",
});
},
onError: () => {
setError("Gagal menambah topic.");
},
},
);
};
return (
<div className="flex flex-col gap-4">
{!isReady && (
<div className="mx-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
{!isMerchantSetup && (
<span>
Silakan setup merchant terlebih dahulu di halaman Settings sebelum
mengakses Topics.
</span>
)}
{isMerchantSetup && !hasCredential && (
<span>
Silakan isi credential terlebih dahulu di halaman Settings sebelum
mengakses Topics.
</span>
)}
{isMerchantSetup && hasCredential && !isCredentialMatch && (
<span>Credential tidak sesuai.</span>
)}
</div>
)}
<div className="p-4 flex justify-end">
<button
onClick={handleOpen}
className="shrink-0 px-4 py-2 rounded-md flex items-center justify-center transition-all bg-orange-500 text-white disabled:cursor-not-allowed disabled:opacity-60"
disabled={!isReady}
>
<PlusIcon />
Tambah Topic
</button>
</div>
<div className="flex justify-between">
<button
onClick={() => setTab("state")}
className={cn(
"w-full text-center p-x2 py-4 border-b-2 border-transparent",
tab === "state" && "border-amber-500",
)}
>
state-reply
</button>
<button
onClick={() => setTab("command")}
className={cn(
"w-full text-center p-x2 py-4 border-b-2 border-transparent",
tab === "command" && "border-amber-500",
)}
>
commands
</button>
</div>
{tab === "state" && <StateSection />}
{tab === "command" && <CommandSection />}
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-md rounded-md bg-white p-4 shadow-lg">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Tambah Topic</h2>
<button
onClick={handleClose}
className="text-sm text-neutral-500"
type="button"
>
Tutup
</button>
</div>
<form onSubmit={handleSubmit} className="mt-4 flex flex-col gap-3">
<label className="text-sm font-medium text-neutral-700">
Topic
<input
className="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm outline-none focus:border-amber-500"
placeholder="Masukkan topic"
value={form.topic}
onChange={(event) =>
setForm((prev) => ({ ...prev, topic: event.target.value }))
}
/>
</label>
<label className="text-sm font-medium text-neutral-700">
Type
<select
className="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm outline-none focus:border-amber-500"
value={form.type}
onChange={(event) =>
setForm((prev) => ({
...prev,
type: event.target.value as TopicPayload["type"],
}))
}
>
<option value="state-reply">state-reply</option>
<option value="commands">commands</option>
</select>
</label>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<div className="mt-2 flex justify-end gap-2">
<button
type="button"
onClick={handleClose}
className="rounded-md border border-neutral-300 px-4 py-2 text-sm"
disabled={createTopic.isPending}
>
Batal
</button>
<button
type="submit"
className="rounded-md bg-amber-500 px-4 py-2 text-sm text-white disabled:cursor-not-allowed disabled:opacity-70"
disabled={createTopic.isPending}
>
{createTopic.isPending ? "Menyimpan..." : "Simpan"}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}