Skip to content

Commit

Permalink
Fix tag name length validation and add delete tag functionality + car…
Browse files Browse the repository at this point in the history
…d link improvements
  • Loading branch information
pheralb committed Mar 31, 2024
1 parent a8252dd commit 0f397ea
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 34 deletions.
9 changes: 3 additions & 6 deletions src/components/links/card-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,18 @@ const CardLink = ({ linkInfo, linkTags, tagsInfo }: CardLinkProps) => {
{linkInfo.url}
</p>
<Collapsible>
<div className="flex items-center justify-between font-mono text-xs font-medium text-neutral-600 dark:text-neutral-500 md:space-x-2">
<div className="flex items-center justify-between font-mono text-xs font-medium text-neutral-600 dark:text-neutral-400 md:space-x-2">
<div className="flex max-w-[75%] items-center space-x-2">
{linkTags.length > 0 && (
<div className="flex items-center space-x-1">
<div className="flex cursor-default items-center space-x-1">
{linkTags.map((tag) => {
const tagInfo = tagsInfo.find((t) => t.id === tag.tagId);
return (
<span
key={tag.tagId}
className={cn(
"rounded-md border border-neutral-200 px-2 py-[0.5px] font-mono text-xs dark:border-neutral-800",
"rounded-md border border-neutral-200 px-2 py-[0.5px] font-mono text-xs dark:border-neutral-800",
)}
style={{
color: tagInfo?.color ?? "transparent",
}}
>
{tagInfo?.name}
</span>
Expand Down
5 changes: 5 additions & 0 deletions src/components/links/create-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export function CreateLink(props: CreateLinkProps) {
setSelectedTags(selectedTags.filter((tag) => tag !== tagId));
return;
}

if (selectedTags.length >= 2) {
toast.error("You can't add more than 2 tags to a link.");
return;
}
setSelectedTags([...selectedTags, tagId]);
};

Expand Down
4 changes: 3 additions & 1 deletion src/components/links/select-tags-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ interface SelectTagsLinkProps {
const SelectTagsLink = (props: SelectTagsLinkProps) => {
return (
<div className="space-y-2">
<p>Add tags to your link:</p>
<p className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Add tags to your link:
</p>
<Select onValueChange={(value) => props.onSelectTag(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a tag" />
Expand Down
13 changes: 0 additions & 13 deletions src/components/tags/create-tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,6 @@ export function CreateTag(props: CreateTagProps) {
</FormItem>
)}
/>
<FormField
control={form.control}
name="color"
render={({ field }) => (
<FormItem>
<FormLabel>Color:</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isError && <Alert variant="error">{message}</Alert>}
</div>
<DialogFooter>
Expand Down
72 changes: 72 additions & 0 deletions src/components/tags/delete-tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import type { Tags } from "@prisma/client";
import { type ReactNode, useState } from "react";

import { toast } from "sonner";

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/ui/dialog";

import { removeTag } from "@/server/actions/tags";
import { Button } from "@/ui/button";
import { LoaderIcon } from "lucide-react";

interface DeleteTagProps {
tag: Tags;
trigger: ReactNode;
}

const DeleteTag = ({ trigger, tag }: DeleteTagProps) => {
const [open, setOpen] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);

const handleDeleteTag = async () => {
try {
setLoading(true);
await removeTag(tag.id);
setOpen(false);
toast.success("Link deleted successfully.", {
description: `The tag ${tag.name} has been deleted.`,
});
} catch (error) {
toast.error(
"An error occurred while deleting the tag. Please try again.",
);
} finally {
setLoading(false);
}
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete "{tag.name}" tag</DialogTitle>
<DialogDescription>
Delete the tag will not delete the links associated with it.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="destructive"
onClick={handleDeleteTag}
disabled={loading}
>
{loading ? <LoaderIcon size={16} /> : "Delete Tag"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default DeleteTag;
72 changes: 59 additions & 13 deletions src/components/tags/search-tags.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
"use client";

import type { Tags } from "@prisma/client";
import type { ReactNode } from "react";
import { useState } from "react";

import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover";
import { CreateTag } from "./create-tag";
import { Button } from "@/ui/button";
import { CheckIcon, PlusIcon, SearchXIcon } from "lucide-react";
import {
CheckIcon,
PlusIcon,
SearchXIcon,
TagIcon,
TagsIcon,
XIcon,
} from "lucide-react";
import DeleteTag from "./delete-tag";

interface SearchTagProps {
tags: Tags[];
tagSelected: string;
children: ReactNode;
tagName?: string;
}

const SearchTag = (props: SearchTagProps) => {
const [isOpened, setIsOpened] = useState<boolean>(false);
const searchTagParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
Expand All @@ -37,28 +46,65 @@ const SearchTag = (props: SearchTagProps) => {
};

return (
<Popover>
<PopoverTrigger asChild>{props.children}</PopoverTrigger>
<Popover open={isOpened} onOpenChange={setIsOpened}>
<PopoverTrigger asChild>
<Button variant="outline">
{isOpened ? <XIcon size={16} /> : <TagsIcon size={16} />}
{props.tagName ? (
<span>
{props.tags.map((tag) => {
if (tag.id === props.tagName) {
return tag.name;
}
})}
</span>
) : (
<span className="hidden md:block">Select a tag</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent>
<p className="text-center my-2 font-medium">My Tags ({props.tags.length})</p>
<p className="my-2 text-center font-medium">
My Tags ({props.tags.length})
</p>
<div className="mb-2 flex w-full flex-col space-y-1">
{props.tags.length === 0 && (
<div className="my-4 flex flex-col items-center justify-center space-y-2 text-sm text-neutral-500 dark:text-neutral-400">
<TagIcon size={24} strokeWidth={1.5} />
<span>No tags found</span>
</div>
)}
{props.tags.map((tag) => {
return (
<button
<div
key={tag.id}
value={tag.id}
onClick={() => handleSearchTag(tag.id)}
className="flex w-full items-center justify-between rounded-md px-2 py-1 text-left text-sm transition-colors duration-200 hover:opacity-80"
aria-label={tag.name}
className="flex w-full items-center justify-between rounded-md border border-neutral-200 px-2 py-1 text-left text-sm transition-colors duration-200 hover:opacity-80 dark:border-neutral-800"
style={{
backgroundColor: tag.color
? `${tag.color}`
: "rgba(23, 23, 23, 0.5)" || "#171717",
color: tag.color ? "#fff" : "#171717",
}}
>
<span>{tag.name}</span>
{tag.id === props.tagSelected && <CheckIcon size={16} />}
</button>
<button
onClick={() => handleSearchTag(tag.id)}
className="w-full text-start"
>
{tag.name}
</button>
<div className="flex items-center space-x-2">
{tag.id === props.tagSelected && <CheckIcon size={16} />}
<DeleteTag
tag={tag}
trigger={
<button className="rounded-md p-1 hover:opacity-80">
<XIcon size={16} />
</button>
}
/>
</div>
</div>
);
})}
</div>
Expand Down
24 changes: 24 additions & 0 deletions src/server/actions/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,29 @@ export const insertTagToLink = async (linkId: string, tagId: string) => {

revalidatePath("/");

return;
};

/**
* Remove a tag.
* Authentication required.
* @type {string()}
*/
export const removeTag = async (tagId: string) => {
const currentUser = await auth();

if (!currentUser) {
console.error("Not authenticated.");
return null;
}

await db.tags.delete({
where: {
id: tagId,
},
});

revalidatePath("/");

return;
};
4 changes: 3 additions & 1 deletion src/server/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export const getSingleLinkSchema = z.object({
});

export const CreateTagSchema = z.object({
name: z.string().min(1, { message: "Tag name is required." }),
name: z.string().min(1, { message: "Tag name is required." }).max(15, {
message: "Tag name must be less than 15 characters.",
}),
color: z.string().min(1, { message: "Tag color is required." }),
});

Expand Down

0 comments on commit 0f397ea

Please sign in to comment.