feat: add board favourites with animations (#318)
* feat: add board favorites with animations * feat: add board favorites functionality with admin-only access * refactor: make board favorites user-specific via junction table * fix: assertUserInWorkspace * Update pnpm lockfile to match package.json * chore: clean up migrations --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
committed by
GitHub
parent
ec37f5480a
commit
ef5bf87fdf
@@ -4,6 +4,8 @@ import {
|
||||
HiLink,
|
||||
HiOutlineDocumentDuplicate,
|
||||
HiOutlineTrash,
|
||||
HiOutlineStar,
|
||||
HiStar,
|
||||
} from "react-icons/hi2";
|
||||
|
||||
import Dropdown from "~/components/Dropdown";
|
||||
@@ -16,39 +18,86 @@ export default function BoardDropdown({
|
||||
isTemplate,
|
||||
isLoading,
|
||||
boardPublicId,
|
||||
workspacePublicId,
|
||||
isFavorite,
|
||||
boardName,
|
||||
}: {
|
||||
isTemplate: boolean;
|
||||
isLoading: boolean;
|
||||
boardPublicId: string;
|
||||
workspacePublicId: string;
|
||||
isFavorite?: boolean;
|
||||
boardName?: string;
|
||||
}) {
|
||||
const { openModal } = useModal();
|
||||
const { canEditBoard, canDeleteBoard, canCreateBoard } = usePermissions();
|
||||
const { showPopup } = usePopup();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
updateBoard.mutate({
|
||||
boardPublicId,
|
||||
favorite: !isFavorite,
|
||||
});
|
||||
};
|
||||
|
||||
const updateBoard = api.board.update.useMutation({
|
||||
onSuccess: (data, variables) => {
|
||||
void utils.board.all.invalidate();
|
||||
void utils.board.byId.invalidate();
|
||||
|
||||
// Show popup notification
|
||||
if (variables.favorite !== undefined) {
|
||||
showPopup({
|
||||
header: variables.favorite
|
||||
? t`Added to favorites`
|
||||
: t`Removed from favorites`,
|
||||
message: variables.favorite
|
||||
? t`${boardName ?? "Board"} has been added to your favorites.`
|
||||
: t`${boardName ?? "Board"} has been removed from your favorites.`,
|
||||
icon: "success",
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
showPopup({
|
||||
header: t`Unable to update board`,
|
||||
message: t`Please try again later, or contact customer support.`,
|
||||
icon: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const items = [
|
||||
...(isTemplate
|
||||
? []
|
||||
: [
|
||||
...(isTemplate && canCreateBoard
|
||||
? [
|
||||
{
|
||||
label: t`Make template`,
|
||||
action: canCreateBoard ? () => openModal("CREATE_TEMPLATE") : undefined,
|
||||
action: () => openModal("CREATE_TEMPLATE"),
|
||||
icon: (
|
||||
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
|
||||
),
|
||||
disabled: !canCreateBoard,
|
||||
},
|
||||
...(canEditBoard
|
||||
? [
|
||||
{
|
||||
label: t`Edit board URL`,
|
||||
action: () => openModal("UPDATE_BOARD_SLUG"),
|
||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
|
||||
]
|
||||
: []),
|
||||
...(!isTemplate && canEditBoard
|
||||
? [
|
||||
{
|
||||
label: t`Edit board URL`,
|
||||
action: () => openModal("UPDATE_BOARD_SLUG"),
|
||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: isFavorite
|
||||
? t`Remove from favorites`
|
||||
: t`Add to favorites`,
|
||||
action: handleToggleFavorite,
|
||||
icon: isFavorite ? (
|
||||
<HiStar className="h-[16px] w-[16px] text-dark-900" />
|
||||
) : (
|
||||
<HiOutlineStar className="h-[16px] w-[16px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
...(canDeleteBoard
|
||||
? [
|
||||
{
|
||||
@@ -59,6 +108,7 @@ export default function BoardDropdown({
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -493,6 +493,8 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
isLoading={!boardData}
|
||||
boardPublicId={boardId ?? ""}
|
||||
workspacePublicId={workspace.publicId}
|
||||
isFavorite={boardData?.favorite}
|
||||
boardName={boardData?.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -594,13 +596,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
? `/templates/${boardId}/cards/${card.publicId}`
|
||||
: `/cards/${card.publicId}`
|
||||
}
|
||||
className={`mb-2 flex !cursor-pointer flex-col ${
|
||||
card.publicId.startsWith(
|
||||
"PLACEHOLDER",
|
||||
)
|
||||
? "pointer-events-none"
|
||||
: ""
|
||||
}`}
|
||||
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
|
||||
"PLACEHOLDER",
|
||||
)
|
||||
? "pointer-events-none"
|
||||
: ""
|
||||
}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { HiOutlineRectangleStack } from "react-icons/hi2";
|
||||
|
||||
import { HiOutlineRectangleStack, HiOutlineStar, HiStar } from "react-icons/hi2";
|
||||
import { motion } from "framer-motion";
|
||||
import Button from "~/components/Button";
|
||||
import PatternedBackground from "~/components/PatternedBackground";
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
@@ -15,6 +15,13 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
||||
const { openModal } = useModal();
|
||||
const { canCreateBoard } = usePermissions();
|
||||
|
||||
const utils = api.useUtils();
|
||||
const updateBoard = api.board.update.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.board.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const { data, isLoading } = api.board.all.useQuery(
|
||||
{
|
||||
workspacePublicId: workspace.publicId,
|
||||
@@ -23,6 +30,20 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
||||
{ enabled: workspace.publicId ? true : false },
|
||||
);
|
||||
|
||||
const handleToggleFavorite = (
|
||||
e: React.MouseEvent,
|
||||
boardPublicId: string,
|
||||
currentFavorite: boolean | undefined
|
||||
) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateBoard.mutate({
|
||||
boardPublicId,
|
||||
favorite: !currentFavorite,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="3xl:grid-cols-4 grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3">
|
||||
@@ -62,20 +83,51 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="3xl:grid-cols-4 grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3">
|
||||
<motion.div
|
||||
className="3xl:grid-cols-4 grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3"
|
||||
layout
|
||||
>
|
||||
{data?.map((board) => (
|
||||
<Link
|
||||
<motion.div
|
||||
key={board.publicId}
|
||||
href={`${isTemplate ? "templates" : "boards"}/${board.publicId}`}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
layout: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
mass: 1
|
||||
},
|
||||
opacity: { duration: 0.2 },
|
||||
scale: { duration: 0.2 }
|
||||
}}
|
||||
>
|
||||
<div className="align-center relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
|
||||
<PatternedBackground />
|
||||
<p className="px-4 text-[14px] font-bold text-neutral-700 dark:text-dark-1000">
|
||||
{board.name}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={`${isTemplate ? "templates" : "boards"}/${board.publicId}`}
|
||||
>
|
||||
<div className="group relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
|
||||
<PatternedBackground />
|
||||
<button
|
||||
onClick={(e) => handleToggleFavorite(e, board.publicId, board.favorite)}
|
||||
className={`absolute right-3 top-3 z-10 rounded p-1 transition-all hover:bg-light-300 dark:hover:bg-dark-200 ${board.favorite ? "" : "md:opacity-0 md:group-hover:opacity-100"
|
||||
}`}
|
||||
aria-label={board.favorite ? "Remove from favorites" : "Add to favorites"}
|
||||
>
|
||||
{board.favorite ? (
|
||||
<HiStar className="h-5 w-5 text-neutral-700 dark:text-dark-1000" />
|
||||
) : (
|
||||
<HiOutlineStar className="h-5 w-5 text-neutral-700 dark:text-dark-800" />
|
||||
)}
|
||||
</button>
|
||||
<p className="px-4 text-[14px] font-bold text-neutral-700 dark:text-dark-1000">
|
||||
{board.name}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import PatternedBackground from "~/components/PatternedBackground";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
Reference in New Issue
Block a user