First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { CompanyLetterheadDefaults } from "@/src/dict/company-dict";
|
||||
import { Mail, MapPin, Phone, type LucideIcon } from "lucide-react";
|
||||
|
||||
export type ContactChannel = {
|
||||
Icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
export const ContactChannels: ContactChannel[] = [
|
||||
{
|
||||
Icon: Mail,
|
||||
label: "Email",
|
||||
value: CompanyLetterheadDefaults["email"],
|
||||
href: `mailto:${CompanyLetterheadDefaults["email"]}`,
|
||||
},
|
||||
{
|
||||
Icon: Phone,
|
||||
label: "Phone",
|
||||
value: CompanyLetterheadDefaults["phone"],
|
||||
href: `tel:${CompanyLetterheadDefaults["phone"].replace(/\s/g, "")}`,
|
||||
},
|
||||
{
|
||||
Icon: MapPin,
|
||||
label: "Office",
|
||||
value: CompanyLetterheadDefaults["address_lines"].join(", "),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,43 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { ApiReqParams, ContactFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function submitContactForm({
|
||||
setLoading,
|
||||
setStatus,
|
||||
form,
|
||||
}: ReturnType<typeof useFormInit<ContactFormObject>>) {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/contact`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
contact: {
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
subject: form.subject,
|
||||
message: form.message,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
setStatus({
|
||||
success: true,
|
||||
msg: res.msg || "Message sent successfully.",
|
||||
});
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Message Failed",
|
||||
});
|
||||
console.log("res", res);
|
||||
console.log("form", form);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import type { ContactChannel } from "../(data)/contact-channels";
|
||||
|
||||
type Props = ContactChannel & {
|
||||
index: number;
|
||||
};
|
||||
|
||||
export default function ContactChannelItem({
|
||||
Icon,
|
||||
label,
|
||||
value,
|
||||
href,
|
||||
index,
|
||||
}: Props) {
|
||||
return (
|
||||
<Row className="w-full flex-nowrap items-start justify-between gap-6! py-6 border-t border-slate-300 dark:border-white/20">
|
||||
<Stack className="gap-2! min-w-0">
|
||||
<Span className="text-xs text-slate-400">
|
||||
{String(index + 1).padStart(2, "0")} / {label}
|
||||
</Span>
|
||||
{href ? (
|
||||
<Link
|
||||
href={href}
|
||||
className="text-lg font-medium tracking-tight text-foreground-light dark:text-foreground-dark border-0 leading-snug break-words hover:text-accent"
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
) : (
|
||||
<Span className="text-lg font-medium tracking-tight text-foreground-light dark:text-foreground-dark leading-snug">
|
||||
{value}
|
||||
</Span>
|
||||
)}
|
||||
</Stack>
|
||||
<Icon
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
className="text-slate-400 shrink-0 mt-1"
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Tag from "@moduletrace/twui/elements/Tag";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import type { TWUIStatusType } from "@moduletrace/twui/hooks/useStatus";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
status?: TWUIStatusType;
|
||||
};
|
||||
|
||||
export default function ContactFormStatus({ status }: Props) {
|
||||
if (!status?.error || !status.msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="error"
|
||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||
>
|
||||
<Row className="flex-nowrap gap-2!">
|
||||
<TriangleAlert size={15} className="shrink-0" />
|
||||
<Span>{status.msg}</Span>
|
||||
</Row>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import H3 from "@moduletrace/twui/layout/H3";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import { CircleCheck, RotateCcw } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
msg?: string;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
export default function ContactFormSuccess({ msg, onReset }: Props) {
|
||||
return (
|
||||
<Stack className="w-full items-start gap-6! py-6">
|
||||
<CircleCheck size={28} strokeWidth={1.5} className="text-accent" />
|
||||
<Stack className="gap-2!">
|
||||
<H3>Message sent</H3>
|
||||
<Span className="text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{msg || "Thanks for reaching out."} We'll reply to the
|
||||
email address you provided.
|
||||
</Span>
|
||||
</Stack>
|
||||
<Button
|
||||
title="Send another message"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
onClick={onReset}
|
||||
beforeIcon={<RotateCcw size={15} />}
|
||||
>
|
||||
Send another
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type ContactFormObject } from "@/src/types";
|
||||
import { Send } from "lucide-react";
|
||||
|
||||
export default function ContactFormAction({}: ReturnType<
|
||||
typeof useFormInit<ContactFormObject>
|
||||
>) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title="Submit Contact Form"
|
||||
type="submit"
|
||||
size="large"
|
||||
className="w-full mt-2"
|
||||
afterIcon={<Send size={17} />}
|
||||
>
|
||||
Send Message
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type ContactFormObject } from "@/src/types";
|
||||
|
||||
export default function ContactFormEmail({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<ContactFormObject>>) {
|
||||
return (
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type ContactFormObject } from "@/src/types";
|
||||
|
||||
export default function ContactFormMessage({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<ContactFormObject>>) {
|
||||
return (
|
||||
<Input
|
||||
istextarea
|
||||
rows={6}
|
||||
placeholder="Message"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
message: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type ContactFormObject } from "@/src/types";
|
||||
|
||||
export default function ContactFormName({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<ContactFormObject>>) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type ContactFormObject } from "@/src/types";
|
||||
|
||||
export default function ContactFormSubject({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<ContactFormObject>>) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Subject (optional)"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
subject: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import LoadingOverlay from "@moduletrace/twui/elements/LoadingOverlay";
|
||||
import Paper from "@moduletrace/twui/elements/Paper";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import H3 from "@moduletrace/twui/layout/H3";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type ContactFormObject } from "@/src/types";
|
||||
import submitContactForm from "../../(functions)/submit-contact-form";
|
||||
import ContactFormName from "./contact-form-name";
|
||||
import ContactFormEmail from "./contact-form-email";
|
||||
import ContactFormSubject from "./contact-form-subject";
|
||||
import ContactFormMessage from "./contact-form-message";
|
||||
import ContactFormAction from "./contact-form-action";
|
||||
import ContactFormStatus from "../contact-form-status";
|
||||
import ContactFormSuccess from "../contact-form-success";
|
||||
|
||||
export default function ContactForm() {
|
||||
const init = useFormInit<ContactFormObject>();
|
||||
const { loading, status, setForm, setStatus } = init;
|
||||
|
||||
return (
|
||||
<Paper className="relative w-full p-6 sm:p-10 rounded-default-xl bg-white">
|
||||
{loading && <LoadingOverlay />}
|
||||
<Stack className="w-full gap-8!">
|
||||
<Row className="w-full items-start justify-between gap-4!">
|
||||
<Stack className="gap-2!">
|
||||
<H3>Send a message</H3>
|
||||
<Span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Fields marked * are required.
|
||||
</Span>
|
||||
</Stack>
|
||||
<Span className="text-xs text-slate-400">
|
||||
Form / 01
|
||||
</Span>
|
||||
</Row>
|
||||
{status?.success ? (
|
||||
<ContactFormSuccess
|
||||
msg={status.msg}
|
||||
onReset={() => {
|
||||
setForm({} as ContactFormObject);
|
||||
setStatus(undefined);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Form
|
||||
className="w-full"
|
||||
onSubmit={() => {
|
||||
submitContactForm(init);
|
||||
}}
|
||||
>
|
||||
<Stack className="w-full items-stretch gap-5!">
|
||||
<div className="grid w-full grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<ContactFormName {...init} />
|
||||
<ContactFormEmail {...init} />
|
||||
</div>
|
||||
<ContactFormSubject {...init} />
|
||||
<ContactFormMessage {...init} />
|
||||
<ContactFormStatus status={status} />
|
||||
<ContactFormAction {...init} />
|
||||
</Stack>
|
||||
</Form>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import H1 from "@moduletrace/twui/layout/H1";
|
||||
import { ContactChannels } from "../(data)/contact-channels";
|
||||
import ContactChannelItem from "./contact-channel-item";
|
||||
|
||||
export default function ContactInfo() {
|
||||
return (
|
||||
<Stack className="w-full gap-10! xl:sticky xl:top-28">
|
||||
<Stack className="gap-8!">
|
||||
<Span className="htag">Contact</Span>
|
||||
<H1>
|
||||
Let's talk.{" "}
|
||||
<Span className="text-slate-400">We read everything.</Span>
|
||||
</H1>
|
||||
<Span className="text-lg text-slate-600 dark:text-slate-400 leading-relaxed max-w-md">
|
||||
Questions, feedback or a project in mind. Send a note and a
|
||||
real person will get back to you, usually within one
|
||||
business day.
|
||||
</Span>
|
||||
</Stack>
|
||||
<Stack className="w-full gap-0! border-b border-slate-300 dark:border-white/20">
|
||||
{ContactChannels.map((channel, index) => (
|
||||
<ContactChannelItem
|
||||
key={channel.label}
|
||||
{...channel}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Section from "@moduletrace/twui/layout/Section";
|
||||
import Container from "@moduletrace/twui/layout/Container";
|
||||
import ContactInfo from "../(partials)/contact-info";
|
||||
import ContactForm from "../(partials)/contact-form";
|
||||
|
||||
export default function ContactSection() {
|
||||
return (
|
||||
<Section className="pt-20! xl:pt-32!">
|
||||
<Container>
|
||||
<div className="grid w-full grid-cols-1 xl:grid-cols-[1fr_minmax(0,560px)] items-start gap-16 xl:gap-24">
|
||||
<ContactInfo />
|
||||
<ContactForm />
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import ContactSection from "./(sections)/contact-section";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function ContactPage() {
|
||||
return <ContactSection />;
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Contact Us | ${SiteData["SiteName"]}`,
|
||||
description: `Get in touch with ${SiteData["SiteName"]}. We'd love to hear from you.`,
|
||||
};
|
||||
Reference in New Issue
Block a user