Skip to content

Commit

Permalink
feat: Card Actions (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-maker authored Mar 6, 2024
2 parents da3a84c + b089069 commit 06be2c7
Show file tree
Hide file tree
Showing 13 changed files with 856 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm run build
- name: Build
run: npm run build
continue-on-error: true
8 changes: 7 additions & 1 deletion app/(dashboard)/_components/board-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Overlay } from "./overlay";
import Image from "next/image";
import Link from "next/link";
import { Skeleton } from "@/components/ui/skeleton";
import { Actions } from "@/components/actions";
import { MoreHorizontal } from "lucide-react";

interface BoardCardProps {
id: string;
Expand Down Expand Up @@ -60,7 +62,11 @@ export const BoardCard = ({
<div className="relative flex-1 bg-amber-50">
<Image src={imageUrl} alt={title} fill className="object-fit" />
<Overlay />
{/*<Actions /> */}
<Actions id={id} title={title} side="right">
<button className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity px-3 py-2 outline-none">
<MoreHorizontal className="text-white opacity-75 hover:opacity-100 transition-opacity" />
</button>
</Actions>
</div>
<Footer
isFavorite={isFavorite}
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner";
import { ModalProvider } from "@/providers/modal-provider";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -21,6 +22,7 @@ export default function RootLayout({
<body className={inter.className}>
<ConvexClientProvider>
<Toaster />
<ModalProvider />
{children}
</ConvexClientProvider>
</body>
Expand Down
87 changes: 87 additions & 0 deletions components/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/convex/_generated/api";
import { useApiMutation } from "@/hooks/use-api-mutation";
import { DropdownMenuContentProps } from "@radix-ui/react-dropdown-menu";
import { Link2, Pencil, Trash } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { ConfirmDialog } from "./confirm-dialog";
import { Button } from "./ui/button";
import { useRenameModal } from "@/store/use-rename-modal";

interface ActionsProps {
children: React.ReactNode;
side?: DropdownMenuContentProps["side"];
sideOffset?: DropdownMenuContentProps["sideOffset"];
id: string;
title: string;
}

export const Actions = ({
children,
side,
sideOffset,
id,
title,
}: ActionsProps) => {
const { onOpen } = useRenameModal();
const { mutate, pending } = useApiMutation(api.board.remove);

const onCopyLink = () => {
navigator.clipboard
.writeText(`${window.location.origin}/board/${id}`)
.then(() => toast.success("Link Copied"))
.catch(() => toast.error("Failed to copy board link"));
};

const onDelete = () => {
mutate({ id })
.then(() => toast.success("Board deleted"))
.catch(() => toast.error("Failed to delete board"));
};

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent
onClick={(e) => e.stopPropagation()}
side={side}
sideOffset={sideOffset}
className="w-60"
>
<DropdownMenuItem onClick={onCopyLink} className="p-3 cursor-pointer">
<Link2 className="h-4 w-4 mr-2" />
Copy board link
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onOpen(id, title)}
className="p-3 cursor-pointer"
>
<Pencil className="h-4 w-4 mr-2" />
Rename
</DropdownMenuItem>
<ConfirmDialog
header="Delete board?"
description="This will delete the board and all of its contents."
disabled={pending}
onConfirm={onDelete}
>
<Button
variant="ghost"
className="p-3 cursor-pointer text-sm w-full justify-start font-normal"
>
<Trash className="h-4 w-4 mr-2" />
Delete
</Button>
</ConfirmDialog>
</DropdownMenuContent>
</DropdownMenu>
);
};
52 changes: 52 additions & 0 deletions components/confirm-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";

interface ConfirmModalProps {
children: React.ReactNode;
onConfirm: () => void;
disabled?: boolean;
header: string;
description?: string;
}

export const ConfirmDialog = ({
children,
onConfirm,
disabled,
header,
description,
}: ConfirmModalProps) => {
const handleConfirm = () => {
onConfirm();
};

return (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{header}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction disabled={disabled} onClick={handleConfirm}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
73 changes: 73 additions & 0 deletions components/modals/rename-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useRenameModal } from "@/store/use-rename-modal";
import { Input } from "@/components/ui/input";
import { FormEventHandler, useEffect, useState } from "react";
import { useApiMutation } from "@/hooks/use-api-mutation";
import { api } from "@/convex/_generated/api";
import { Button } from "../ui/button";
import { toast } from "sonner";

export const RenameModal = () => {
const { mutate, pending } = useApiMutation(api.board.update);

const { isOpen, onClose, initialValues } = useRenameModal();

const [title, setTitle] = useState(initialValues.title);

useEffect(() => {
setTitle(initialValues.title);
}, [initialValues.title]);

const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();

mutate({
id: initialValues.id,
title,
})
.then(() => {
toast.success("Board renamed");
onClose();
})
.catch(() => toast.error("Failed to rename board"));
};

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit board title</DialogTitle>
</DialogHeader>
<DialogDescription>Enter a new title for this board</DialogDescription>
<form onSubmit={onSubmit} className="space-y-4">
<Input
disabled={pending}
required
maxLength={60}
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Board title"
/>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button disabled={pending} type="submit">
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
Loading

0 comments on commit 06be2c7

Please sign in to comment.