Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/seo/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthedRouteRouteImport } from './routes/_authed/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiSplatRouteImport } from './routes/api/$'
import { Route as AuthedAdminRouteRouteImport } from './routes/_authed/admin/route'
import { Route as AuthedOrganizationSlugRouteRouteImport } from './routes/_authed/$organizationSlug/route'
import { Route as AuthedOnboardingIndexRouteImport } from './routes/_authed/onboarding/index'
import { Route as AuthedOrganizationSlugIndexRouteImport } from './routes/_authed/$organizationSlug/index'
Expand Down Expand Up @@ -54,6 +55,11 @@ const ApiSplatRoute = ApiSplatRouteImport.update({
path: '/api/$',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedAdminRouteRoute = AuthedAdminRouteRouteImport.update({
id: '/admin',
path: '/admin',
getParentRoute: () => AuthedRouteRoute,
} as any)
const AuthedOrganizationSlugRouteRoute =
AuthedOrganizationSlugRouteRouteImport.update({
id: '/$organizationSlug',
Expand Down Expand Up @@ -187,6 +193,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/$organizationSlug': typeof AuthedOrganizationSlugRouteRouteWithChildren
'/admin': typeof AuthedAdminRouteRoute
'/api/$': typeof ApiSplatRoute
'/$organizationSlug/$projectSlug': typeof AuthedOrganizationSlugProjectSlugRouteRouteWithChildren
'/invite/$invitationId': typeof AuthedInviteInvitationIdRoute
Expand All @@ -212,6 +219,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/admin': typeof AuthedAdminRouteRoute
'/api/$': typeof ApiSplatRoute
'/invite/$invitationId': typeof AuthedInviteInvitationIdRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
Expand All @@ -238,6 +246,7 @@ export interface FileRoutesById {
'/_authed': typeof AuthedRouteRouteWithChildren
'/login': typeof LoginRoute
'/_authed/$organizationSlug': typeof AuthedOrganizationSlugRouteRouteWithChildren
'/_authed/admin': typeof AuthedAdminRouteRoute
'/api/$': typeof ApiSplatRoute
'/_authed/$organizationSlug/$projectSlug': typeof AuthedOrganizationSlugProjectSlugRouteRouteWithChildren
'/_authed/invite/$invitationId': typeof AuthedInviteInvitationIdRoute
Expand Down Expand Up @@ -266,6 +275,7 @@ export interface FileRouteTypes {
| '/'
| '/login'
| '/$organizationSlug'
| '/admin'
| '/api/$'
| '/$organizationSlug/$projectSlug'
| '/invite/$invitationId'
Expand All @@ -291,6 +301,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/login'
| '/admin'
| '/api/$'
| '/invite/$invitationId'
| '/api/rpc/$'
Expand All @@ -316,6 +327,7 @@ export interface FileRouteTypes {
| '/_authed'
| '/login'
| '/_authed/$organizationSlug'
| '/_authed/admin'
| '/api/$'
| '/_authed/$organizationSlug/$projectSlug'
| '/_authed/invite/$invitationId'
Expand Down Expand Up @@ -377,6 +389,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/admin': {
id: '/_authed/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AuthedAdminRouteRouteImport
parentRoute: typeof AuthedRouteRoute
}
'/_authed/$organizationSlug': {
id: '/_authed/$organizationSlug'
path: '/$organizationSlug'
Expand Down Expand Up @@ -615,13 +634,15 @@ const AuthedOrganizationSlugRouteRouteWithChildren =

interface AuthedRouteRouteChildren {
AuthedOrganizationSlugRouteRoute: typeof AuthedOrganizationSlugRouteRouteWithChildren
AuthedAdminRouteRoute: typeof AuthedAdminRouteRoute
AuthedInviteInvitationIdRoute: typeof AuthedInviteInvitationIdRoute
AuthedOnboardingIndexRoute: typeof AuthedOnboardingIndexRoute
}

const AuthedRouteRouteChildren: AuthedRouteRouteChildren = {
AuthedOrganizationSlugRouteRoute:
AuthedOrganizationSlugRouteRouteWithChildren,
AuthedAdminRouteRoute: AuthedAdminRouteRoute,
AuthedInviteInvitationIdRoute: AuthedInviteInvitationIdRoute,
AuthedOnboardingIndexRoute: AuthedOnboardingIndexRoute,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export function ManageStrategyDialog({
id={`${fieldPrefix}-description`}
placeholder="What will you do and how will it work?"
rows={8}
value={field.value || ""}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
Expand Down
292 changes: 292 additions & 0 deletions apps/seo/src/routes/_authed/admin/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import { Button } from "@rectangular-labs/ui/components/ui/button";
import { Input } from "@rectangular-labs/ui/components/ui/input";
import { Label } from "@rectangular-labs/ui/components/ui/label";
import { toast } from "@rectangular-labs/ui/components/ui/sonner";
import { Textarea } from "@rectangular-labs/ui/components/ui/textarea";
import { useMutation } from "@tanstack/react-query";
import { createFileRoute, notFound } from "@tanstack/react-router";
import { useState } from "react";
import { getApiClientRq } from "~/lib/api";

export const Route = createFileRoute("/_authed/admin")({
beforeLoad: ({ context }) => {
if (!context.user?.email?.endsWith("fluidposts.com")) {
throw notFound();
}
},
component: RouteComponent,
});

function RouteComponent() {
const api = getApiClientRq();
const [organizationSlug, setOrganizationSlug] = useState("");
const [projectSlug, setProjectSlug] = useState("");
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both forms share the same projectSlug state variable. When a user enters a project slug in one form, it will appear in both forms. This creates a confusing user experience. Each form should have its own independent state variable for the project slug.

Copilot uses AI. Check for mistakes.
const [instructions, setInstructions] = useState("");
const [phaseStrategyName, setPhaseStrategyName] = useState("");

const { mutate: triggerOnboarding, isPending } = useMutation(
api.admin.triggerOnboardingTask.mutationOptions({
onSuccess: () => {
toast.success("Triggered onboarding workflow successfully.");
},
onError: (error) => {
toast.error(error.message);
},
}),
);

const { mutate: triggerStrategySuggestions, isPending: isPendingStrategy } =
useMutation(
api.admin.triggerStrategySuggestionsTask.mutationOptions({
onSuccess: () => {
toast.success(
"Triggered strategy suggestions workflow successfully.",
);
setInstructions("");
},
onError: (error) => {
toast.error(error.message);
},
}),
);

const { mutate: triggerStrategyPhase, isPending: isPendingStrategyPhase } =
useMutation(
api.admin.triggerStrategyPhaseGenerationTask.mutationOptions({
onSuccess: () => {
toast.success(
"Triggered strategy phase generation workflow successfully.",
);
setPhaseStrategyName("");
},
onError: (error) => {
toast.error(error.message);
},
}),
);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!organizationSlug.trim()) {
toast.error("Please enter an organization slug");
return;
}
if (!projectSlug.trim()) {
toast.error("Please enter a project slug");
return;
}
triggerOnboarding({ organizationSlug, projectSlug });
};

const handleStrategySubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!organizationSlug.trim()) {
toast.error("Please enter an organization slug");
return;
}
if (!projectSlug.trim()) {
toast.error("Please enter a project slug");
return;
}
triggerStrategySuggestions({
organizationSlug,
projectSlug,
instructions,
});
};

const handleStrategyPhaseSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!organizationSlug.trim()) {
toast.error("Please enter an organization slug");
return;
}
if (!projectSlug.trim()) {
toast.error("Please enter a project slug");
return;
}
if (!phaseStrategyName.trim()) {
toast.error("Please enter a strategy name");
return;
}

triggerStrategyPhase({
organizationSlug,
projectSlug,
strategyName: phaseStrategyName,
});
};

return (
<main className="mx-auto flex w-full max-w-3xl flex-col items-center space-y-6 p-6">
<div className="flex w-full max-w-md flex-col gap-2">
<h1 className="font-bold text-3xl tracking-tight">Admin</h1>
<p className="text-muted-foreground">
Internal tools for the fluidposts team.
</p>
</div>

<div className="w-full max-w-md rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold leading-none tracking-tight">
Trigger Onboarding Task
</h3>
<p className="text-muted-foreground text-sm">
Trigger the onboarding workflow in the api-seo package for a
specific project.
</p>
</div>
<div className="p-6 pt-0">
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="organizationSlug">Organization Slug</Label>
<Input
disabled={isPending}
id="organizationSlug"
onChange={(e) => setOrganizationSlug(e.target.value)}
placeholder="e.g. acme"
value={organizationSlug}
/>
</div>
<div className="space-y-2">
<Label htmlFor="projectSlug">Project Slug</Label>
<Input
disabled={isPending}
id="projectSlug"
onChange={(e) => setProjectSlug(e.target.value)}
placeholder="e.g. acme-corp"
value={projectSlug}
/>
</div>
<Button
disabled={
!organizationSlug.trim() || !projectSlug.trim() || isPending
}
isLoading={isPending}
type="submit"
>
Trigger Workflow
</Button>
</form>
</div>
</div>

<div className="w-full max-w-md rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold leading-none tracking-tight">
Trigger Strategy Suggestions Task
</h3>
<p className="text-muted-foreground text-sm">
Trigger the strategy suggestions workflow in the api-seo package for
a specific project.
</p>
</div>
<div className="p-6 pt-0">
<form className="space-y-4" onSubmit={handleStrategySubmit}>
<div className="space-y-2">
<Label htmlFor="strategyOrganizationSlug">
Organization Slug
</Label>
<Input
disabled={isPendingStrategy}
id="strategyOrganizationSlug"
onChange={(e) => setOrganizationSlug(e.target.value)}
placeholder="e.g. acme"
value={organizationSlug}
/>
</div>
<div className="space-y-2">
<Label htmlFor="strategyProjectSlug">Project Slug</Label>
<Input
disabled={isPendingStrategy}
id="strategyProjectSlug"
onChange={(e) => setProjectSlug(e.target.value)}
placeholder="e.g. acme-corp"
value={projectSlug}
/>
</div>
<div className="space-y-2">
<Label htmlFor="instructions">Instructions</Label>
<Textarea
disabled={isPendingStrategy}
id="instructions"
onChange={(e) => setInstructions(e.target.value)}
placeholder="Enter instructions for strategy generation..."
value={instructions}
/>
</div>
<Button
disabled={
!organizationSlug.trim() ||
!projectSlug.trim() ||
isPendingStrategy
}
isLoading={isPendingStrategy}
type="submit"
>
Trigger Strategy Suggestions
</Button>
</form>
</div>
</div>

<div className="w-full max-w-md rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold leading-none tracking-tight">
Trigger Strategy Phase Generation
</h3>
<p className="text-muted-foreground text-sm">
Trigger the strategy phase generation workflow in the api-seo
package for a specific strategy.
</p>
</div>
<div className="p-6 pt-0">
<form className="space-y-4" onSubmit={handleStrategyPhaseSubmit}>
<div className="space-y-2">
<Label htmlFor="phaseOrganizationSlug">Organization Slug</Label>
<Input
disabled={isPendingStrategyPhase}
id="phaseOrganizationSlug"
onChange={(e) => setOrganizationSlug(e.target.value)}
placeholder="e.g. rectangular-labs"
value={organizationSlug}
/>
</div>
<div className="space-y-2">
<Label htmlFor="phaseProjectSlug">Project Slug</Label>
<Input
disabled={isPendingStrategyPhase}
id="phaseProjectSlug"
onChange={(e) => setProjectSlug(e.target.value)}
placeholder="e.g. acme-corp"
value={projectSlug}
/>
</div>
<div className="space-y-2">
<Label htmlFor="phaseStrategyName">Strategy Name</Label>
<Input
disabled={isPendingStrategyPhase}
id="phaseStrategyName"
onChange={(e) => setPhaseStrategyName(e.target.value)}
placeholder="e.g. Topic Cluster For CRM ROI"
value={phaseStrategyName}
/>
</div>
<Button
disabled={
!organizationSlug.trim() ||
!projectSlug.trim() ||
!phaseStrategyName.trim() ||
isPendingStrategyPhase
}
isLoading={isPendingStrategyPhase}
type="submit"
>
Trigger Strategy Phase Generation
</Button>
</form>
</div>
</div>
</main>
);
}
Loading
Loading