UI & Feature update - Alpha #9
@@ -8,7 +8,8 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { CategoryService } from "@/services/category.service";
|
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() {
|
export function SearchSidebar() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -16,10 +17,14 @@ export function SearchSidebar() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||||
|
const [popularTags, setPopularTags] = React.useState<Tag[]>([]);
|
||||||
const [query, setQuery] = React.useState(searchParams.get("query") || "");
|
const [query, setQuery] = React.useState(searchParams.get("query") || "");
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
CategoryService.getAll().then(setCategories).catch(console.error);
|
CategoryService.getAll().then(setCategories).catch(console.error);
|
||||||
|
TagService.getAll({ limit: 10, sort: "popular" })
|
||||||
|
.then(setPopularTags)
|
||||||
|
.catch(console.error);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateSearch = React.useCallback(
|
const updateSearch = React.useCallback(
|
||||||
@@ -116,19 +121,23 @@ export function SearchSidebar() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
|
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(
|
{popularTags.map((tag) => (
|
||||||
(tag) => (
|
<Badge
|
||||||
<Badge
|
key={tag.id}
|
||||||
key={tag}
|
variant={searchParams.get("tag") === tag.name ? "default" : "outline"}
|
||||||
variant={searchParams.get("tag") === tag ? "default" : "outline"}
|
className="cursor-pointer hover:bg-secondary"
|
||||||
className="cursor-pointer hover:bg-secondary"
|
onClick={() =>
|
||||||
onClick={() =>
|
updateSearch(
|
||||||
updateSearch("tag", searchParams.get("tag") === tag ? null : tag)
|
"tag",
|
||||||
}
|
searchParams.get("tag") === tag.name ? null : tag.name,
|
||||||
>
|
)
|
||||||
#{tag}
|
}
|
||||||
</Badge>
|
>
|
||||||
),
|
#{tag.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{popularTags.length === 0 && (
|
||||||
|
<p className="text-xs text-muted-foreground">Aucun tag trouvé.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="breadcrumb-page"
|
data-slot="breadcrumb-page"
|
||||||
role="link"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
className={cn("text-foreground font-normal", className)}
|
className={cn("text-foreground font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function ButtonGroup({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: standard pattern for button groups
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="button-group"
|
data-slot="button-group"
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ function Carousel({
|
|||||||
canScrollNext,
|
canScrollNext,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* biome-ignore lint/a11y/useSemanticElements: standard pattern for carousels */}
|
||||||
<div
|
<div
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
@@ -156,6 +157,7 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
const { orientation } = useCarousel();
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: standard pattern for carousel items
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ function Field({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: standard pattern for field components
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="field"
|
data-slot="field"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: standard pattern for input groups
|
||||||
<div
|
<div
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
@@ -62,6 +63,7 @@ function InputGroupAddon({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: standard pattern for input groups
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="input-group-addon"
|
data-slot="input-group-addon"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function InputOTPSlot({
|
|||||||
|
|
||||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
<div data-slot="input-otp-separator" aria-hidden="true" {...props}>
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: standard pattern for item groups
|
||||||
<div
|
<div
|
||||||
role="list"
|
role="list"
|
||||||
data-slot="item-group"
|
data-slot="item-group"
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ function SidebarProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
// 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}`;
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
},
|
},
|
||||||
[setOpenProp, open],
|
[setOpenProp, open],
|
||||||
|
|||||||
23
frontend/src/components/view-counter.tsx
Normal file
23
frontend/src/components/view-counter.tsx
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user