FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Loader2, Minus, Plus } from "lucide-react";
|
|
import {
|
|
useCreateNotificationDestination,
|
|
useUnlinkNotificationDestinationCompany,
|
|
} from "@/hooks/use-notification-destinations";
|
|
import type { NotificationDestinationResponse, NotificationType } from "@/lib/types";
|
|
|
|
/**
|
|
* A simplified single-destination view over the full notification
|
|
* destinations list, scoped to one company - shows the first destination
|
|
* of `type` linked to this company (if any) with an add/remove control.
|
|
* Shares the same React Query cache as the Settings page's full
|
|
* multi-destination UI, so changes here show up there instantly.
|
|
*/
|
|
export function NotificationChannelBox({
|
|
type,
|
|
label,
|
|
placeholder,
|
|
companyId,
|
|
destination,
|
|
disabled,
|
|
disabledReason,
|
|
}: {
|
|
type: NotificationType;
|
|
label: string;
|
|
placeholder: string;
|
|
companyId: string;
|
|
destination: NotificationDestinationResponse | undefined;
|
|
disabled?: boolean;
|
|
disabledReason?: string;
|
|
}) {
|
|
const [value, setValue] = useState("");
|
|
const create = useCreateNotificationDestination();
|
|
const unlink = useUnlinkNotificationDestinationCompany();
|
|
|
|
return (
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700">{label}</label>
|
|
<div className="mt-1 flex items-center gap-2">
|
|
<input
|
|
value={destination ? destination.destination_value : value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
readOnly={Boolean(destination)}
|
|
disabled={disabled}
|
|
placeholder={placeholder}
|
|
className="focus-ring block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm transition-colors duration-150 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400"
|
|
/>
|
|
{destination ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => unlink.mutate({ destinationId: destination.id, companyId })}
|
|
disabled={disabled || unlink.isPending}
|
|
title={`Remove this ${label.toLowerCase()}`}
|
|
className="focus-ring inline-flex shrink-0 items-center justify-center rounded-md border border-red-300 p-2 text-red-600 transition-colors duration-150 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-40"
|
|
>
|
|
{unlink.isPending ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
|
) : (
|
|
<Minus className="h-4 w-4" aria-hidden />
|
|
)}
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (!value.trim()) return;
|
|
create.mutate(
|
|
{ type, destination_value: value.trim(), company_ids: [companyId] },
|
|
{ onSuccess: () => setValue("") },
|
|
);
|
|
}}
|
|
disabled={disabled || !value.trim() || create.isPending}
|
|
title={`Add this ${label.toLowerCase()}`}
|
|
className="focus-ring inline-flex shrink-0 items-center justify-center rounded-md border border-green-300 p-2 text-green-700 transition-colors duration-150 hover:bg-green-50 disabled:cursor-not-allowed disabled:opacity-40"
|
|
>
|
|
{create.isPending ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
|
) : (
|
|
<Plus className="h-4 w-4" aria-hidden />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{disabled && disabledReason && <p className="mt-1 text-xs text-slate-500">{disabledReason}</p>}
|
|
</div>
|
|
);
|
|
}
|