feat: deal tags

This commit is contained in:
2025-10-19 12:12:28 +04:00
parent 9023b07c65
commit 3a1d8e23e3
25 changed files with 978 additions and 166 deletions

View File

@ -0,0 +1,34 @@
import { lighten, Pill, useMantineColorScheme } from "@mantine/core";
import { DealTagSchema } from "@/lib/client";
type Props = {
tag: Partial<DealTagSchema>;
};
const DealTag = ({ tag }: Props) => {
const theme = useMantineColorScheme();
const isInherit = tag.tagColor!.backgroundColor === "inherit";
let color = tag.tagColor!.color;
const backgroundColor = tag.tagColor!.backgroundColor;
if (!(theme.colorScheme === "dark" || isInherit)) {
color = lighten(color, 0.95);
}
return (
<Pill
key={tag.id}
style={{
opacity: 0.7,
color,
backgroundColor,
border: "1px solid",
borderColor: color,
}}>
{tag.name}
</Pill>
);
};
export default DealTag;

View File

@ -0,0 +1,30 @@
.add-tag-button {
@mixin light {
background-color: var(--mantine-color-gray-1);
}
@mixin dark {
background-color: var(--mantine-color-dark-6);
}
color: gray;
border: 1px gray dashed;
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
padding: 0;
cursor: pointer;
}
.add-tag-button:hover {
@mixin light {
border-color: black;
color: black;
}
@mixin dark {
border-color: white;
color: white;
}
}
.add-tag-button-icon {
color: inherit !important;
}

View File

@ -0,0 +1,93 @@
import React, { useMemo } from "react";
import { IconPlus } from "@tabler/icons-react";
import classNames from "classnames";
import { Button, Center, Checkbox, Group, Menu, Stack } from "@mantine/core";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import DealTag from "@/components/ui/DealTag/DealTag";
import useDealTags from "@/components/ui/DealTags/hooks/useDealTags";
import { DealTagSchema } from "@/lib/client";
import styles from "./DealTags.module.css";
type Props = {
dealId?: number;
groupId?: number;
tags: DealTagSchema[];
};
const DealTags = ({ tags, dealId, groupId }: Props) => {
const { selectedProject } = useProjectsContext();
const { switchTag } = useDealTags();
const tagIdsSet = useMemo(() => new Set(tags.map(t => t.id)), [tags]);
if (selectedProject?.tags.length === 0) return;
const onTagClick = (tagId: number, event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
switchTag({ dealId, groupId, tagId });
};
const addTagButton = useMemo(
() => (
<Menu withArrow>
<Menu.Target>
<Button
onClick={e => e.stopPropagation()}
unstyled
className={classNames(styles["add-tag-button"])}>
<Center>
<IconPlus
size={"1.2em"}
className={classNames(
styles["add-tag-button-icon"]
)}
/>
</Center>
</Button>
</Menu.Target>
<Menu.Dropdown>
<Stack
p={"xs"}
gap={"sm"}
onClick={e => e.stopPropagation()}>
{selectedProject?.tags.map(tag => (
<Group
key={tag.id}
wrap={"nowrap"}>
<Checkbox
checked={tagIdsSet.has(tag.id)}
onChange={event =>
onTagClick(
tag.id,
event as unknown as any
)
}
label={tag.name}
/>
</Group>
))}
</Stack>
</Menu.Dropdown>
</Menu>
),
[selectedProject?.tags, tags]
);
return (
<Group gap={"xs"}>
{addTagButton}
{selectedProject?.tags.map(
tag =>
tagIdsSet.has(tag.id) && (
<DealTag
key={tag.id}
tag={tag}
/>
)
)}
</Group>
);
};
export default DealTags;

View File

@ -0,0 +1,25 @@
import { useMutation } from "@tanstack/react-query";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { SwitchDealTagRequest } from "@/lib/client";
import { switchDealTagMutation } from "@/lib/client/@tanstack/react-query.gen";
const useDealTags = () => {
const { refetchDeals } = useDealsContext();
const switchTagMutation = useMutation({
...switchDealTagMutation(),
onSettled: refetchDeals,
});
const switchTag = (data: SwitchDealTagRequest) => {
switchTagMutation.mutate({
body: data,
});
};
return {
switchTag,
};
};
export default useDealTags;