@flaredev/cli 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flare.js +9 -1
- package/dist/index.js +985 -337
- package/package.json +3 -2
- package/templates/app/app/admin/page.tsx +9 -61
- package/templates/app/app/api/openapi.json/route.ts +23 -0
- package/templates/app/app/api/reference/route.ts +36 -0
- package/templates/app/app/dashboard/account/page.tsx +40 -0
- package/templates/app/app/{admin → dashboard}/actions.ts +109 -11
- package/templates/app/app/dashboard/import-actions.ts +91 -0
- package/templates/app/app/dashboard/layout.tsx +42 -0
- package/templates/app/app/dashboard/observability/page.tsx +174 -0
- package/templates/app/app/dashboard/page.tsx +123 -9
- package/templates/app/app/forgot-password/page.tsx +22 -0
- package/templates/app/app/globals.css +109 -4
- package/templates/app/app/layout.tsx +28 -26
- package/templates/app/app/page.tsx +126 -13
- package/templates/app/app/reset-password/page.tsx +31 -0
- package/templates/app/app/sign-in/page.tsx +49 -33
- package/templates/app/app/sign-up/page.tsx +42 -23
- package/templates/app/app/two-factor/page.tsx +25 -0
- package/templates/app/app/verify-email/page.tsx +33 -0
- package/templates/app/components/account/security-settings.tsx +522 -0
- package/templates/app/components/auth/auth-shell.tsx +284 -0
- package/templates/app/components/auth/otp-input.tsx +95 -0
- package/templates/app/components/auth/password-field.tsx +116 -0
- package/templates/app/components/auth/password-forms.tsx +76 -0
- package/templates/app/components/auth/provider-icons.tsx +39 -0
- package/templates/app/components/auth/sign-in-flow.tsx +306 -0
- package/templates/app/components/auth/sign-up-form.tsx +93 -0
- package/templates/app/components/auth/social-buttons.tsx +56 -0
- package/templates/app/components/auth/two-factor-form.tsx +139 -0
- package/templates/app/components/auth/ui.tsx +96 -0
- package/templates/app/components/auth/use-resend.ts +44 -0
- package/templates/app/components/auth/verify-email-form.tsx +135 -0
- package/templates/app/components/{admin/admin-header.tsx → dashboard/dashboard-header.tsx} +78 -78
- package/templates/app/components/{admin/resource-nav.tsx → dashboard/dashboard-sidebar.tsx} +66 -9
- package/templates/app/components/dashboard/export-button.tsx +41 -0
- package/templates/app/components/dashboard/fields/country-field.tsx +152 -0
- package/templates/app/components/{admin → dashboard}/fields/field-widget.tsx +186 -128
- package/templates/app/components/{admin → dashboard}/fields/file-field.tsx +302 -302
- package/templates/app/components/dashboard/fields/multi-select-field.tsx +46 -0
- package/templates/app/components/dashboard/fields/phone-field.tsx +86 -0
- package/templates/app/components/dashboard/import-dialog.tsx +487 -0
- package/templates/app/components/dashboard/page-header.tsx +57 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +80 -0
- package/templates/app/components/{admin → dashboard}/resource-form-page.tsx +61 -55
- package/templates/app/components/{admin → dashboard}/resource-form.tsx +165 -139
- package/templates/app/components/dashboard/resource-stats.tsx +53 -0
- package/templates/app/components/{admin → dashboard}/resource-table.tsx +331 -251
- package/templates/app/components/{admin → dashboard}/row-actions.tsx +144 -111
- package/templates/app/components/dashboard/stat-card.tsx +69 -0
- package/templates/app/components/dashboard/table-selection.tsx +185 -0
- package/templates/app/components/dashboard/traffic-chart.tsx +32 -0
- package/templates/app/components/marketing/product-preview.tsx +70 -0
- package/templates/app/components/marketing/site-header.tsx +39 -0
- package/templates/app/components/sign-out-button.tsx +1 -1
- package/templates/app/components/ui/checkbox.tsx +25 -0
- package/templates/app/components/ui/radio-group.tsx +29 -0
- package/templates/app/db/auth-schema.ts +167 -118
- package/templates/app/db/flare-schema.ts +33 -1
- package/templates/app/lib/analytics.ts +121 -0
- package/templates/app/lib/api-docs.ts +16 -0
- package/templates/app/lib/audit.ts +80 -0
- package/templates/app/lib/auth-client.ts +16 -1
- package/templates/app/lib/auth-config.ts +38 -0
- package/templates/app/lib/auth-emails.ts +94 -0
- package/templates/app/lib/auth.ts +115 -6
- package/templates/app/lib/cache.ts +83 -83
- package/templates/app/lib/csv.ts +24 -0
- package/templates/app/lib/dashboard-nav.ts +15 -0
- package/templates/app/lib/{admin.ts → dashboard.ts} +138 -103
- package/templates/app/lib/password-rules.ts +159 -0
- package/templates/app/lib/site.ts +56 -0
- package/templates/app/lib/theme.ts +79 -0
- package/templates/app/migrations/0000_init.sql +47 -4
- package/templates/app/migrations/meta/0000_snapshot.json +311 -1
- package/templates/app/migrations/meta/_journal.json +1 -1
- package/templates/app/vite.config.ts +5 -3
- package/templates/app/worker-configuration.d.ts +15764 -0
- package/templates/locks/package-lock.json +9189 -0
- package/templates/locks/pnpm-lock.yaml +6347 -0
- package/templates/app/app/admin/layout.tsx +0 -32
- package/templates/app/components/auth-form.tsx +0 -120
- package/templates/app/lib/admin-nav.ts +0 -15
- /package/templates/app/components/{admin → dashboard}/fields/date-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/fields/relation-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/query.ts +0 -0
- /package/templates/app/components/{admin → dashboard}/resource-icon.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/resource-table-toolbar.tsx +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
|
|
5
|
+
import { countries, type Country } from "@flaredev/core";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
|
|
11
|
+
/** Countries named in the viewer's language (falls back to English). */
|
|
12
|
+
function useCountries(): Country[] {
|
|
13
|
+
return useMemo(() => countries(typeof navigator === "undefined" ? "en" : navigator.language.split("-")[0]), []);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ListProps {
|
|
17
|
+
value: string;
|
|
18
|
+
onSelect: (country: Country) => void;
|
|
19
|
+
/** Show dialling codes next to names (for the phone input). */
|
|
20
|
+
withDialCode?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The searchable list itself: type a name, an ISO code or a dialling code. */
|
|
24
|
+
function CountryList({ value, onSelect, withDialCode }: ListProps) {
|
|
25
|
+
const list = useCountries();
|
|
26
|
+
return (
|
|
27
|
+
<Command>
|
|
28
|
+
<CommandInput placeholder={withDialCode ? "Search country or code…" : "Search countries…"} />
|
|
29
|
+
<CommandList>
|
|
30
|
+
<CommandEmpty>No country found.</CommandEmpty>
|
|
31
|
+
<CommandGroup>
|
|
32
|
+
{list.map((country) => (
|
|
33
|
+
<CommandItem
|
|
34
|
+
key={country.code}
|
|
35
|
+
// cmdk filters on this: name, ISO code and dialling code all match.
|
|
36
|
+
value={`${country.name} ${country.code} +${country.dialCode}`}
|
|
37
|
+
onSelect={() => onSelect(country)}
|
|
38
|
+
>
|
|
39
|
+
<span aria-hidden="true" className="text-base leading-none">{country.flag}</span>
|
|
40
|
+
<span className="truncate">{country.name}</span>
|
|
41
|
+
{withDialCode && <span className="ml-auto text-xs text-muted-foreground tabular-nums">+{country.dialCode}</span>}
|
|
42
|
+
<CheckIcon className={cn(withDialCode ? "" : "ml-auto", country.code === value ? "opacity-100" : "opacity-0")} />
|
|
43
|
+
</CommandItem>
|
|
44
|
+
))}
|
|
45
|
+
</CommandGroup>
|
|
46
|
+
</CommandList>
|
|
47
|
+
</Command>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface FieldProps {
|
|
52
|
+
id: string;
|
|
53
|
+
value: string;
|
|
54
|
+
onChange: (value: string) => void;
|
|
55
|
+
invalid: boolean;
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
required: boolean;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A `country` field: one ISO 3166-1 alpha-2 code, chosen from a searchable list. */
|
|
62
|
+
export function CountryField({ id, value, onChange, invalid, disabled, required, placeholder }: FieldProps) {
|
|
63
|
+
const [open, setOpen] = useState(false);
|
|
64
|
+
const list = useCountries();
|
|
65
|
+
const selected = list.find((country) => country.code === value.toUpperCase());
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
69
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
70
|
+
<PopoverTrigger asChild>
|
|
71
|
+
<Button
|
|
72
|
+
id={id}
|
|
73
|
+
type="button"
|
|
74
|
+
variant="outline"
|
|
75
|
+
role="combobox"
|
|
76
|
+
aria-expanded={open}
|
|
77
|
+
aria-invalid={invalid || undefined}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
className={cn("min-w-0 flex-1 justify-between font-normal", !selected && "text-muted-foreground")}
|
|
80
|
+
>
|
|
81
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
82
|
+
{selected && <span aria-hidden="true" className="text-base leading-none">{selected.flag}</span>}
|
|
83
|
+
<span className="truncate">{selected ? selected.name : (placeholder ?? "Choose a country")}</span>
|
|
84
|
+
</span>
|
|
85
|
+
<ChevronsUpDownIcon className="opacity-50" />
|
|
86
|
+
</Button>
|
|
87
|
+
</PopoverTrigger>
|
|
88
|
+
<PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0" align="start">
|
|
89
|
+
<CountryList
|
|
90
|
+
value={selected?.code ?? ""}
|
|
91
|
+
onSelect={(country) => {
|
|
92
|
+
onChange(country.code);
|
|
93
|
+
setOpen(false);
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
</PopoverContent>
|
|
97
|
+
</Popover>
|
|
98
|
+
{selected && !required && (
|
|
99
|
+
<Button type="button" variant="ghost" size="icon" aria-label="Clear country" onClick={() => onChange("")} disabled={disabled}>
|
|
100
|
+
<XIcon />
|
|
101
|
+
</Button>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface CodeProps {
|
|
108
|
+
value: string;
|
|
109
|
+
onChange: (country: Country) => void;
|
|
110
|
+
disabled?: boolean;
|
|
111
|
+
invalid: boolean;
|
|
112
|
+
/** Accessible name, e.g. "Country code for Phone". */
|
|
113
|
+
label: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The compact country-code button at the start of a phone input. */
|
|
117
|
+
export function DialCodePicker({ value, onChange, disabled, invalid, label }: CodeProps) {
|
|
118
|
+
const [open, setOpen] = useState(false);
|
|
119
|
+
const list = useCountries();
|
|
120
|
+
const selected = list.find((country) => country.code === value);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
124
|
+
<PopoverTrigger asChild>
|
|
125
|
+
<Button
|
|
126
|
+
type="button"
|
|
127
|
+
variant="outline"
|
|
128
|
+
role="combobox"
|
|
129
|
+
aria-expanded={open}
|
|
130
|
+
aria-label={selected ? `${label}: ${selected.name} +${selected.dialCode}` : label}
|
|
131
|
+
aria-invalid={invalid || undefined}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
className="shrink-0 gap-1.5 rounded-r-none border-r-0 px-2.5 font-normal tabular-nums"
|
|
134
|
+
>
|
|
135
|
+
<span aria-hidden="true" className="text-base leading-none">{selected?.flag ?? "🌐"}</span>
|
|
136
|
+
<span>+{selected?.dialCode ?? ""}</span>
|
|
137
|
+
<ChevronsUpDownIcon className="opacity-50" />
|
|
138
|
+
</Button>
|
|
139
|
+
</PopoverTrigger>
|
|
140
|
+
<PopoverContent className="w-72 p-0" align="start">
|
|
141
|
+
<CountryList
|
|
142
|
+
value={value}
|
|
143
|
+
withDialCode
|
|
144
|
+
onSelect={(country) => {
|
|
145
|
+
onChange(country);
|
|
146
|
+
setOpen(false);
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
</PopoverContent>
|
|
150
|
+
</Popover>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -1,128 +1,186 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { optionLabel, type FileField as FileFieldDef, type StoredField } from "@flaredev/core";
|
|
4
|
-
import { Input } from "@/components/ui/input";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { normalizeDomain, optionLabel, slugify, type FileField as FileFieldDef, type MultiSelectField as MultiSelectDef, type StoredField } from "@flaredev/core";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
7
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
8
|
+
import { Switch } from "@/components/ui/switch";
|
|
9
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
10
|
+
import { DateField, DateTimeField } from "./date-field";
|
|
11
|
+
import { CountryField } from "./country-field";
|
|
12
|
+
import { FileField } from "./file-field";
|
|
13
|
+
import { MultiSelectField } from "./multi-select-field";
|
|
14
|
+
import { PhoneField } from "./phone-field";
|
|
15
|
+
import { RelationField } from "./relation-field";
|
|
16
|
+
|
|
17
|
+
export interface RelationMeta {
|
|
18
|
+
name: string;
|
|
19
|
+
label: string;
|
|
20
|
+
pluralLabel: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
titleField: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WidgetProps {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
/** Resource name and field key, for upload actions. */
|
|
29
|
+
resourceName: string;
|
|
30
|
+
fieldKey: string;
|
|
31
|
+
field: StoredField & { label: string };
|
|
32
|
+
value: string | boolean;
|
|
33
|
+
onChange: (value: string | boolean) => void;
|
|
34
|
+
invalid: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** belongsTo: the related resource and the current value's title. */
|
|
37
|
+
relation?: RelationMeta & { initialTitle?: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const NONE = "__none__";
|
|
41
|
+
|
|
42
|
+
/** The input for a field, chosen from its kind in the descriptor. */
|
|
43
|
+
export function FieldWidget(props: WidgetProps) {
|
|
44
|
+
const { id, name, field, value, onChange, invalid, disabled, relation, resourceName, fieldKey } = props;
|
|
45
|
+
const text = typeof value === "string" ? value : "";
|
|
46
|
+
const common = { id, name, disabled, "aria-invalid": invalid || undefined, placeholder: field.placeholder };
|
|
47
|
+
|
|
48
|
+
switch (field.kind) {
|
|
49
|
+
case "text":
|
|
50
|
+
return <Textarea {...common} value={text} rows={4} onChange={(event) => onChange(event.target.value)} />;
|
|
51
|
+
|
|
52
|
+
case "int":
|
|
53
|
+
case "float":
|
|
54
|
+
return (
|
|
55
|
+
<Input
|
|
56
|
+
{...common}
|
|
57
|
+
type="text"
|
|
58
|
+
inputMode={field.kind === "int" ? "numeric" : "decimal"}
|
|
59
|
+
className="tabular-nums"
|
|
60
|
+
value={text}
|
|
61
|
+
onChange={(event) => onChange(event.target.value)}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
case "boolean":
|
|
66
|
+
return (
|
|
67
|
+
<Switch
|
|
68
|
+
id={id}
|
|
69
|
+
name={name}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
aria-invalid={invalid || undefined}
|
|
72
|
+
checked={value === true}
|
|
73
|
+
onCheckedChange={(checked) => onChange(checked)}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
case "enum":
|
|
78
|
+
if (field.widget === "radio") {
|
|
79
|
+
return (
|
|
80
|
+
<RadioGroup id={id} name={name} value={text} onValueChange={onChange} disabled={disabled} aria-invalid={invalid || undefined} className="flex flex-wrap gap-x-5 gap-y-2.5">
|
|
81
|
+
{field.options.map((option) => (
|
|
82
|
+
<div key={option} className="flex items-center gap-2">
|
|
83
|
+
<RadioGroupItem id={`${id}-${option}`} value={option} aria-invalid={invalid || undefined} />
|
|
84
|
+
<Label htmlFor={`${id}-${option}`} className="font-normal">
|
|
85
|
+
{optionLabel(field, option)}
|
|
86
|
+
</Label>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</RadioGroup>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return (
|
|
93
|
+
// No selection is `undefined`, so the placeholder shows; "None" clears an optional value.
|
|
94
|
+
<Select value={text || undefined} onValueChange={(next) => onChange(next === NONE ? "" : next)} disabled={disabled} name={name}>
|
|
95
|
+
<SelectTrigger id={id} aria-invalid={invalid || undefined} className="w-full">
|
|
96
|
+
<SelectValue placeholder={field.placeholder ?? `Choose ${field.label.toLowerCase()}`} />
|
|
97
|
+
</SelectTrigger>
|
|
98
|
+
<SelectContent>
|
|
99
|
+
<SelectGroup>
|
|
100
|
+
{!field.required && <SelectItem value={NONE}>None</SelectItem>}
|
|
101
|
+
{field.options.map((option) => (
|
|
102
|
+
<SelectItem key={option} value={option}>
|
|
103
|
+
{optionLabel(field, option)}
|
|
104
|
+
</SelectItem>
|
|
105
|
+
))}
|
|
106
|
+
</SelectGroup>
|
|
107
|
+
</SelectContent>
|
|
108
|
+
</Select>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
case "multiselect":
|
|
112
|
+
return (
|
|
113
|
+
<MultiSelectField id={id} field={field as MultiSelectDef & { label: string }} value={text} onChange={onChange} invalid={invalid} disabled={disabled} />
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
case "date":
|
|
117
|
+
return (
|
|
118
|
+
<DateField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
case "datetime":
|
|
122
|
+
return (
|
|
123
|
+
<DateTimeField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
case "belongsTo":
|
|
127
|
+
return relation ? (
|
|
128
|
+
<RelationField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} relation={relation} />
|
|
129
|
+
) : (
|
|
130
|
+
<Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
case "file":
|
|
134
|
+
return (
|
|
135
|
+
<FileField
|
|
136
|
+
id={id}
|
|
137
|
+
value={text}
|
|
138
|
+
onChange={onChange}
|
|
139
|
+
invalid={invalid}
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
field={field as FileFieldDef & { label: string }}
|
|
142
|
+
resourceName={resourceName}
|
|
143
|
+
fieldKey={fieldKey}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
case "string":
|
|
148
|
+
switch (field.format) {
|
|
149
|
+
case "tel":
|
|
150
|
+
return <PhoneField id={id} name={name} label={field.label} value={text} onChange={onChange} invalid={invalid} disabled={disabled} placeholder={field.placeholder} />;
|
|
151
|
+
case "country":
|
|
152
|
+
return <CountryField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />;
|
|
153
|
+
case "color":
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex items-center gap-2">
|
|
156
|
+
<input
|
|
157
|
+
type="color"
|
|
158
|
+
aria-label={`${field.label} picker`}
|
|
159
|
+
value={/^#[0-9a-f]{6}$/i.test(text) ? text : "#000000"}
|
|
160
|
+
onChange={(event) => onChange(event.target.value)}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
className="h-9 w-11 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1"
|
|
163
|
+
/>
|
|
164
|
+
<Input {...common} className="font-mono" placeholder={field.placeholder ?? "#f2541d"} value={text} onChange={(event) => onChange(event.target.value)} />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
case "slug":
|
|
168
|
+
return (
|
|
169
|
+
<Input {...common} className="font-mono" placeholder={field.placeholder ?? "my-first-post"} value={text} onChange={(event) => onChange(event.target.value)} onBlur={() => text && onChange(slugify(text))} />
|
|
170
|
+
);
|
|
171
|
+
case "domain":
|
|
172
|
+
return (
|
|
173
|
+
<Input {...common} inputMode="url" autoCapitalize="none" spellCheck={false} placeholder={field.placeholder ?? "example.com"} value={text} onChange={(event) => onChange(event.target.value)} onBlur={() => text && onChange(normalizeDomain(text))} />
|
|
174
|
+
);
|
|
175
|
+
case "email":
|
|
176
|
+
return <Input {...common} type="email" autoComplete="email" value={text} onChange={(event) => onChange(event.target.value)} />;
|
|
177
|
+
case "url":
|
|
178
|
+
return <Input {...common} type="url" placeholder={field.placeholder ?? "https://"} value={text} onChange={(event) => onChange(event.target.value)} />;
|
|
179
|
+
default:
|
|
180
|
+
return <Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
default:
|
|
184
|
+
return <Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />;
|
|
185
|
+
}
|
|
186
|
+
}
|