Blogs listing + Categories

This commit is contained in:
cdricms
2025-02-25 00:13:53 +01:00
parent ae228710e1
commit 793e3748f9
21 changed files with 695 additions and 192 deletions

View File

@@ -4,9 +4,9 @@ import { LocalEditor } from "@/components/editor";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog";
import useApiMutation from "@/hooks/use-api";
import useApiMutation, { useApi } from "@/hooks/use-api";
import sluggify from "@/lib/sluggify";
import { NewBlog } from "@/types/types";
import { Category, NewBlog } from "@/types/types";
import { useEffect, useMemo, useState } from "react";
import { DialogTitle } from "@radix-ui/react-dialog";
import { useToast } from "@/hooks/use-toast";
@@ -17,12 +17,28 @@ import {
ActionButtonLoading,
ActionButtonSuccess,
} from "@/components/action-button";
import ComboBox from "@/components/ui/combobox";
export default function BlogEditor() {
const { toast } = useToast();
const [title, setTitle] = useState("");
const [imageUrl, setImageUrl] = useState<string>("/placeholder.svg");
const [category, setCategory] = useState("");
const { data } = useApi<Category[]>(
"/blogs/categories",
undefined,
false,
false,
);
const [categories, setCategories] = useState<string[]>([]);
useEffect(() => {
if (data) setCategories(data.map((c) => c.category) ?? []);
}, [data]);
const content = localStorage.getItem("blog_draft") ?? "";
useEffect(() => {
@@ -82,12 +98,38 @@ export default function BlogEditor() {
/>
</DialogContent>
</Dialog>
<Input
type="text"
placeholder="Enter a summary"
value={summary}
onChange={(e) => setSummary(e.target.value)}
/>
<div className="flex gap-4">
<Input
type="text"
placeholder="Enter a summary"
value={summary}
onChange={(e) => setSummary(e.target.value)}
/>
<ComboBox
value={category}
setValue={setCategory}
key={categories.join(",")}
elements={categories}
trigger={(value) => (
<Button>
{value ?? "Selectionner une catégorie"}
</Button>
)}
onSubmit={(value) => {
setCategories((prev) => {
if (prev.includes(value)) return prev;
return [...prev, value];
});
setCategory(value);
}}
>
{(Item, element) => (
<Item value={element} key={element} label={element}>
{element}
</Item>
)}
</ComboBox>
</div>
</div>
<div className="flex">
<div className="flex-1">
@@ -110,6 +152,7 @@ export default function BlogEditor() {
image: imageUrl,
slug,
content: blogContent,
category,
});
if (!res) {
toast({ title: "Aucune réponse du serveur." });

View File

@@ -36,8 +36,8 @@ export default async function About() {
Nicolas GORUK
</CardTitle>
<CardDescription>
Président l'association française de Latosa
Escrima
Président de l'association française de
Latosa Escrima
</CardDescription>
</CardHeader>
<CardContent className="px-8 sm:px-10 py-14">

View File

@@ -0,0 +1,137 @@
"use client";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { Button } from "@/components/ui/button";
import request from "@/lib/request";
import { useEffect, useState } from "react";
import { Category } from "@/types/types";
import { useIsMobile } from "@/hooks/use-mobile";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
} from "@/components/ui/select";
import { SelectValue } from "@radix-ui/react-select";
import { CircleXIcon } from "lucide-react";
export default function Categories({
selectedCategory,
}: {
selectedCategory?: string;
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [categories, setCategories] = useState<Category[]>([]);
const [selected, setSelected] = useState<string | undefined>(
selectedCategory,
);
useEffect(() => {
setSelected(selectedCategory);
}, [selectedCategory]); // Sync local state when URL changes
const [isReady, setIsReady] = useState(false);
const isMobile = useIsMobile();
useEffect(() => {
setIsReady(true); // Ensures component renders only after first render
}, []);
// Fetch categories on mount
useEffect(() => {
const fetchCategories = async () => {
const response = await request<Category[]>("/blogs/categories", {
requiresAuth: false,
});
if (response.status !== "Error" && response.data) {
setCategories(response.data);
}
};
fetchCategories();
}, []);
// Function to update query params
const updateQueryParams = (category: string) => {
const params = new URLSearchParams(searchParams.toString());
if (category === selectedCategory) {
params.delete("category");
} else {
params.set("category", category);
}
router.push(`${pathname}?${params.toString()}`, { scroll: false });
};
if (!isReady) {
return null; // Prevents rendering the wrong section before isMobile is determined
}
if (isMobile) {
return (
<div
className={`p-4 flex flex-row gap-2 ${isReady ? "animate-fadeIn" : ""}`}
>
<Select
key={selected}
onValueChange={(value) => {
setSelected(value);
updateQueryParams(value);
}}
value={selected}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une catégorie" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Catégories</SelectLabel>
{categories.map((c) => (
<SelectItem key={c.category} value={c.category}>
{c.category}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
{selected && (
<Button
onClick={() => {
setSelected(undefined);
updateQueryParams(selected);
}}
>
<CircleXIcon />
</Button>
)}
</div>
);
}
return (
<div
className={`flex gap-2 flex-col p-5 lg:p-8 w-1/5 border-r ${isReady ? "animate-fadeIn" : ""}`}
>
<h2>Catégories</h2>
{categories.map((cat) => (
<Button
className={`w-full flex justify-between ${
selectedCategory === cat.category
? "bg-blue-500 text-white"
: ""
}`}
key={cat.category}
variant="secondary"
onClick={() => updateQueryParams(cat.category)}
>
<span>{cat.category}</span>
<span>{cat.count}</span>
</Button>
))}
</div>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { Button } from "@/components/ui/button"; // You can use your button component from your UI library
import { Card } from "@/components/ui/card";
import { CircleXIcon, RotateCwIcon } from "lucide-react"; // Optional icon
export default function NoBlogs() {
return (
<div className="flex justify-center items-center p-8">
<Card className="w-full md:w-96 p-6 bg-transparent border-transparent shadow-none text-center flex flex-col">
<h3 className="text-xl font-semibold mb-4">
Aucun blog trouvé.
</h3>
<p className="text-gray-500 mb-6">
Nous n'avons pu trouver aucun blog.
</p>
<Button
variant="outline"
className="flex items-center justify-center gap-2"
onClick={() => window.location.reload()}
>
<RotateCwIcon className="h-4 w-4" />
Réessayer
</Button>
</Card>
</div>
);
}

View File

@@ -1,9 +1,34 @@
import Blog from "@/components/blog";
"use server";
import Blogs from "@/components/blog";
import request from "@/lib/request";
import { Blog } from "@/types/types";
import Categories from "./categories";
import NoBlogs from "./no-blogs";
export default async function History({
searchParams,
}: {
searchParams: Promise<{ category?: string }>;
}) {
const { category } = await searchParams;
let url = "/blogs";
if (category) url += `?category=${category}`;
const blogs = await request<Blog[]>(url, {
requiresAuth: false,
});
if (!blogs.data || blogs.data.length < 1) {
return <NoBlogs />;
}
if (blogs?.status === "Error") {
return <p>Un problème est survenue.</p>;
}
export default function History() {
return (
<div className="flex flex-col">
<Blog />
<div className="flex flex-col md:flex-row">
<Categories selectedCategory={category} />
<Blogs blogs={blogs.data} />
</div>
);
}

View File

@@ -77,3 +77,12 @@ body {
@apply text-4xl font-bold;
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}