@shipsite.dev/components 0.2.55 → 0.2.64
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/components.json +102 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/use-form-submit.d.ts +7 -0
- package/dist/lib/use-form-submit.d.ts.map +1 -0
- package/dist/lib/use-form-submit.js +47 -0
- package/dist/lib/use-form-submit.js.map +1 -0
- package/dist/marketing/ContactForm.d.ts +16 -0
- package/dist/marketing/ContactForm.d.ts.map +1 -0
- package/dist/marketing/ContactForm.js +29 -0
- package/dist/marketing/ContactForm.js.map +1 -0
- package/dist/marketing/Form.d.ts +25 -0
- package/dist/marketing/Form.d.ts.map +1 -0
- package/dist/marketing/Form.js +18 -0
- package/dist/marketing/Form.js.map +1 -0
- package/dist/marketing/FormClient.d.ts +23 -0
- package/dist/marketing/FormClient.d.ts.map +1 -0
- package/dist/marketing/FormClient.js +41 -0
- package/dist/marketing/FormClient.js.map +1 -0
- package/dist/marketing/FormEmbed.d.ts +13 -0
- package/dist/marketing/FormEmbed.d.ts.map +1 -0
- package/dist/marketing/FormEmbed.js +27 -0
- package/dist/marketing/FormEmbed.js.map +1 -0
- package/dist/marketing/NewsletterForm.d.ts +13 -0
- package/dist/marketing/NewsletterForm.d.ts.map +1 -0
- package/dist/marketing/NewsletterForm.js +23 -0
- package/dist/marketing/NewsletterForm.js.map +1 -0
- package/dist/marketing/WaitlistForm.d.ts +16 -0
- package/dist/marketing/WaitlistForm.d.ts.map +1 -0
- package/dist/marketing/WaitlistForm.js +27 -0
- package/dist/marketing/WaitlistForm.js.map +1 -0
- package/dist/ui/input.d.ts +5 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +7 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/label.d.ts +5 -0
- package/dist/ui/label.d.ts.map +1 -0
- package/dist/ui/label.js +9 -0
- package/dist/ui/label.js.map +1 -0
- package/dist/ui/select.d.ts +14 -0
- package/dist/ui/select.d.ts.map +1 -0
- package/dist/ui/select.js +28 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/textarea.d.ts +5 -0
- package/dist/ui/textarea.d.ts.map +1 -0
- package/dist/ui/textarea.js +7 -0
- package/dist/ui/textarea.js.map +1 -0
- package/package.json +3 -1
- package/src/index.ts +7 -0
- package/src/lib/use-form-submit.ts +53 -0
- package/src/marketing/ContactForm.tsx +157 -0
- package/src/marketing/Form.tsx +41 -0
- package/src/marketing/FormClient.tsx +194 -0
- package/src/marketing/FormEmbed.tsx +118 -0
- package/src/marketing/NewsletterForm.tsx +102 -0
- package/src/marketing/WaitlistForm.tsx +145 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/select.tsx +160 -0
- package/src/ui/textarea.tsx +20 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { CheckCircle, Loader2 } from "lucide-react";
|
|
5
|
+
import { Section } from "../ui/section";
|
|
6
|
+
import { Input } from "../ui/input";
|
|
7
|
+
import { Label } from "../ui/label";
|
|
8
|
+
import { Button } from "../ui/button";
|
|
9
|
+
import { Badge } from "../ui/badge";
|
|
10
|
+
import { useFormSubmit } from "../lib/use-form-submit";
|
|
11
|
+
export function WaitlistForm({ id, action, title = "Join the Waitlist", description, badge, showName = false, nameLabel = "Name", emailLabel = "Email", submitLabel = "Join Waitlist", successTitle = "You're on the list!", successMessage = "We'll notify you when it's your turn.", }) {
|
|
12
|
+
const uid = useId();
|
|
13
|
+
const { status, errorMsg, submit } = useFormSubmit(action);
|
|
14
|
+
const [fields, setFields] = useState({ name: "", email: "" });
|
|
15
|
+
function update(key, value) {
|
|
16
|
+
setFields((prev) => ({ ...prev, [key]: value }));
|
|
17
|
+
}
|
|
18
|
+
function handleSubmit(e) {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
const data = { email: fields.email };
|
|
21
|
+
if (showName)
|
|
22
|
+
data.name = fields.name;
|
|
23
|
+
submit(data);
|
|
24
|
+
}
|
|
25
|
+
return (_jsx(Section, { id: id, children: _jsxs("div", { className: "container-main max-w-2xl text-center", children: [badge && (_jsx("div", { className: "mb-4", children: _jsx(Badge, { variant: "outline", children: badge }) })), (title || description) && (_jsxs("div", { className: "mb-12", children: [title && (_jsx("h2", { className: "text-3xl md:text-4xl font-bold text-foreground mb-4", children: title })), description && (_jsx("p", { className: "text-lg text-muted-foreground max-w-2xl mx-auto", children: description }))] })), status === "success" ? (_jsxs("div", { className: "text-center py-12", children: [_jsx("div", { className: "w-16 h-16 rounded-full bg-primary/10 mx-auto mb-4 flex items-center justify-center", children: _jsx(CheckCircle, { className: "w-8 h-8 text-primary" }) }), _jsx("h3", { className: "text-xl font-semibold text-foreground mb-2", children: successTitle }), _jsx("p", { className: "text-muted-foreground", children: successMessage })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "glass-1 rounded-2xl p-8 max-w-md mx-auto space-y-6", children: [showName && (_jsxs("div", { className: "space-y-2 text-left", children: [_jsx(Label, { htmlFor: `${uid}-name`, children: nameLabel }), _jsx(Input, { id: `${uid}-name`, name: "name", placeholder: "Your name", value: fields.name, onChange: (e) => update("name", e.target.value), required: true, disabled: status === "loading" })] })), _jsxs("div", { className: "space-y-2 text-left", children: [_jsx(Label, { htmlFor: `${uid}-email`, children: emailLabel }), _jsx(Input, { id: `${uid}-email`, name: "email", type: "email", placeholder: "you@example.com", value: fields.email, onChange: (e) => update("email", e.target.value), required: true, disabled: status === "loading" })] }), _jsx(Button, { type: "submit", variant: "default", size: "lg", className: "w-full", disabled: status === "loading", children: status === "loading" ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "size-4 animate-spin mr-2" }), "Joining..."] })) : (submitLabel) }), status === "error" && (_jsx("p", { className: "text-sm text-destructive", "aria-live": "polite", children: errorMsg }))] }))] }) }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=WaitlistForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WaitlistForm.js","sourceRoot":"","sources":["../../src/marketing/WaitlistForm.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/ui/input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,MAAM,UAAU,GAAG,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;AAErE,iBAAS,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,2CAYvD;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/ui/input.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
function Input({ className, type, ...props }) {
|
|
4
|
+
return (_jsx("input", { type: type, "data-slot": "input", className: cn("border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className), ...props }));
|
|
5
|
+
}
|
|
6
|
+
export { Input };
|
|
7
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/ui/input.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAIlC,SAAS,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,KAAK,EAAc;IACtD,OAAO,CACL,gBACE,IAAI,EAAE,IAAI,eACA,OAAO,EACjB,SAAS,EAAE,EAAE,CACX,gWAAgW,EAChW,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
declare function Label({ className, ...props }: React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export { Label };
|
|
5
|
+
//# sourceMappingURL=label.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../src/ui/label.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,iBAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,wBAAwB,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC,2CAW5D;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/ui/label.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
function Label({ className, ...props }) {
|
|
6
|
+
return (_jsx(LabelPrimitive.Root, { "data-slot": "label", className: cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className), ...props }));
|
|
7
|
+
}
|
|
8
|
+
export { Label };
|
|
9
|
+
//# sourceMappingURL=label.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label.js","sourceRoot":"","sources":["../../src/ui/label.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAElC,SAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACmD;IAC3D,OAAO,CACL,KAAC,cAAc,CAAC,IAAI,iBACR,OAAO,EACjB,SAAS,EAAE,EAAE,CACX,4FAA4F,EAC5F,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
declare const Select: React.FC<SelectPrimitive.SelectProps>;
|
|
4
|
+
declare const SelectGroup: React.ForwardRefExoticComponent<SelectPrimitive.SelectGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
5
|
+
declare const SelectValue: React.ForwardRefExoticComponent<SelectPrimitive.SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
|
|
6
|
+
declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
7
|
+
declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
8
|
+
declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
9
|
+
declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
+
declare const SelectLabel: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
11
|
+
declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
12
|
+
declare const SelectSeparator: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
13
|
+
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
14
|
+
//# sourceMappingURL=select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/ui/select.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAC;AAE1D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,QAAA,MAAM,MAAM,uCAAuB,CAAC;AAEpC,QAAA,MAAM,WAAW,yGAAwB,CAAC;AAE1C,QAAA,MAAM,WAAW,0GAAwB,CAAC;AAE1C,QAAA,MAAM,aAAa,oKAiBjB,CAAC;AAGH,QAAA,MAAM,oBAAoB,qKAcxB,CAAC;AAGH,QAAA,MAAM,sBAAsB,uKAc1B,CAAC;AAIH,QAAA,MAAM,aAAa,8JA6BjB,CAAC;AAGH,QAAA,MAAM,WAAW,4JASf,CAAC;AAGH,QAAA,MAAM,UAAU,2JAoBd,CAAC;AAGH,QAAA,MAAM,eAAe,gKASnB,CAAC;AAGH,OAAO,EACL,MAAM,EACN,aAAa,EACb,WAAW,EACX,UAAU,EACV,WAAW,EACX,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,WAAW,GACZ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
4
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
const Select = SelectPrimitive.Root;
|
|
8
|
+
const SelectGroup = SelectPrimitive.Group;
|
|
9
|
+
const SelectValue = SelectPrimitive.Value;
|
|
10
|
+
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Trigger, { ref: ref, className: cn("border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDown, { className: "size-4 opacity-50" }) })] })));
|
|
11
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
12
|
+
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.ScrollUpButton, { ref: ref, className: cn("flex cursor-default items-center justify-center py-1", className), ...props, children: _jsx(ChevronUp, { className: "size-4" }) })));
|
|
13
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
14
|
+
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.ScrollDownButton, { ref: ref, className: cn("flex cursor-default items-center justify-center py-1", className), ...props, children: _jsx(ChevronDown, { className: "size-4" }) })));
|
|
15
|
+
SelectScrollDownButton.displayName =
|
|
16
|
+
SelectPrimitive.ScrollDownButton.displayName;
|
|
17
|
+
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { ref: ref, className: cn("bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border dark:border-border/15 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md", position === "popper" &&
|
|
18
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { className: cn("p-1", position === "popper" &&
|
|
19
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"), children: children }), _jsx(SelectScrollDownButton, {})] }) })));
|
|
20
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
21
|
+
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Label, { ref: ref, className: cn("py-1.5 pr-2 pl-8 text-sm font-semibold", className), ...props })));
|
|
22
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
23
|
+
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, className: cn("focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "size-4" }) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] })));
|
|
24
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
25
|
+
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Separator, { ref: ref, className: cn("bg-muted -mx-1 my-1 h-px", className), ...props })));
|
|
26
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
27
|
+
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
28
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.js","sourceRoot":"","sources":["../../src/ui/select.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
3
|
+
declare function Textarea({ className, ...props }: TextareaProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export { Textarea };
|
|
5
|
+
//# sourceMappingURL=textarea.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textarea.d.ts","sourceRoot":"","sources":["../../src/ui/textarea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC,sBAAsB,CAAC,mBAAmB,CAAC,CAAC;AAE9E,iBAAS,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,2CAWvD;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
function Textarea({ className, ...props }) {
|
|
4
|
+
return (_jsx("textarea", { "data-slot": "textarea", className: cn("border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[120px] w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className), ...props }));
|
|
5
|
+
}
|
|
6
|
+
export { Textarea };
|
|
7
|
+
//# sourceMappingURL=textarea.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textarea.js","sourceRoot":"","sources":["../../src/ui/textarea.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAIlC,SAAS,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAiB;IACtD,OAAO,CACL,gCACY,UAAU,EACpB,SAAS,EAAE,EAAE,CACX,ySAAyS,EACzS,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipsite.dev/components",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.64",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/shipsite/shipsite",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@radix-ui/react-accordion": "^1.2.3",
|
|
38
38
|
"@radix-ui/react-dialog": "^1.1.6",
|
|
39
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
39
40
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
41
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
40
42
|
"@radix-ui/react-slot": "^1.2.0",
|
|
41
43
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
42
44
|
"class-variance-authority": "^0.7.1",
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,13 @@ export { SocialProof } from './marketing/SocialProof';
|
|
|
49
49
|
export { Carousel, CarouselItem } from './marketing/Carousel';
|
|
50
50
|
export { TabsSection, TabItem } from './marketing/TabsSection';
|
|
51
51
|
|
|
52
|
+
// Forms
|
|
53
|
+
export { ContactForm } from './marketing/ContactForm';
|
|
54
|
+
export { NewsletterForm } from './marketing/NewsletterForm';
|
|
55
|
+
export { WaitlistForm } from './marketing/WaitlistForm';
|
|
56
|
+
export { Form, FormField } from './marketing/Form';
|
|
57
|
+
export { FormEmbed } from './marketing/FormEmbed';
|
|
58
|
+
|
|
52
59
|
// Blog
|
|
53
60
|
export { BlogArticle } from './blog/BlogArticle';
|
|
54
61
|
export { BlogIndex } from './blog/BlogIndex';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
export type FormStatus = "idle" | "loading" | "success" | "error";
|
|
6
|
+
|
|
7
|
+
export function useFormSubmit(action: string) {
|
|
8
|
+
const [status, setStatus] = useState<FormStatus>("idle");
|
|
9
|
+
const [errorMsg, setErrorMsg] = useState("");
|
|
10
|
+
|
|
11
|
+
const submit = useCallback(
|
|
12
|
+
async (data: Record<string, string>) => {
|
|
13
|
+
setStatus("loading");
|
|
14
|
+
setErrorMsg("");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(action, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(data),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
let msg = "Something went wrong. Please try again.";
|
|
28
|
+
try {
|
|
29
|
+
const body = await res.json();
|
|
30
|
+
// Formspree returns { errors: [{ message }] }
|
|
31
|
+
// Getform/Basin return { message }
|
|
32
|
+
if (body.errors?.[0]?.message) msg = body.errors[0].message;
|
|
33
|
+
else if (body.error) msg = body.error;
|
|
34
|
+
else if (body.message) msg = body.message;
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore JSON parse errors
|
|
37
|
+
}
|
|
38
|
+
setErrorMsg(msg);
|
|
39
|
+
setStatus("error");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setStatus("success");
|
|
44
|
+
} catch {
|
|
45
|
+
setErrorMsg("Network error. Please check your connection and try again.");
|
|
46
|
+
setStatus("error");
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[action],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return { status, errorMsg, submit };
|
|
53
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { FormEvent, useId, useState } from "react";
|
|
4
|
+
import { CheckCircle, Loader2 } from "lucide-react";
|
|
5
|
+
import { Section } from "../ui/section";
|
|
6
|
+
import { Input } from "../ui/input";
|
|
7
|
+
import { Textarea } from "../ui/textarea";
|
|
8
|
+
import { Label } from "../ui/label";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
10
|
+
import { useFormSubmit } from "../lib/use-form-submit";
|
|
11
|
+
|
|
12
|
+
interface ContactFormProps {
|
|
13
|
+
id?: string;
|
|
14
|
+
action: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
nameLabel?: string;
|
|
18
|
+
emailLabel?: string;
|
|
19
|
+
messageLabel?: string;
|
|
20
|
+
submitLabel?: string;
|
|
21
|
+
successTitle?: string;
|
|
22
|
+
successMessage?: string;
|
|
23
|
+
variant?: "section" | "card";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ContactForm({
|
|
27
|
+
id,
|
|
28
|
+
action,
|
|
29
|
+
title = "Get in Touch",
|
|
30
|
+
description,
|
|
31
|
+
nameLabel = "Name",
|
|
32
|
+
emailLabel = "Email",
|
|
33
|
+
messageLabel = "Message",
|
|
34
|
+
submitLabel = "Send Message",
|
|
35
|
+
successTitle = "Message Sent!",
|
|
36
|
+
successMessage = "We'll get back to you as soon as possible.",
|
|
37
|
+
variant = "section",
|
|
38
|
+
}: ContactFormProps) {
|
|
39
|
+
const uid = useId();
|
|
40
|
+
const { status, errorMsg, submit } = useFormSubmit(action);
|
|
41
|
+
const [fields, setFields] = useState({ name: "", email: "", message: "" });
|
|
42
|
+
|
|
43
|
+
function update(key: string, value: string) {
|
|
44
|
+
setFields((prev) => ({ ...prev, [key]: value }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleSubmit(e: FormEvent) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
submit(fields);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const successContent = (
|
|
53
|
+
<div className="text-center py-12">
|
|
54
|
+
<div className="w-16 h-16 rounded-full bg-primary/10 mx-auto mb-4 flex items-center justify-center">
|
|
55
|
+
<CheckCircle className="w-8 h-8 text-primary" />
|
|
56
|
+
</div>
|
|
57
|
+
<h3 className="text-xl font-semibold text-foreground mb-2">{successTitle}</h3>
|
|
58
|
+
<p className="text-muted-foreground">{successMessage}</p>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const formContent =
|
|
63
|
+
status === "success" ? (
|
|
64
|
+
successContent
|
|
65
|
+
) : (
|
|
66
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
67
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
<Label htmlFor={`${uid}-name`}>{nameLabel}</Label>
|
|
70
|
+
<Input
|
|
71
|
+
id={`${uid}-name`}
|
|
72
|
+
name="name"
|
|
73
|
+
placeholder="Your name"
|
|
74
|
+
value={fields.name}
|
|
75
|
+
onChange={(e) => update("name", e.target.value)}
|
|
76
|
+
required
|
|
77
|
+
disabled={status === "loading"}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
<Label htmlFor={`${uid}-email`}>{emailLabel}</Label>
|
|
82
|
+
<Input
|
|
83
|
+
id={`${uid}-email`}
|
|
84
|
+
name="email"
|
|
85
|
+
type="email"
|
|
86
|
+
placeholder="you@example.com"
|
|
87
|
+
value={fields.email}
|
|
88
|
+
onChange={(e) => update("email", e.target.value)}
|
|
89
|
+
required
|
|
90
|
+
disabled={status === "loading"}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="space-y-2">
|
|
95
|
+
<Label htmlFor={`${uid}-message`}>{messageLabel}</Label>
|
|
96
|
+
<Textarea
|
|
97
|
+
id={`${uid}-message`}
|
|
98
|
+
name="message"
|
|
99
|
+
placeholder="Your message..."
|
|
100
|
+
value={fields.message}
|
|
101
|
+
onChange={(e) => update("message", e.target.value)}
|
|
102
|
+
required
|
|
103
|
+
disabled={status === "loading"}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
<Button
|
|
107
|
+
type="submit"
|
|
108
|
+
variant="default"
|
|
109
|
+
size="lg"
|
|
110
|
+
className="w-full mt-2"
|
|
111
|
+
disabled={status === "loading"}
|
|
112
|
+
>
|
|
113
|
+
{status === "loading" ? (
|
|
114
|
+
<>
|
|
115
|
+
<Loader2 className="size-4 animate-spin mr-2" />
|
|
116
|
+
Sending...
|
|
117
|
+
</>
|
|
118
|
+
) : (
|
|
119
|
+
submitLabel
|
|
120
|
+
)}
|
|
121
|
+
</Button>
|
|
122
|
+
{status === "error" && (
|
|
123
|
+
<p className="text-sm text-destructive" aria-live="polite">
|
|
124
|
+
{errorMsg}
|
|
125
|
+
</p>
|
|
126
|
+
)}
|
|
127
|
+
</form>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (variant === "card") {
|
|
131
|
+
return (
|
|
132
|
+
<div className="glass-2 rounded-2xl p-8">{formContent}</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Section id={id}>
|
|
138
|
+
<div className="container-main max-w-2xl">
|
|
139
|
+
{(title || description) && (
|
|
140
|
+
<div className="text-center mb-12">
|
|
141
|
+
{title && (
|
|
142
|
+
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
|
|
143
|
+
{title}
|
|
144
|
+
</h2>
|
|
145
|
+
)}
|
|
146
|
+
{description && (
|
|
147
|
+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
148
|
+
{description}
|
|
149
|
+
</p>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
<div className="glass-1 rounded-2xl p-8">{formContent}</div>
|
|
154
|
+
</div>
|
|
155
|
+
</Section>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { Children, isValidElement } from "react";
|
|
2
|
+
import { FormClient, type FormFieldDef } from "./FormClient";
|
|
3
|
+
|
|
4
|
+
interface FormFieldProps {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
type?: "text" | "email" | "tel" | "url" | "textarea" | "select";
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
options?: string[];
|
|
11
|
+
colSpan?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function FormField(_props: FormFieldProps) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface FormProps {
|
|
19
|
+
id?: string;
|
|
20
|
+
action: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
columns?: number;
|
|
24
|
+
submitLabel?: string;
|
|
25
|
+
successTitle?: string;
|
|
26
|
+
successMessage?: string;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Form({ children, ...rest }: FormProps) {
|
|
31
|
+
const fields: FormFieldDef[] = [];
|
|
32
|
+
|
|
33
|
+
Children.forEach(children, (child) => {
|
|
34
|
+
if (!isValidElement(child)) return;
|
|
35
|
+
if (child.type === FormField) {
|
|
36
|
+
fields.push(child.props as FormFieldDef);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return <FormClient {...rest} fields={fields} />;
|
|
41
|
+
}
|