← back to Open Seo
src/client/features/onboarding/OnboardingChat.tsx
129 lines
import { useMutation, useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { DEFAULT_LOCATION_CODE } from "@/shared/keyword-locations";
import { LocationSelect } from "@/client/components/LocationSelect";
import { useSession } from "@/lib/auth-client";
import { saveOnboardingSite } from "@/serverFunctions/onboardingChat";
import { OnboardingAccountMenu } from "./OnboardingAccountMenu";
import { OnboardingChatConversation } from "./OnboardingChatConversation";
import {
invalidateOnboardingChatState,
onboardingChatStateQueryOptions,
} from "./onboardingChatQueries";
// Full-viewport chat surface. Breaks out of the centered, padded AuthPageShell
// with `fixed inset-0` so the chat fills the screen. There's no header bar —
// the strategy's first message carries the context — and inner content is
// constrained to a narrow column for comfortable reading width.
function StrategyShell({ children }: { children: React.ReactNode }) {
return (
<div className="fixed inset-0 flex flex-col bg-base-100">{children}</div>
);
}
export function OnboardingChat() {
const stateQuery = useQuery(onboardingChatStateQueryOptions());
const { data: session } = useSession();
const accountMenu = <OnboardingAccountMenu email={session?.user?.email} />;
if (stateQuery.isError) {
return (
<StrategyShell>
<div className="flex flex-1 items-center justify-center p-6 text-sm text-error">
Couldn’t load your strategy. Please refresh to try again.
</div>
</StrategyShell>
);
}
if (!stateQuery.data) {
return (
<StrategyShell>
<div className="flex flex-1 items-center justify-center gap-2 p-6 text-sm text-base-content/60">
<Loader2 className="size-4 animate-spin" />
Loading…
</div>
</StrategyShell>
);
}
const { projectId, domain } = stateQuery.data;
return (
<StrategyShell>
{accountMenu}
{!domain ? (
<SiteForm projectId={projectId} />
) : (
<OnboardingChatConversation projectId={projectId} domain={domain} />
)}
</StrategyShell>
);
}
function SiteForm({ projectId }: { projectId: string }) {
const [domain, setDomain] = useState("");
const [locationCode, setLocationCode] = useState(DEFAULT_LOCATION_CODE);
const save = useMutation({
mutationFn: () =>
saveOnboardingSite({ data: { projectId, domain, locationCode } }),
onSuccess: invalidateOnboardingChatState,
});
return (
<div className="flex flex-1 items-center justify-center overflow-y-auto p-6">
<form
className="w-full max-w-md space-y-6"
onSubmit={(event) => {
event.preventDefault();
if (domain.trim()) {
save.mutate();
}
}}
>
<div className="space-y-3 text-center">
<img
src="/transparent-logo.png"
alt="OpenSEO"
className="mx-auto size-10 rounded-lg"
/>
<h1 className="text-xl font-semibold">Tell us about your website.</h1>
<p className="text-sm text-base-content/60">
If you have multiple websites, you can set that up later.
</p>
</div>
<div className="space-y-4 rounded-lg border border-base-300 bg-base-100 p-5 shadow-sm">
<label className="block space-y-1">
<span className="text-sm font-medium">Your website</span>
<input
type="text"
className="input input-bordered w-full"
placeholder="example.com"
value={domain}
onChange={(event) => setDomain(event.target.value)}
/>
</label>
<label className="block space-y-1">
<span className="text-sm font-medium">
This is the country we will use when getting SEO data.
</span>
<LocationSelect value={locationCode} onChange={setLocationCode} />
</label>
<button
type="submit"
className="btn btn-primary w-full"
disabled={!domain.trim() || save.isPending}
>
{save.isPending ? "Saving…" : "Continue"}
</button>
</div>
</form>
</div>
);
}