- 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
210 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|