refactor(web): wrap Licenses in Badge

This commit is contained in:
Collin Barrett 2025-06-27 19:30:57 -05:00
parent 4de9282310
commit cd4aa3b59c
12 changed files with 119 additions and 72 deletions

View file

@ -0,0 +1,62 @@
"use client";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface BadgeItem {
key: number;
value: string;
tooltip?: string;
href?: string;
}
interface BadgeCloudProps {
items: readonly BadgeItem[];
}
const BadgeElement = ({ item }: { item: BadgeItem }) => {
const badge = <Badge variant="secondary">{item.value}</Badge>;
return item.href ? (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
aria-label={item.tooltip ?? `View ${item.value}'s homepage`}
>
{badge}
</a>
) : (
badge
);
};
export function BadgeCloud({ items }: BadgeCloudProps) {
const hasTooltips = items.some((item) => item.tooltip || item.href);
const badges = items.map((item) =>
item.tooltip || item.href ? (
<Tooltip key={item.key}>
<TooltipTrigger>
<BadgeElement item={item} />
</TooltipTrigger>
<TooltipContent>
<p>{item.tooltip ?? `View ${item.value}'s homepage`}</p>
</TooltipContent>
</Tooltip>
) : (
<BadgeElement key={item.key} item={item} />
),
);
return (
<div className="flex flex-wrap gap-1">
{hasTooltips ? <TooltipProvider>{badges}</TooltipProvider> : badges}
</div>
);
}

View file

@ -1,9 +1,9 @@
"use client";
import { FilterList } from "@/services/get-filterlists";
import { createColumnHelper } from "@tanstack/react-table";
import { FilterListsTableMeta } from ".";
import { LanguageBadges } from "./language-badges";
import { BadgeCloud } from "@/components/badge-cloud";
import { FilterList } from "@/services/get-filterlists";
const columnHelper = createColumnHelper<FilterList>();
@ -19,11 +19,19 @@ export const columns = [
cell: (info) => {
const meta = info.table.options.meta as FilterListsTableMeta;
const languages =
info
.getValue()
?.flatMap((id) => meta.languages.find((l) => l.id === id) || []) ??
[];
return <LanguageBadges languages={languages} />;
info.getValue()?.flatMap((id) => {
const language = meta.languages.find((l) => l.id === id);
return language
? [
{
key: language.id,
value: language.iso6391,
tooltip: language.name,
},
]
: [];
}) ?? [];
return <BadgeCloud items={languages} />;
},
}),
columnHelper.accessor((row) => row.licenseId, {
@ -31,7 +39,17 @@ export const columns = [
cell: (info) => {
const meta = info.table.options.meta as FilterListsTableMeta;
const license = meta.licenses.find((l) => l.id === info.getValue());
return license?.name;
return license ? (
<BadgeCloud
items={[
{
key: license.id,
value: license.name,
href: license.url ?? undefined,
},
]}
/>
) : null;
},
}),
];

View file

@ -28,19 +28,19 @@ import { Syntax } from "@/services/get-syntaxes";
import { Tag } from "@/services/get-tags";
export interface FilterListsTableMeta extends TableMeta<FilterList> {
languages: Language[];
licenses: License[];
languages: readonly Language[];
licenses: readonly License[];
}
interface FilterListTableProps {
columns: typeof columns;
initialFilterLists: FilterList[];
languages: Language[];
licenses: License[];
maintainers: Maintainer[];
software: Software[];
syntaxes: Syntax[];
tags: Tag[];
languages: readonly Language[];
licenses: readonly License[];
maintainers: readonly Maintainer[];
software: readonly Software[];
syntaxes: readonly Syntax[];
tags: readonly Tag[];
}
export function FilterListTable({

View file

@ -1,33 +0,0 @@
"use client";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Language } from "@/services/get-languages";
interface LanguageBadgesProps {
languages: Language[];
}
export function LanguageBadges({ languages }: LanguageBadgesProps) {
return (
<div className="flex flex-wrap gap-1">
<TooltipProvider>
{languages.map((language) => (
<Tooltip key={language.id}>
<TooltipTrigger>
<Badge variant="secondary">{language.iso6391}</Badge>
</TooltipTrigger>
<TooltipContent>
<p>{language.name}</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
);
}

View file

@ -11,10 +11,10 @@ export type FilterListDetails = {
name: string;
description: string | null;
licenseId: number;
syntaxIds: number[] | null;
languageIds: number[] | null;
tagIds: number[] | null;
viewUrls: ViewUrl[] | null;
syntaxIds: readonly number[] | null;
languageIds: readonly number[] | null;
tagIds: readonly number[] | null;
viewUrls: readonly ViewUrl[] | null;
homeUrl: string | null;
onionUrl: string | null;
policyUrl: string | null;
@ -24,13 +24,13 @@ export type FilterListDetails = {
chatUrl: string | null;
emailAddress: string | null;
donateUrl: string | null;
maintainerIds: number[] | null;
upstreamFilterListIds: number[] | null;
forkFilterListIds: number[] | null;
includedInFilterListIds: number[] | null;
includesFilterListIds: number[] | null;
dependencyFilterListIds: number[] | null;
dependentFilterListIds: number[] | null;
maintainerIds: readonly number[] | null;
upstreamFilterListIds: readonly number[] | null;
forkFilterListIds: readonly number[] | null;
includedInFilterListIds: readonly number[] | null;
includesFilterListIds: readonly number[] | null;
dependencyFilterListIds: readonly number[] | null;
dependentFilterListIds: readonly number[] | null;
};
export async function getFilterListDetails(

View file

@ -5,11 +5,11 @@ export type FilterList = {
name: string;
description: string | null;
licenseId: number;
syntaxIds: number[];
languageIds: number[];
tagIds: number[];
syntaxIds: readonly number[];
languageIds: readonly number[];
tagIds: readonly number[];
primaryViewUrl: string | null;
maintainerIds: number[];
maintainerIds: readonly number[];
};
export async function getFilterLists(): Promise<FilterList[]> {

View file

@ -4,7 +4,7 @@ export type Language = {
id: number;
iso6391: string;
name: string;
filterListIds: number[]; // TODO: are these needed, or can we remove them from the API?
filterListIds: readonly number[]; // TODO: are these needed, or can we remove them from the API?
};
export async function getLanguages(): Promise<Language[]> {

View file

@ -2,12 +2,12 @@ import { API_BASE_URL, DEFAULT_REVALIDATE_SECS } from "./constants";
export type License = {
id: number;
name: string | null;
name: string;
url: string | null;
permitsModification: boolean;
permitsDistribution: boolean;
permitsCommercialUse: boolean;
filterListIds: number[] | null;
filterListIds: readonly number[] | null; // TODO: are these needed, or can we remove them from the API?
};
export async function getLicenses(): Promise<License[]> {

View file

@ -6,7 +6,7 @@ export type Maintainer = {
url: string | null;
emailAddress: string | null;
twitterHandle: string | null;
filterListIds: number[] | null;
filterListIds: readonly number[] | null;
};
export async function getMaintainers(): Promise<Maintainer[]> {

View file

@ -7,7 +7,7 @@ export type Software = {
homeUrl: string | null;
downloadUrl: string | null;
supportsAbpUrlScheme: boolean;
syntaxIds: number[] | null;
syntaxIds: readonly number[] | null;
};
export async function getSoftware(): Promise<Software[]> {

View file

@ -5,8 +5,8 @@ export type Syntax = {
name: string | null;
description: string | null;
url: string | null;
filterListIds: number[] | null;
softwareIds: number[] | null;
filterListIds: readonly number[] | null;
softwareIds: readonly number[] | null;
};
export async function getSyntaxes(): Promise<Syntax[]> {

View file

@ -4,7 +4,7 @@ export type Tag = {
id: number;
name: string | null;
description: string | null;
filterListIds: number[] | null;
filterListIds: readonly number[] | null;
};
export async function getTags(): Promise<Tag[]> {