Skip to content

Commit

Permalink
Initial Create Link UI component.
Browse files Browse the repository at this point in the history
  • Loading branch information
pheralb committed Jan 7, 2024
1 parent cc4417e commit e5a9f71
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 40 deletions.
13 changes: 8 additions & 5 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from "@/ui/button";
import { PlusIcon } from "lucide-react";

import DashboardRoutesComponent from "@/components/dashboard-routes";
import { CreateLink } from "@/components/links/create-link";

interface DashboardLayoutProps {
children: ReactNode;
Expand All @@ -13,17 +14,19 @@ interface DashboardLayoutProps {
const DashboardLayout = (props: DashboardLayoutProps) => {
return (
<>
<nav className="border-b border-gray-100 dark:border-neutral-800 bg-gray-50 dark:bg-neutral-800/50">
<nav className="border-b border-gray-100 bg-gray-50 dark:border-neutral-800 dark:bg-neutral-800/50">
<Container>
<div className="mx-auto">
<div className="flex w-full items-center justify-between">
<div className="mt-0 flex flex-row space-x-0 text-sm font-medium rtl:space-x-reverse">
<DashboardRoutesComponent />
</div>
<Button>
<PlusIcon size={16} />
<span>Create Link</span>
</Button>
<CreateLink>
<Button>
<PlusIcon size={16} />
<span>Create Link</span>
</Button>
</CreateLink>
</div>
</div>
</Container>
Expand Down
29 changes: 0 additions & 29 deletions src/components/create-url.tsx

This file was deleted.

185 changes: 185 additions & 0 deletions src/components/links/create-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"use client";

import type { z } from "zod";
import { CreateLinkSchema } from "@/server/schemas";
import { useState, type ReactNode } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";

import { api } from "@/server/trpc/react";

import Alert from "@/ui/alert";
import { Button } from "@/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/ui/form";
import { Input, Textarea } from "@/ui/input";
import { LoaderIcon, PlusIcon, RocketIcon, ShuffleIcon } from "lucide-react";

interface CreateLinkProps {
children: ReactNode;
}

export function CreateLink(props: CreateLinkProps) {
const [loading, setLoading] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false);
const [message, setMessage] = useState<string>("");
const [isError, setError] = useState<boolean>(false);

// Main form:
const form = useForm<z.infer<typeof CreateLinkSchema>>({
resolver: zodResolver(CreateLinkSchema),
defaultValues: {
url: "",
slug: "",
description: "",
},
});

// Create link mutation:
const { mutate } = api.links.createLink.useMutation({
onSuccess: () => {
setLoading(false);
setOpen(false);
toast("Link created successfully");
form.reset();
},
onError: () => {
setLoading(false);
setError(true);
setMessage(
"Slug already exists. Please try another one or click 'Randomize' button.",
);
},
});

// Form Submit method:
const onSubmit = (values: z.infer<typeof CreateLinkSchema>) => {
// Check if slug & url are equals to prevent infinite redirect =>
setError(false);
setMessage("");
if (values.slug === values.url) {
setLoading(false);
setError(true);
setMessage("The URL and the slug cannot be the same");
return;
}
setLoading(true);
mutate(values);
};

const handleGenerateRandomSlug = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const randomSlug = Math.random().toString(36).substring(7);
form.setValue("slug", randomSlug);
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent>
<DialogHeader className="mb-2">
<DialogTitle>Create new link</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-5">
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Destination URL:</FormLabel>
<FormControl>
<Input
{...field}
placeholder="https://"
disabled={loading}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Short link:</FormLabel>
<FormControl>
<div className="relative flex items-center">
<Input
{...field}
placeholder="mylink"
disabled={loading}
/>
<Button
onClick={handleGenerateRandomSlug}
variant="secondary"
className="absolute right-0"
>
<ShuffleIcon size={14} />
<span>Randomize</span>
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description (optional):</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="Enter a description"
disabled={loading}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isError && <Alert variant="error">{message}</Alert>}
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost" disabled={loading}>
Cancel
</Button>
</DialogClose>
<Button type="submit" disabled={loading}>
{loading ? (
<LoaderIcon size={16} className="animate-spin" />
) : (
<RocketIcon size={16} />
)}
<span>{loading ? "Creating..." : "Create"}</span>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
22 changes: 19 additions & 3 deletions src/server/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,24 @@ export const LinkSchema = z.object({
});

export const CreateLinkSchema = z.object({
url: z.string(),
slug: z.string(),
url: z
.string()
.min(1, { message: "URL is required." })
.url({
message: "Please enter a valid URL. Include http:// or https://",
})
.regex(/^\S+$/, {
message: "URL must not contain any blank spaces.",
}),
slug: z
.string()
.min(1, {
message:
"Short link is required. Enter a custom slug or click on 'Randomize' button.",
})
.regex(/^\S+$/, {
message: "Custom short link must not contain any blank spaces.",
}),
description: z.string(),
});

Expand Down Expand Up @@ -72,4 +88,4 @@ export const newPasswordSchema = z.object({
password: z.string().min(6, {
message: "Password must be at least 6 characters long.",
}),
});
});
6 changes: 3 additions & 3 deletions src/server/trpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";

import { getServerAuthSession } from "@/server/auth";
import { auth } from "@/server/auth";
import { db } from "@/server/db";

export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await getServerAuthSession();
const session = await auth();

return {
db,
Expand Down Expand Up @@ -53,4 +53,4 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
});
});

export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);

0 comments on commit e5a9f71

Please sign in to comment.