feat(ui): add ViewCounter component and enhance accessibility annotations

Introduce a new `ViewCounter` component to manage view tracking for content. Add biome-ignore comments for accessibility standards in several UI components, enhance semantic element usage, and improve tag handling in `SearchSidebar`.
This commit is contained in:
Mathis HERRIOT
2026-01-14 22:18:50 +01:00
parent 0a1391674f
commit 0611ef715c
10 changed files with 55 additions and 17 deletions

View File

@@ -8,7 +8,8 @@ import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content";
import { TagService } from "@/services/tag.service";
import type { Category, Tag } from "@/types/content";
export function SearchSidebar() {
const router = useRouter();
@@ -16,10 +17,14 @@ export function SearchSidebar() {
const pathname = usePathname();
const [categories, setCategories] = React.useState<Category[]>([]);
const [popularTags, setPopularTags] = React.useState<Tag[]>([]);
const [query, setQuery] = React.useState(searchParams.get("query") || "");
React.useEffect(() => {
CategoryService.getAll().then(setCategories).catch(console.error);
TagService.getAll({ limit: 10, sort: "popular" })
.then(setPopularTags)
.catch(console.error);
}, []);
const updateSearch = React.useCallback(
@@ -116,19 +121,23 @@ export function SearchSidebar() {
<div>
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
<div className="flex flex-wrap gap-2">
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(
(tag) => (
<Badge
key={tag}
variant={searchParams.get("tag") === tag ? "default" : "outline"}
className="cursor-pointer hover:bg-secondary"
onClick={() =>
updateSearch("tag", searchParams.get("tag") === tag ? null : tag)
}
>
#{tag}
</Badge>
),
{popularTags.map((tag) => (
<Badge
key={tag.id}
variant={searchParams.get("tag") === tag.name ? "default" : "outline"}
className="cursor-pointer hover:bg-secondary"
onClick={() =>
updateSearch(
"tag",
searchParams.get("tag") === tag.name ? null : tag.name,
)
}
>
#{tag.name}
</Badge>
))}
{popularTags.length === 0 && (
<p className="text-xs text-muted-foreground">Aucun tag trouvé.</p>
)}
</div>
</div>

View File

@@ -53,8 +53,6 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}

View File

@@ -26,6 +26,7 @@ function ButtonGroup({
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
// biome-ignore lint/a11y/useSemanticElements: standard pattern for button groups
<div
role="group"
data-slot="button-group"

View File

@@ -117,6 +117,7 @@ function Carousel({
canScrollNext,
}}
>
{/* biome-ignore lint/a11y/useSemanticElements: standard pattern for carousels */}
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
@@ -156,6 +157,7 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
// biome-ignore lint/a11y/useSemanticElements: standard pattern for carousel items
<div
role="group"
aria-roledescription="slide"

View File

@@ -83,6 +83,7 @@ function Field({
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
// biome-ignore lint/a11y/useSemanticElements: standard pattern for field components
<div
role="group"
data-slot="field"

View File

@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
// biome-ignore lint/a11y/useSemanticElements: standard pattern for input groups
<div
data-slot="input-group"
role="group"
@@ -62,6 +63,7 @@ function InputGroupAddon({
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
// biome-ignore lint/a11y/useSemanticElements: standard pattern for input groups
<div
role="group"
data-slot="input-group-addon"

View File

@@ -68,7 +68,7 @@ function InputOTPSlot({
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<div data-slot="input-otp-separator" aria-hidden="true" {...props}>
<MinusIcon />
</div>
);

View File

@@ -6,6 +6,7 @@ import { cn } from "@/lib/utils";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
// biome-ignore lint/a11y/useSemanticElements: standard pattern for item groups
<div
role="list"
data-slot="item-group"

View File

@@ -82,6 +82,7 @@ function SidebarProvider({
}
// This sets the cookie to keep the sidebar state.
// biome-ignore lint/suspicious/noDocumentCookie: persistence of sidebar state
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],

View File

@@ -0,0 +1,23 @@
"use client";
import { useEffect, useRef } from "react";
import { ContentService } from "@/services/content.service";
interface ViewCounterProps {
contentId: string;
}
export function ViewCounter({ contentId }: ViewCounterProps) {
const hasIncremented = useRef(false);
useEffect(() => {
if (!hasIncremented.current) {
ContentService.incrementViews(contentId).catch((err) => {
console.error("Failed to increment views:", err);
});
hasIncremented.current = true;
}
}, [contentId]);
return null;
}