From d3fd1992db740cd21c9cce04abaa62e5951078e8 Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Wed, 28 May 2025 14:44:59 -0700 Subject: [PATCH] Create campaigns --- src/components/CreateCampaignButton.tsx | 80 +++++++++++++++++++ src/lib/types.ts | 11 ++- src/routes/_authenticated/campaigns.index.tsx | 10 +++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/components/CreateCampaignButton.tsx diff --git a/src/components/CreateCampaignButton.tsx b/src/components/CreateCampaignButton.tsx new file mode 100644 index 0000000..a8a6052 --- /dev/null +++ b/src/components/CreateCampaignButton.tsx @@ -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(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 ( + + ); + } + + return ( +
+ setName(e.target.value)} + disabled={loading} + autoFocus + /> + + + {error && {error}} +
+ ); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index b48d68a..c4009ce 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,10 +1,17 @@ +export type Id = string & { __type: T }; + +export type UserId = Id<"User">; +export type CampaignId = Id<"Campaign">; +export type DocumentId = Id<"Document">; + export type Campaign = { - id: string; + id: CampaignId; name: string; + owner: UserId; }; export type Document = { - id: string; + id: DocumentId; campaign: Campaign; data: {}; }; diff --git a/src/routes/_authenticated/campaigns.index.tsx b/src/routes/_authenticated/campaigns.index.tsx index 713d848..20cbe8b 100644 --- a/src/routes/_authenticated/campaigns.index.tsx +++ b/src/routes/_authenticated/campaigns.index.tsx @@ -3,6 +3,8 @@ import { pb } from "@/lib/pocketbase"; import type { Campaign } from "@/lib/types"; import { Link } from "@tanstack/react-router"; import { Loader } from "@/components/Loader"; +import { CreateCampaignButton } from "@/components/CreateCampaignButton"; +import { useRouter } from "@tanstack/react-router"; export const Route = createFileRoute("/_authenticated/campaigns/")({ loader: async () => { @@ -15,6 +17,11 @@ export const Route = createFileRoute("/_authenticated/campaigns/")({ function RouteComponent() { const { campaigns } = Route.useLoaderData(); + const router = useRouter(); + + const handleCreated = async () => { + await router.invalidate(); + }; if (!campaigns || campaigns.length === 0) return
No campaigns found.
; @@ -34,6 +41,9 @@ function RouteComponent() { ))} +
+ +
); }