Add forms and validation for domains (#27)
This commit is contained in:
@@ -12,22 +12,51 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@unsend/ui/src/dialog";
|
} from "@unsend/ui/src/dialog";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@unsend/ui/src/form";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Domain } from "@prisma/client";
|
import { Domain } from "@prisma/client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "@unsend/ui/src/toaster";
|
import { toast } from "@unsend/ui/src/toaster";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
const domainSchema = z.object({
|
||||||
|
domain: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
|
export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [domainName, setDomainName] = useState("");
|
const [domainName, setDomainName] = useState("");
|
||||||
const deleteDomainMutation = api.domain.deleteDomain.useMutation();
|
const deleteDomainMutation = api.domain.deleteDomain.useMutation();
|
||||||
|
|
||||||
|
const domainForm = useForm<z.infer<typeof domainSchema>>({
|
||||||
|
resolver: zodResolver(domainSchema),
|
||||||
|
});
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function handleSave() {
|
async function onDomainDelete(values: z.infer<typeof domainSchema>) {
|
||||||
|
if (values.domain !== domain.name) {
|
||||||
|
domainForm.setError("domain", {
|
||||||
|
message: "Domain name does not match",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deleteDomainMutation.mutate(
|
deleteDomainMutation.mutate(
|
||||||
{
|
{
|
||||||
id: domain.id,
|
id: domain.id,
|
||||||
@@ -62,30 +91,41 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
|
|||||||
You can't reverse this.
|
You can't reverse this.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-2">
|
<Form {...domainForm}>
|
||||||
<Label htmlFor="name" className="text-right">
|
<form
|
||||||
Type <span className="text-primary">{domain.name}</span> to confirm
|
onSubmit={domainForm.handleSubmit(onDomainDelete)}
|
||||||
</Label>
|
className="space-y-4"
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
defaultValue=""
|
|
||||||
className="mt-2"
|
|
||||||
onChange={(e) => setDomainName(e.target.value)}
|
|
||||||
value={domainName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={
|
|
||||||
deleteDomainMutation.isPending || domainName !== domain.name
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{deleteDomainMutation.isPending ? "Deleting..." : "Delete"}
|
<FormField
|
||||||
</Button>
|
control={domainForm.control}
|
||||||
</DialogFooter>
|
name="domain"
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="subdomain.example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
{formState.errors.domain ? (
|
||||||
|
<FormMessage />
|
||||||
|
) : (
|
||||||
|
<FormDescription className=" text-transparent">
|
||||||
|
.
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={deleteDomainMutation.isPending}
|
||||||
|
>
|
||||||
|
{deleteDomainMutation.isPending ? "Deleting..." : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@@ -2,34 +2,60 @@
|
|||||||
|
|
||||||
import { Button } from "@unsend/ui/src/button";
|
import { Button } from "@unsend/ui/src/button";
|
||||||
import { Input } from "@unsend/ui/src/input";
|
import { Input } from "@unsend/ui/src/input";
|
||||||
import { Label } from "@unsend/ui/src/label";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@unsend/ui/src/dialog";
|
} from "@unsend/ui/src/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@unsend/ui/src/form";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import * as tldts from "tldts";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
const domainSchema = z.object({
|
||||||
|
domain: z.string({ required_error: "Domain is required" }),
|
||||||
|
});
|
||||||
|
|
||||||
export default function AddDomain() {
|
export default function AddDomain() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [domainName, setDomainName] = useState("");
|
|
||||||
const addDomainMutation = api.domain.createDomain.useMutation();
|
const addDomainMutation = api.domain.createDomain.useMutation();
|
||||||
|
|
||||||
|
const domainForm = useForm<z.infer<typeof domainSchema>>({
|
||||||
|
resolver: zodResolver(domainSchema),
|
||||||
|
});
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function handleSave() {
|
async function onDomainAdd(values: z.infer<typeof domainSchema>) {
|
||||||
|
const domain = tldts.getDomain(values.domain);
|
||||||
|
if (!domain) {
|
||||||
|
domainForm.setError("domain", {
|
||||||
|
message: "Invalid domain",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
addDomainMutation.mutate(
|
addDomainMutation.mutate(
|
||||||
{
|
{
|
||||||
name: domainName,
|
name: values.domain,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
@@ -55,30 +81,45 @@ export default function AddDomain() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add a new domain</DialogTitle>
|
<DialogTitle>Add a new domain</DialogTitle>
|
||||||
<DialogDescription>This creates a new domain</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Label htmlFor="name" className="text-right">
|
<Form {...domainForm}>
|
||||||
Domain Name
|
<form
|
||||||
</Label>
|
onSubmit={domainForm.handleSubmit(onDomainAdd)}
|
||||||
<Input
|
className="space-y-8"
|
||||||
id="name"
|
>
|
||||||
placeholder="subdomain.example.com"
|
<FormField
|
||||||
defaultValue=""
|
control={domainForm.control}
|
||||||
className="col-span-3"
|
name="domain"
|
||||||
onChange={(e) => setDomainName(e.target.value)}
|
render={({ field, formState }) => (
|
||||||
value={domainName}
|
<FormItem>
|
||||||
/>
|
<FormLabel>Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="subdomain.example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
{formState.errors.domain ? (
|
||||||
|
<FormMessage />
|
||||||
|
) : (
|
||||||
|
<FormDescription>
|
||||||
|
Use subdomains to separate transactional and marketing
|
||||||
|
emails.{" "}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
className=" w-[100px] bg-white hover:bg-gray-100 focus:bg-gray-100"
|
||||||
|
type="submit"
|
||||||
|
disabled={addDomainMutation.isPending}
|
||||||
|
>
|
||||||
|
{addDomainMutation.isPending ? "Adding..." : "Add"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={addDomainMutation.isPending}
|
|
||||||
>
|
|
||||||
Save changes
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@@ -7,6 +7,12 @@ import { db } from "~/server/db";
|
|||||||
const dnsResolveTxt = util.promisify(dns.resolveTxt);
|
const dnsResolveTxt = util.promisify(dns.resolveTxt);
|
||||||
|
|
||||||
export async function createDomain(teamId: number, name: string) {
|
export async function createDomain(teamId: number, name: string) {
|
||||||
|
const domainStr = tldts.getDomain(name);
|
||||||
|
|
||||||
|
if (!domainStr) {
|
||||||
|
throw new Error("Invalid domain");
|
||||||
|
}
|
||||||
|
|
||||||
const subdomain = tldts.getSubdomain(name);
|
const subdomain = tldts.getSubdomain(name);
|
||||||
const publicKey = await ses.addDomain(name);
|
const publicKey = await ses.addDomain(name);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user