Skip to content

Commit

Permalink
Add delete link + update link improvements & fix search bar
Browse files Browse the repository at this point in the history
  • Loading branch information
pheralb committed Mar 18, 2024
1 parent 425f8a3 commit d62b126
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 16 deletions.
32 changes: 27 additions & 5 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { Metadata } from "next";

import { getLinksByUser } from "@/server/queries";

import CardLink from "@/components/links/card-link";
import LinksLimit from "@/components/links/links-limit";
import SearchLinks from "@/components/links/search-link";
import { CreateLink } from "@/components/links/create-link";
import { Button } from "@/ui/button";
import { PackageOpenIcon, PlusIcon } from "lucide-react";

export const metadata: Metadata = {
title: "Dashboard",
};

const DashboardPage = async ({
searchParams,
Expand All @@ -18,27 +27,40 @@ const DashboardPage = async ({
return <div>Error</div>;
}

const filteredLinks = data.links.filter((link) => {
if (!query) return true;
return link.slug.includes(query);
});

return (
<>
<div className="mb-2 flex w-full items-center justify-between">
<SearchLinks className="w-72 max-w-72" />
<LinksLimit length={data.links.length} />
</div>
<div className="grid grid-cols-1 gap-2 md:grid-cols-1 lg:grid-cols-2">
{data.links
{filteredLinks
.sort((a, b) => {
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
})
.filter((link) => {
if (!query) return true;
return link.slug.includes(query);
})
.map((link) => {
return <CardLink key={link.id} linkInfo={link} />;
})}
</div>
{filteredLinks.length === 0 && (
<div className="mt-4 flex flex-col items-center justify-center space-y-3 text-center">
<PackageOpenIcon size={40} strokeWidth={0.5} />
<p>No slug found.</p>
<CreateLink slug={query}>
<Button variant="outline">
<PlusIcon size={14} />
<p>Create link with <span className="font-mono">{query}</span> slug</p>
</Button>
</CreateLink>
</div>
)}
</>
);
};
Expand Down
18 changes: 14 additions & 4 deletions src/components/links/card-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { Links } from "@prisma/client";
import ExternalLink from "@/ui/external-link";
import { formatDate } from "@/utils/formatDate";
import EditLink from "./edit-link";
import { CopyIcon, SettingsIcon } from "lucide-react";
import { CopyIcon, SettingsIcon, TrashIcon } from "lucide-react";
import CopyLink from "./copy-link";
import DeleteLink from "./delete-link";

interface CardLinkProps {
linkInfo: Links;
Expand Down Expand Up @@ -35,6 +36,14 @@ const CardLink = ({ linkInfo }: CardLinkProps) => {
}
link={linkInfo}
/>
<DeleteLink
link={linkInfo}
trigger={
<button className="transition-opacity hover:opacity-75">
<TrashIcon size={16} />
</button>
}
/>
</div>
</div>
<p
Expand All @@ -43,10 +52,11 @@ const CardLink = ({ linkInfo }: CardLinkProps) => {
>
{linkInfo.url}
</p>
<div className="flex items-center justify-end">
<p className="font-mono text-xs font-medium text-neutral-600 dark:text-neutral-500">
{formatDate(linkInfo.createdAt)}
<div className="flex items-center justify-between space-x-2 font-mono text-xs font-medium text-neutral-600 dark:text-neutral-500">
<p className="max-w-[75%] truncate" title={linkInfo.description ?? ""}>
{linkInfo.description}
</p>
<p>{formatDate(linkInfo.createdAt)}</p>
</div>
</div>
);
Expand Down
10 changes: 6 additions & 4 deletions src/components/links/create-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { LoaderIcon, RocketIcon, ShuffleIcon } from "lucide-react";

interface CreateLinkProps {
children: ReactNode;
slug?: string;
}

export function CreateLink(props: CreateLinkProps) {
Expand All @@ -50,7 +51,7 @@ export function CreateLink(props: CreateLinkProps) {
resolver: zodResolver(CreateLinkSchema),
defaultValues: {
url: "",
slug: "",
slug: props.slug ?? "",
description: "",
},
});
Expand Down Expand Up @@ -94,10 +95,11 @@ export function CreateLink(props: CreateLinkProps) {
duration: 10000,
closeButton: true,
});

setOpen(false);
} catch (error) {
toast.error("An unexpected error has occurred. Please try again later.");
} finally {
setOpen(false);
setError(false);
setMessage("");
setLoading(false);
Expand Down Expand Up @@ -152,8 +154,8 @@ export function CreateLink(props: CreateLinkProps) {
/>
<Button
onClick={handleGenerateRandomSlug}
variant="secondary"
className="absolute right-0"
variant="outline"
className="absolute right-0 rounded-none rounded-tr-md rounded-br-md"
>
<ShuffleIcon size={14} />
<span>Randomize</span>
Expand Down
122 changes: 122 additions & 0 deletions src/components/links/delete-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import type { Links } from "@prisma/client";
import type { z } from "zod";
import { useState, type ReactNode } from "react";

import { deleteLink } from "@/server/actions/links";
import { DeleteLinkSchema } from "@/server/schemas";

import { toast } from "sonner";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

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

interface DeleteLinkProps {
link: Links;
trigger: ReactNode;
}

const DeleteLink = ({ link, trigger }: DeleteLinkProps) => {
const [open, setOpen] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);

const form = useForm<z.infer<typeof DeleteLinkSchema>>({
resolver: zodResolver(DeleteLinkSchema),
});

const handleDelete = async (values: z.infer<typeof DeleteLinkSchema>) => {
if (values.slug !== link.slug) {
toast.error("The slug does not match.");
return;
}

try {
setLoading(true);
await deleteLink(link.id);
setOpen(false);
toast.success("Link deleted successfully.", {
description: `The link /${link.slug} has been deleted.`,
});
} catch (error) {
toast.error(
"An error occurred while deleting the link. Please try again.",
);
} finally {
setLoading(false);
}
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete /{link.slug}</DialogTitle>
<DialogDescription className="text-red-500 dark:text-red-400">
Access to the link will be permanently removed. This action cannot
be undone.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleDelete)}>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>
Type <span className="font-mono">{link.slug}</span> to
confirm:
</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="mt-3">
<DialogClose asChild>
<Button variant="ghost" disabled={loading}>
Cancel
</Button>
</DialogClose>
<Button disabled={loading} type="submit">
{loading ? (
<LoaderIcon size={16} className="animate-spin" />
) : (
<TrashIcon size={16} />
)}
<span>{loading ? "Deleting..." : "Delete"}</span>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

export default DeleteLink;
41 changes: 38 additions & 3 deletions src/components/links/edit-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
DialogTrigger,
} from "@/ui/dialog";
import { Button } from "@/ui/button";
import { LoaderIcon, SaveIcon } from "lucide-react";
import { LoaderIcon, LockIcon, LockOpenIcon, SaveIcon } from "lucide-react";
import Alert from "@/ui/alert";
import {
Form,
Expand All @@ -33,6 +33,7 @@ import {
} from "@/ui/form";
import { Input, Textarea } from "@/ui/input";
import { EditLinkSchema } from "@/server/schemas";
import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover";

interface EditLinkProps {
trigger: ReactNode;
Expand All @@ -44,11 +45,13 @@ const EditLink = (props: EditLinkProps) => {
const [open, setOpen] = useState<boolean>(false);
const [message, setMessage] = useState<string>("");
const [isError, setError] = useState<boolean>(false);
const [unlockSlug, setUnlockSlug] = useState<boolean>(true);

// Main form:
const form = useForm<z.infer<typeof EditLinkSchema>>({
resolver: zodResolver(EditLinkSchema),
defaultValues: {
id: props.link.id,
url: props.link.url,
slug: props.link.slug,
description: props.link.description ?? "",
Expand Down Expand Up @@ -124,8 +127,41 @@ const EditLink = (props: EditLinkProps) => {
<Input
{...field}
placeholder={props.link.slug}
disabled={true}
disabled={unlockSlug}
/>
{unlockSlug ? (
<Popover>
<PopoverTrigger className="absolute bottom-0 right-0 top-0 flex items-center px-3">
<LockIcon size={16} />
</PopoverTrigger>
<PopoverContent
sideOffset={0.2}
className="text-sm"
>
<p className="mb-2">
Editing the custom link will remove access from
the previous link and it will be available to
everyone. Are you sure you want to continue?
</p>
<Button
onClick={() => setUnlockSlug(false)}
variant="outline"
className="w-full"
>
<LockOpenIcon size={16} />
<span>Unlock</span>
</Button>
</PopoverContent>
</Popover>
) : (
<button
type="button"
onClick={() => setUnlockSlug(true)}
className="absolute bottom-0 right-0 top-0 flex items-center px-3"
>
<LockOpenIcon size={16} />
</button>
)}
</div>
</FormControl>
<FormMessage />
Expand All @@ -142,7 +178,6 @@ const EditLink = (props: EditLinkProps) => {
<Textarea
{...field}
placeholder={props.link.description ?? "Description"}

disabled={loading}
/>
</FormControl>
Expand Down
2 changes: 2 additions & 0 deletions src/components/links/search-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const SearchLinks = (props: SearchLinksProps) => {
size={16}
/>
<Input
type="search"
autoComplete="off"
placeholder="Search links"
className="pl-8"
onChange={handleSearch}
Expand Down

0 comments on commit d62b126

Please sign in to comment.