Files
kan/apps/web/src/views/settings/components/NewApiKeyModal.tsx
LovelessCodes 3e21b23f0a refactor: reorganize settings page with tabbed interface (#57)
* refactor: reorganize settings page with tabbed interface

* feat: revamp API key management with new list view and confirmation modals

* refactor: update tab styling

* refactor: tweak UI/UX for managing API keys

* refactor: only show update button when change has been made

* refactor: only show update button when content of display name has been updated

* feat: store tab state in params

* refactor: remove focus state from tabs

* refactor: tweak styling on mobile select

* refactor: simplify settings pages

* feat: open upgrade modal if upgrade=pro is in params

* feat: add scroll to api key list on mobile

* chore: add translations

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2025-09-14 15:48:55 +01:00

182 lines
5.6 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import {
HiInformationCircle,
HiMiniCheck,
HiOutlineDocumentDuplicate,
HiXMark,
} from "react-icons/hi2";
import { z } from "zod";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import Input from "~/components/Input";
import { useClipboard } from "~/hooks/useClipboard";
import { useModal } from "~/providers/modal";
const newApiKeySchema = z.object({
name: z
.string()
.min(1, { message: t`API key name is required` })
.max(30, { message: t`API key name cannot exceed 30 characters` }),
});
export default function NewApiKeyModal() {
const { closeModal } = useModal();
const { copied, copy } = useClipboard({ timeout: 2000 });
const [createdApiKey, setCreatedApiKey] = useState<{
key: string;
name: string;
} | null>(null);
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<z.infer<typeof newApiKeySchema>>({
resolver: zodResolver(newApiKeySchema),
defaultValues: {
name: "",
},
});
const qc = useQueryClient();
const createApiKeyMutation = useMutation({
mutationFn: ({ name }: { name: string }) =>
authClient.apiKey.create({ name, prefix: "kan_" }),
onSuccess: ({ data: apiKey }) => {
void qc.invalidateQueries({
queryKey: ["apiKeys"],
});
if (apiKey && apiKey.key && apiKey.name) {
setCreatedApiKey({
key: apiKey.key,
name: apiKey.name,
});
}
},
onError: () => {
// Handle error if needed
},
});
const onSubmit = (data: z.infer<typeof newApiKeySchema>) => {
createApiKeyMutation.mutate({ name: data.name });
};
useEffect(() => {
// Reset state and form when modal opens
setCreatedApiKey(null);
reset();
}, [reset]);
useEffect(() => {
if (!createdApiKey) {
const nameElement = document.querySelector<HTMLElement>("#name");
if (nameElement) nameElement.focus();
}
}, [createdApiKey]);
if (createdApiKey) {
return (
<div>
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
<h2 className="text-sm font-bold">{t`API key created`}</h2>
<button
type="button"
className="rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
}}
>
<HiXMark
size={18}
className="text-light-900 dark:text-dark-900"
/>
</button>
</div>
<div className="mb-4">
<div className="relative">
<Input
value={createdApiKey.key}
className="pr-10 text-sm text-light-900 dark:text-dark-900"
readOnly
/>
<button
type="button"
className="absolute inset-y-0 right-0 flex items-center pr-3 text-light-900 hover:text-light-950 dark:text-dark-900 dark:hover:text-dark-950"
onClick={() => copy(createdApiKey.key)}
>
{copied ? (
<HiMiniCheck className="h-5 w-5 text-green-600" />
) : (
<HiOutlineDocumentDuplicate className="h-5 w-5" />
)}
</button>
</div>
<div className="mt-2 flex items-start gap-1">
<HiInformationCircle className="mt-0.5 h-4 w-4 text-dark-900" />
<p className="text-xs text-gray-500 dark:text-dark-900">
{t`This API key will only be shown once. Please save it in a secure location.`}
</p>
</div>
</div>
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div>
<Button onClick={() => closeModal()}>{t`Close`}</Button>
</div>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
<h2 className="text-sm font-bold">{t`New API key`}</h2>
<button
type="button"
className="rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
}}
>
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
<Input
id="name"
placeholder={t`API key name`}
{...register("name", { required: true })}
errorMessage={errors.name?.message}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
await handleSubmit(onSubmit)();
}
}}
/>
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div>
<Button type="submit" isLoading={createApiKeyMutation.isPending}>
{t`Create API key`}
</Button>
</div>
</div>
</form>
);
}