feat(web): add Languages column

This commit is contained in:
Collin Barrett 2025-06-27 17:38:51 -05:00
parent 2185b38717
commit 4de9282310
14 changed files with 161 additions and 12 deletions

BIN
web/package-lock.json generated

Binary file not shown.

View file

@ -12,6 +12,7 @@
"@azure/monitor-opentelemetry": "^1.11.1",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View file

@ -3,6 +3,7 @@
import { FilterList } from "@/services/get-filterlists";
import { createColumnHelper } from "@tanstack/react-table";
import { FilterListsTableMeta } from ".";
import { LanguageBadges } from "./language-badges";
const columnHelper = createColumnHelper<FilterList>();
@ -13,6 +14,18 @@ export const columns = [
columnHelper.accessor((row) => row.description, {
header: "Description",
}),
columnHelper.accessor((row) => row.languageIds, {
header: "Languages",
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} />;
},
}),
columnHelper.accessor((row) => row.licenseId, {
header: "License",
cell: (info) => {

View file

@ -1,6 +1,5 @@
"use client";
import * as React from "react";
import {
useReactTable,
getCoreRowModel,
@ -29,6 +28,7 @@ import { Syntax } from "@/services/get-syntaxes";
import { Tag } from "@/services/get-tags";
export interface FilterListsTableMeta extends TableMeta<FilterList> {
languages: Language[];
licenses: License[];
}
@ -46,6 +46,7 @@ interface FilterListTableProps {
export function FilterListTable({
columns,
initialFilterLists,
languages,
licenses,
}: FilterListTableProps) {
const [data, setData] = useState<FilterList[]>(initialFilterLists);
@ -67,7 +68,10 @@ export function FilterListTable({
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
meta: { licenses: licenses } as FilterListsTableMeta,
meta: {
languages,
licenses,
},
});
return (

View file

@ -0,0 +1,33 @@
"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

@ -1,4 +1,3 @@
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Table } from "@tanstack/react-table";
import { FilterList } from "@/services/get-filterlists";

View file

@ -1,6 +1,5 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({

View file

@ -1,6 +1,5 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";

View file

@ -0,0 +1,45 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View file

@ -1,4 +1,3 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

View file

@ -1,6 +1,5 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";

View file

@ -1,7 +1,5 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {

View file

@ -0,0 +1,60 @@
"use client";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -2,9 +2,9 @@ import { API_BASE_URL, DEFAULT_REVALIDATE_SECS } from "./constants";
export type Language = {
id: number;
iso6391: string | null;
name: string | null;
filterListIds: number[] | null;
iso6391: string;
name: string;
filterListIds: number[]; // TODO: are these needed, or can we remove them from the API?
};
export async function getLanguages(): Promise<Language[]> {