● Zion Boggan
repos/Drophawk/frontend/components/watchlist/WatchlistForm.tsx
zionboggan.com ↗
109 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useForm } from "react-hook-form";
4
import { zodResolver } from "@hookform/resolvers/zod";
5
import { z } from "zod";
6
import { Plus } from "lucide-react";
7
import { Button } from "@/components/ui/button";
8
import { Input, Label } from "@/components/ui/input";
9
import { Select } from "@/components/ui/select";
10
import { Card } from "@/components/ui/card";
11
import { useCreateWatch } from "@/hooks/useWatchlist";
12
import type { WatchlistKind, Priority } from "@/lib/types";
13
 
14
const KINDS: { value: WatchlistKind; label: string }[] = [
15
  { value: "artist", label: "Artist" },
16
  { value: "team", label: "Team" },
17
  { value: "venue", label: "Venue" },
18
  { value: "sport", label: "Sport" },
19
  { value: "category", label: "Category" },
20
  { value: "keyword", label: "Keyword" },
21
];
22
 
23
const PRIORITIES: Priority[] = ["low", "normal", "high", "critical"];
24
 
25
const schema = z.object({
26
  kind: z.enum(["artist", "team", "venue", "sport", "category", "keyword"]),
27
  label: z.string().trim().min(2, "Add at least 2 characters").max(120),
28
  priority: z.enum(["low", "normal", "high", "critical"]),
29
});
30
type FormValues = z.infer<typeof schema>;
31
 
32
function labelToPattern(label: string): string {
33
  const escaped = label.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
34
  return `(?i)\\b${escaped}\\b`;
35
}
36
 
37
export function WatchlistForm() {
38
  const create = useCreateWatch();
39
  const {
40
    register,
41
    handleSubmit,
42
    reset,
43
    formState: { errors, isSubmitting },
44
  } = useForm<FormValues>({
45
    resolver: zodResolver(schema),
46
    defaultValues: { kind: "artist", priority: "normal", label: "" },
47
  });
48
 
49
  const onSubmit = async (values: FormValues) => {
50
    await create.mutateAsync({
51
      kind: values.kind,
52
      label: values.label.trim(),
53
      pattern: labelToPattern(values.label),
54
      priority: values.priority,
55
    });
56
    reset({ kind: values.kind, priority: values.priority, label: "" });
57
  };
58
 
59
  return (
60
    <Card className="p-4">
61
      <form
62
        onSubmit={handleSubmit(onSubmit)}
63
        className="grid grid-cols-1 gap-3 sm:grid-cols-[140px_1fr_140px_auto] sm:items-end"
64
      >
65
        <div className="space-y-1.5">
66
          <Label htmlFor="wl-kind">Type</Label>
67
          <Select id="wl-kind" {...register("kind")}>
68
            {KINDS.map((k) => (
69
              <option key={k.value} value={k.value}>
70
                {k.label}
71
              </option>
72
            ))}
73
          </Select>
74
        </div>
75
 
76
        <div className="space-y-1.5">
77
          <Label htmlFor="wl-label">Who or what to track</Label>
78
          <Input
79
            id="wl-label"
80
            placeholder="e.g. Taylor Swift, Lakers, Coachella…"
81
            aria-invalid={!!errors.label}
82
            {...register("label")}
83
          />
84
          {errors.label ? (
85
            <p role="alert" className="text-xs text-[var(--color-accent-red)]">
86
              {errors.label.message}
87
            </p>
88
          ) : null}
89
        </div>
90
 
91
        <div className="space-y-1.5">
92
          <Label htmlFor="wl-priority">Priority</Label>
93
          <Select id="wl-priority" {...register("priority")}>
94
            {PRIORITIES.map((p) => (
95
              <option key={p} value={p}>
96
                {p[0]!.toUpperCase() + p.slice(1)}
97
              </option>
98
            ))}
99
          </Select>
100
        </div>
101
 
102
        <Button type="submit" loading={isSubmitting} className="sm:w-auto">
103
          <Plus className="h-4 w-4" aria-hidden />
104
          Track
105
        </Button>
106
      </form>
107
    </Card>
108
  );
109
}