Create campaigns

This commit is contained in:
2025-05-28 14:44:59 -07:00
parent 5604b6ffdc
commit d3fd1992db
3 changed files with 99 additions and 2 deletions

View File

@@ -0,0 +1,80 @@
import { useState } from "react";
import { pb } from "@/lib/pocketbase";
import { useAuth } from "@/context/auth/AuthContext";
import type { Campaign } from "@/lib/types";
/**
* Button and form for creating a new campaign. Handles UI state and creation logic.
*/
export function CreateCampaignButton({ onCreated }: { onCreated?: (campaign: Campaign) => void }) {
const [creating, setCreating] = useState(false);
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { user } = useAuth();
if (!user) return null;
const handleCreate = async () => {
if (!name.trim()) {
setError("Campaign name is required.");
return;
}
setLoading(true);
setError(null);
try {
const record = await pb.collection("campaigns").create({
name,
owner: user.id,
});
setName("");
setCreating(false);
if (onCreated) onCreated({ id: record.id, name: record.name });
} catch (e: any) {
setError(e?.message || "Failed to create campaign.");
} finally {
setLoading(false);
}
};
if (!creating) {
return (
<button
className="px-4 py-2 rounded bg-violet-600 hover:bg-violet-700 text-white font-semibold transition-colors"
onClick={() => setCreating(true)}
>
Create Campaign
</button>
);
}
return (
<div className="flex items-center gap-2">
<input
type="text"
className="px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500"
placeholder="Campaign name"
value={name}
onChange={e => setName(e.target.value)}
disabled={loading}
autoFocus
/>
<button
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
onClick={handleCreate}
disabled={loading}
>
{loading ? "Creating…" : "Create"}
</button>
<button
className="px-2 py-2 rounded text-slate-400 hover:text-red-400"
onClick={() => { setCreating(false); setName(""); setError(null); }}
disabled={loading}
aria-label="Cancel"
>
</button>
{error && <span className="ml-2 text-red-400 text-sm">{error}</span>}
</div>
);
}

View File

@@ -1,10 +1,17 @@
export type Id<T extends string> = string & { __type: T };
export type UserId = Id<"User">;
export type CampaignId = Id<"Campaign">;
export type DocumentId = Id<"Document">;
export type Campaign = { export type Campaign = {
id: string; id: CampaignId;
name: string; name: string;
owner: UserId;
}; };
export type Document = { export type Document = {
id: string; id: DocumentId;
campaign: Campaign; campaign: Campaign;
data: {}; data: {};
}; };

View File

@@ -3,6 +3,8 @@ import { pb } from "@/lib/pocketbase";
import type { Campaign } from "@/lib/types"; import type { Campaign } from "@/lib/types";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { CreateCampaignButton } from "@/components/CreateCampaignButton";
import { useRouter } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated/campaigns/")({ export const Route = createFileRoute("/_authenticated/campaigns/")({
loader: async () => { loader: async () => {
@@ -15,6 +17,11 @@ export const Route = createFileRoute("/_authenticated/campaigns/")({
function RouteComponent() { function RouteComponent() {
const { campaigns } = Route.useLoaderData(); const { campaigns } = Route.useLoaderData();
const router = useRouter();
const handleCreated = async () => {
await router.invalidate();
};
if (!campaigns || campaigns.length === 0) return <div>No campaigns found.</div>; if (!campaigns || campaigns.length === 0) return <div>No campaigns found.</div>;
@@ -34,6 +41,9 @@ function RouteComponent() {
</li> </li>
))} ))}
</ul> </ul>
<div className="mt-8">
<CreateCampaignButton onCreated={handleCreated} />
</div>
</div> </div>
); );
} }