1
0
Fork 0
polaris-task-force/src/components/frontend/intelligence/CreateCampaignDialog.tsx
Z8MB1E 7180b0aa4c feat(missions): make code name optional, add UGC cover images
- Missions.codeName is now optional (still unique when set); migration included
- Campaign and side-op dialogs accept an optional cover image (uploadUgcImage)
- Reminder toasts, evaluation aggregates, calendar, cards and mission headers
  handle a null code name
- Mission read filter no longer hides drafts by status; visibility follows
  ownershipAndStatus.visibility only
2026-09-10 20:02:47 -04:00

210 lines
7.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { createCampaign, uploadUgcImage } from "./create";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Shield, Loader2 } from "lucide-react";
export function CreateCampaignDialog() {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [coverImage, setCoverImage] = useState<{ id: number; url: string } | null>(null);
const [formData, setFormData] = useState({
name: "",
summary: "An Arma 3 campaign.",
status: "concept" as "concept" | "inProgress" | "completed",
description: "",
startDate: "",
endDate: "",
});
const handleImageUpload = async (file: File) => {
setIsUploading(true);
try {
const formData = new FormData();
formData.append("file", file);
const result = await uploadUgcImage(formData);
if (!result.success || !result.data) {
alert("Error uploading image: " + (result.error || "Unknown error"));
return;
}
setCoverImage({ id: result.data.id, url: result.data.url });
} finally {
setIsUploading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
const result = await createCampaign({ ...formData, coverImageId: coverImage?.id ?? null });
setIsSubmitting(false);
if (result.success) {
setOpen(false);
setCoverImage(null);
setFormData({
name: "",
summary: "An Arma 3 campaign.",
status: "concept",
description: "",
startDate: "",
endDate: "",
});
window.location.href = "/intelligence/campaigns";
} else {
alert("Error creating campaign: " + (result.error || "Unknown error"));
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Shield className="size-4" />
<span>Create campaign</span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Campaign</DialogTitle>
<DialogDescription>
Create a new Arma 3 campaign to manage your operations.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium">
Campaign Name <span className="text-red-500">*</span>
</label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter campaign name"
required
/>
</div>
<div className="space-y-2">
<label htmlFor="summary" className="text-sm font-medium">
Summary <span className="text-red-500">*</span>
</label>
<Textarea
id="summary"
value={formData.summary}
onChange={(e) => setFormData({ ...formData, summary: e.target.value })}
placeholder="Brief description of the campaign"
required
/>
</div>
<div className="space-y-2">
<label htmlFor="status" className="text-sm font-medium">
Status <span className="text-red-500">*</span>
</label>
<select
id="status"
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
required
>
<option value="concept">Concept</option>
<option value="inProgress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
<div className="space-y-2">
<label htmlFor="description" className="text-sm font-medium">
Description
</label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Optional: Detailed description of the campaign"
/>
</div>
<div className="space-y-2">
<label htmlFor="startDate" className="text-sm font-medium">
Start Date
</label>
<Input
id="startDate"
type="date"
value={formData.startDate}
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
max={formData.endDate}
/>
</div>
<div className="space-y-2">
<label htmlFor="endDate" className="text-sm font-medium">
End Date
</label>
<Input
id="endDate"
type="date"
value={formData.endDate}
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
min={formData.startDate}
/>
</div>
<div className="space-y-2">
<label htmlFor="coverImage" className="text-sm font-medium">
Cover Image <span className="text-xs text-muted-foreground">(optional)</span>
</label>
<Input
id="coverImage"
type="file"
accept="image/*"
disabled={isUploading}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) void handleImageUpload(file);
e.target.value = "";
}}
/>
{coverImage && (
<img
src={coverImage.url}
alt="Campaign cover preview"
className="h-28 w-full rounded-md object-cover"
/>
)}
<p className="text-xs text-muted-foreground">
Optional background image for the campaign card. 16:9 or square works best, 8 MB max.
</p>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Campaign"
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}