Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions components/links/link-sheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { ButtonTooltip } from "@/components/ui/tooltip";
import { CustomFieldData } from "./custom-fields-panel";
import DomainSection from "./domain-section";
import { LinkOptions } from "./link-options";
import TagSection from "./tags/tag-section";

export const DEFAULT_LINK_PROPS = (
linkType: LinkType,
Expand Down Expand Up @@ -81,6 +82,7 @@ export const DEFAULT_LINK_PROPS = (
audienceType: groupId ? LinkAudienceType.GROUP : LinkAudienceType.GENERAL,
groupId: groupId,
customFields: [],
tags: [],
enableConversation: false,
enableUpload: false,
isFileRequestOnly: false,
Expand Down Expand Up @@ -119,6 +121,7 @@ export type DEFAULT_LINK_TYPE = {
audienceType: LinkAudienceType;
groupId: string | null;
customFields: CustomFieldData[];
tags: string[];
enableConversation: boolean;
enableUpload: boolean;
isFileRequestOnly: boolean;
Expand All @@ -144,6 +147,7 @@ export default function LinkSheet({
id: string;
groupId?: string;
};

const { domains } = useDomains();

const {
Expand Down Expand Up @@ -503,6 +507,13 @@ export default function LinkSheet({
editLink={!!currentLink}
/>
</div>
<div className="space-y-2">
<TagSection
{...{ data, setData }}
editLink={!!currentLink}
teamId={teamInfo?.currentTeam?.id as string}
/>
</div>

{/* Preset Selector - only show when creating a new link */}
{!currentLink &&
Expand Down Expand Up @@ -656,6 +667,13 @@ export default function LinkSheet({
editLink={!!currentLink}
/>
</div>
<div className="space-y-2">
<TagSection
{...{ data, setData }}
editLink={!!currentLink}
teamId={teamInfo?.currentTeam?.id as string}
/>
</div>

{/* Preset Selector for Group links - only show when creating a new link */}
{!currentLink &&
Expand Down
88 changes: 88 additions & 0 deletions components/links/link-sheet/tags/tag-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { TagIcon } from "lucide-react";

import { TagColorProps } from "@/lib/types";
import { cn } from "@/lib/utils";

export default function TagBadge({
name,
color,
withIcon,
plus,
className,
isSelected,
}: {
name?: string;
color: TagColorProps;
withIcon?: boolean;
plus?: number;
className?: string;
isSelected?: boolean;
}) {
return (
<span
className={cn(
"my-auto block whitespace-nowrap rounded-md border px-2 py-0.5 text-sm",
(withIcon || plus) &&
"flex items-center gap-x-1.5 p-1.5 sm:rounded-md sm:px-2 sm:py-0.5",
COLORS_LIST.find((c) => c.color === color)?.css,
isSelected && "border-2 bg-transparent",
className,
)}
>
{withIcon && (
<TagIcon
className={cn(
"!size-5 shrink-0 p-0.5 dark:text-primary-foreground",
isSelected && `bg-${color}-100 rounded-sm border border-gray-200`,
)}
/>
)}
{name && (
<p {...(withIcon && { className: "hidden sm:inline-block" })}>
{name || ""}
</p>
)}
{!!plus && (
<span className="hidden sm:block">
<span className="pr-1.5 opacity-30 md:pl-1 md:pr-2.5">|</span>+{plus}
</span>
)}
</span>
);
}

export const COLORS_LIST: { color: TagColorProps; css: string }[] = [
{
color: "red",
css: "border-red-300 bg-red-100 text-red-500",
},
{
color: "yellow",
css: "border-yellow-300 bg-yellow-100 text-yellow-500",
},
{
color: "green",
css: "border-emerald-300 bg-emerald-100 text-emerald-500",
},
{
color: "blue",
css: "border-blue-300 bg-blue-100 text-blue-500",
},
{
color: "purple",
css: "border-purple-300 bg-purple-100 text-purple-500",
},
{
color: "slate",
css: "border-stone-300 bg-stone-100 text-stone-500",
},
{
color: "fuchsia",
css: "border-fuchsia-300 bg-fuchsia-100 text-fuchsia-500",
},
];

export function randomBadgeColor() {
const randomIndex = Math.floor(Math.random() * COLORS_LIST.length);
return COLORS_LIST[randomIndex].color;
}
115 changes: 115 additions & 0 deletions components/links/link-sheet/tags/tag-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { useRouter } from "next/router";

import { PropsWithChildren, useMemo, useRef } from "react";

import { LinkWithViews, TagColorProps, TagProps } from "@/lib/types";

import { BadgeTooltip } from "@/components/ui/tooltip";

import TagBadge from "./tag-badge";

function useOrganizedTags(tags: LinkWithViews["tags"]) {
const searchParams = useSearchParams();

const [primaryTag, additionalTags] = useMemo(() => {
const filteredTagIds =
searchParams?.get("tagIds")?.split(",")?.filter(Boolean) ?? [];

const sortedTags =
filteredTagIds.length > 0
? [...tags].sort(
(a, b) =>
filteredTagIds.indexOf(b.id) - filteredTagIds.indexOf(a.id),
)
: tags;

return [sortedTags?.[0], sortedTags.slice(1)];
}, [tags, searchParams]);

return { primaryTag, additionalTags };
}

export function TagColumn({ link }: { link: LinkWithViews }) {
const { tags } = link;

const ref = useRef<HTMLDivElement>(null);

const { primaryTag, additionalTags } = useOrganizedTags(tags);

return (
<div ref={ref} className="flex items-center gap-2 sm:gap-5">
{primaryTag ? (
<TagsTooltip additionalTags={additionalTags}>
<TagButton tag={primaryTag} plus={additionalTags.length} />
</TagsTooltip>
) : (
<p>-</p>
)}
</div>
);
}

function TagsTooltip({
additionalTags,
children,
}: PropsWithChildren<{ additionalTags: TagProps[] }>) {
return !!additionalTags.length ? (
<BadgeTooltip
align="end"
content={
<div className="flex flex-wrap gap-1.5 rounded-md p-1">
{additionalTags.map((tag) => (
<TagButton key={tag.id} tag={tag} />
))}
</div>
}
>
<div>{children}</div>
</BadgeTooltip>
) : (
children
);
}

function TagButton({ tag, plus }: { tag: TagProps; plus?: number }) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const selectedTagIds =
searchParams?.get("tagIds")?.split(",")?.filter(Boolean) ?? [];

const handleClick = () => {
const newTagIds = selectedTagIds.includes(tag.id)
? selectedTagIds.filter((id) => id !== tag.id)
: [...selectedTagIds, tag.id];

const params = new URLSearchParams(searchParams?.toString());

if (newTagIds.length) {
params.set("tagIds", newTagIds.join(","));
} else {
params.delete("tagIds");
}

const paramString = params.toString();
router.push(
paramString ? `${pathname}?${paramString}` : `${pathname}`,
undefined,
{ shallow: true },
);
};
return (
<button onClick={handleClick}>
<TagBadge
{...tag}
color={tag.color as TagColorProps}
withIcon
plus={plus}
isSelected={selectedTagIds.includes(tag.id)}
/>
</button>
);
}
Loading