Update + Delete articles
This commit is contained in:
@@ -3,18 +3,25 @@ package blogs
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
core "fr.latosa-escrima/core"
|
core "fr.latosa-escrima/core"
|
||||||
"fr.latosa-escrima/core/models"
|
"fr.latosa-escrima/core/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleBlog(w http.ResponseWriter, r *http.Request) {
|
func HandleBlog(w http.ResponseWriter, r *http.Request) {
|
||||||
slug := r.PathValue("slug")
|
identifier := r.PathValue("identifier")
|
||||||
|
query := "slug = ?"
|
||||||
|
if regexp.
|
||||||
|
MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`).
|
||||||
|
MatchString(identifier) {
|
||||||
|
query = "blog_id = ?"
|
||||||
|
}
|
||||||
|
|
||||||
var blog models.Blog
|
var blog models.Blog
|
||||||
_, err := core.DB.NewSelect().
|
_, err := core.DB.NewSelect().
|
||||||
Model(&blog).
|
Model(&blog).
|
||||||
Where("slug = ?", slug).
|
Where(query, identifier).
|
||||||
Relation("Author").
|
Relation("Author").
|
||||||
ScanAndCount(context.Background())
|
ScanAndCount(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,7 +18,17 @@ var BlogsRoutes = map[string]core.Handler{
|
|||||||
Handler: blogs.HandleCategories,
|
Handler: blogs.HandleCategories,
|
||||||
Middlewares: []core.Middleware{Methods("GET")},
|
Middlewares: []core.Middleware{Methods("GET")},
|
||||||
},
|
},
|
||||||
"/blogs/{slug}": {
|
"/blogs/{identifier}": {
|
||||||
Handler: blogs.HandleBlog,
|
Handler: blogs.HandleBlog,
|
||||||
Middlewares: []core.Middleware{Methods("GET")}},
|
Middlewares: []core.Middleware{Methods("GET")}},
|
||||||
|
"/blogs/{blog_uuid}/delete": {
|
||||||
|
Handler: blogs.HandleDelete,
|
||||||
|
Middlewares: []core.Middleware{Methods("DELETE"),
|
||||||
|
HasPermissions("blogs", "delete"), AuthJWT},
|
||||||
|
},
|
||||||
|
"/blogs/{blog_uuid}/update": {
|
||||||
|
Handler: blogs.HandleUpdate,
|
||||||
|
Middlewares: []core.Middleware{Methods("PATCH"),
|
||||||
|
HasPermissions("blogs", "update"), AuthJWT},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/app/(auth)/dashboard/blogs/[uuid]/page.tsx
Normal file
27
frontend/app/(auth)/dashboard/blogs/[uuid]/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useApi } from "@/hooks/use-api";
|
||||||
|
import { Blog } from "@/types/types";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
|
const BlogEditor = dynamic(
|
||||||
|
() => import("@/components/article/edit").then((mod) => mod.default),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <Loader2 className="animate-spin" />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const params = useParams<{ uuid: string }>();
|
||||||
|
const {
|
||||||
|
data: blog,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
success,
|
||||||
|
isLoading,
|
||||||
|
} = useApi<Blog>(`/blogs/${params.uuid}`, {}, false, false);
|
||||||
|
return <BlogEditor blog={blog} />;
|
||||||
|
}
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import EditableText from "@/components/editable-text";
|
|
||||||
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, { useApi } from "@/hooks/use-api";
|
|
||||||
import sluggify from "@/lib/sluggify";
|
|
||||||
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";
|
|
||||||
import {
|
|
||||||
ActionButton,
|
|
||||||
ActionButtonDefault,
|
|
||||||
ActionButtonError,
|
|
||||||
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(() => {
|
|
||||||
const localImage = localStorage.getItem("blog_draft_image");
|
|
||||||
setImageUrl(
|
|
||||||
localImage && localImage.length > 0
|
|
||||||
? localImage
|
|
||||||
: "/placeholder.svg",
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [summary, setSummary] = useState("");
|
|
||||||
const slug = useMemo(() => sluggify(title), [title]);
|
|
||||||
const {
|
|
||||||
trigger: newBlog,
|
|
||||||
isMutating: isSending,
|
|
||||||
isSuccess,
|
|
||||||
error,
|
|
||||||
} = useApiMutation<undefined, NewBlog>(
|
|
||||||
"/blogs/new",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="m-10 flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<EditableText onChange={setTitle}>
|
|
||||||
<h1>
|
|
||||||
{title.length > 0 ? title : "Un titre doit-être fourni"}
|
|
||||||
</h1>
|
|
||||||
</EditableText>
|
|
||||||
<p className="italic">{slug}</p>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger>
|
|
||||||
<img
|
|
||||||
src={imageUrl}
|
|
||||||
alt="Blog cover"
|
|
||||||
className="w-full h-60 object-cover cursor-pointer rounded-lg"
|
|
||||||
/>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogTitle></DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter image URL"
|
|
||||||
value={imageUrl}
|
|
||||||
onChange={(e) => {
|
|
||||||
setImageUrl(e.target.value);
|
|
||||||
localStorage.setItem(
|
|
||||||
"blog_draft_image",
|
|
||||||
e.currentTarget.value,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<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">
|
|
||||||
<LocalEditor content={content} setTitle={setTitle} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
isLoading={isSending}
|
|
||||||
isSuccess={isSuccess}
|
|
||||||
error={error}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
const blogContent = localStorage.getItem("blog_draft");
|
|
||||||
if (!blogContent) return;
|
|
||||||
if (title.length < 1) return;
|
|
||||||
const res = await newBlog({
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
image: imageUrl,
|
|
||||||
slug,
|
|
||||||
content: blogContent,
|
|
||||||
category,
|
|
||||||
});
|
|
||||||
if (!res) {
|
|
||||||
toast({ title: "Aucune réponse du serveur." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (res.status === "Error") {
|
|
||||||
toast({
|
|
||||||
title: "Erreur.",
|
|
||||||
content: "Une erreur est survenue.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (res.data) console.log(res.data);
|
|
||||||
return res;
|
|
||||||
} catch (error: any) {
|
|
||||||
toast({
|
|
||||||
title: "Erreur.",
|
|
||||||
content: "Une erreur est survenue.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionButtonDefault>Publier</ActionButtonDefault>
|
|
||||||
<ActionButtonSuccess />
|
|
||||||
<ActionButtonError />
|
|
||||||
<ActionButtonLoading />
|
|
||||||
</ActionButton>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const BlogEditor = dynamic(() => import("./new").then((mod) => mod.default), {
|
const BlogEditor = dynamic(
|
||||||
|
() => import("@/components/article/edit").then((mod) => mod.default),
|
||||||
|
{
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <Loader2 className="animate-spin" />,
|
loading: () => <Loader2 className="animate-spin" />,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <BlogEditor />;
|
return <BlogEditor />;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import BlogArticle from "@/components/article";
|
import BlogArticle from "@/components/article";
|
||||||
|
import getMe from "@/lib/getMe";
|
||||||
import request from "@/lib/request";
|
import request from "@/lib/request";
|
||||||
import { Blog } from "@/types/types";
|
import { Blog } from "@/types/types";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
@@ -20,5 +21,7 @@ export default async function HistoryDetails({
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BlogArticle blog={blog.data} />;
|
const me = await getMe();
|
||||||
|
|
||||||
|
return <BlogArticle blog={blog.data} user={me?.data} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,26 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { CalendarIcon } from "lucide-react";
|
import { CalendarIcon } from "lucide-react";
|
||||||
import { Blog } from "@/types/types";
|
import { Blog } from "@/types/types";
|
||||||
|
import IUser from "@/interfaces/IUser";
|
||||||
|
import hasPermissions from "@/lib/hasPermissions";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import DeleteArticleButton from "./article/delete-button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const BlogArticle: React.FC<{ blog: Blog; user?: IUser }> = ({
|
||||||
|
blog,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
const UpdateButton = () => {
|
||||||
|
if (!user || !hasPermissions(user.roles, { blogs: ["update"] })) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="secondary">
|
||||||
|
<Link href={`/dashboard/blogs/${blog.blogID}`}>Modifier</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const BlogArticle: React.FC<{ blog: Blog }> = ({ blog }) => {
|
|
||||||
return (
|
return (
|
||||||
<article className="mx-auto max-w-3xl px-4 py-8 md:py-12">
|
<article className="mx-auto max-w-3xl px-4 py-8 md:py-12">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -28,6 +46,7 @@ const BlogArticle: React.FC<{ blog: Blog }> = ({ blog }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
@@ -48,14 +67,17 @@ const BlogArticle: React.FC<{ blog: Blog }> = ({ blog }) => {
|
|||||||
<p className="text-sm text-muted-foreground">idk</p>
|
<p className="text-sm text-muted-foreground">idk</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<UpdateButton />
|
||||||
|
<DeleteArticleButton id={blog.blogID} user={user} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative aspect-video overflow-hidden rounded-lg">
|
<div className="relative aspect-video overflow-hidden rounded-lg">
|
||||||
<Image
|
<img
|
||||||
src={blog.image ?? "/placeholder.svg"}
|
src={blog.image ?? "/placeholder.svg"}
|
||||||
alt={blog.title}
|
alt={blog.title}
|
||||||
className="object-cover"
|
className="object-contain"
|
||||||
fill
|
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
97
frontend/components/article/delete-button.tsx
Normal file
97
frontend/components/article/delete-button.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Loader2, Trash2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import hasPermissions from "@/lib/hasPermissions";
|
||||||
|
import IUser from "@/interfaces/IUser";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import request from "@/lib/request";
|
||||||
|
import { ApiResponse } from "@/types/types";
|
||||||
|
|
||||||
|
interface DeleteArticleButtonProps {
|
||||||
|
id: string;
|
||||||
|
user?: IUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteArticleButton: React.FC<DeleteArticleButtonProps> = ({
|
||||||
|
id,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
if (!user || !hasPermissions(user.roles, { blogs: ["update"] })) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await request(`/blogs/${id}/delete`, {
|
||||||
|
requiresAuth: true,
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
// if (res.status === "Success") {
|
||||||
|
// toast({
|
||||||
|
// title: "Article supprimé",
|
||||||
|
// description: "L'article a été supprimé avec succès.",
|
||||||
|
// });
|
||||||
|
// setOpen(false); // Only close on success
|
||||||
|
// router.replace("/blogs");
|
||||||
|
// } else {
|
||||||
|
// toast({
|
||||||
|
// title: "Erreur",
|
||||||
|
// description: res?.message || "Une erreur est survenue.",
|
||||||
|
// variant: "destructive",
|
||||||
|
// });
|
||||||
|
// // Don't setOpen(false) here - keep dialog open on error
|
||||||
|
// }
|
||||||
|
// } catch (e: unknown) {
|
||||||
|
// toast({
|
||||||
|
// title: "Erreur",
|
||||||
|
// description:
|
||||||
|
// (e as Error)?.message || "Une erreur est survenue.",
|
||||||
|
// variant: "destructive",
|
||||||
|
// });
|
||||||
|
// // Don't setOpen(false) here - keep dialog open on exception
|
||||||
|
// } finally {
|
||||||
|
// setIsDeleting(false); // Just reset the loading state
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Êtes-vous sûr de vouloir supprimer cet article ?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Cette action supprimera définitivement cet article.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||||
|
<Button variant="destructive" onClick={handleDelete}>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteArticleButton;
|
||||||
261
frontend/components/article/edit.tsx
Normal file
261
frontend/components/article/edit.tsx
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
"use client";
|
||||||
|
import EditableText from "@/components/editable-text";
|
||||||
|
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, { useApi } from "@/hooks/use-api";
|
||||||
|
import sluggify from "@/lib/sluggify";
|
||||||
|
import { Blog, Category, NewBlog } from "@/types/types";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
ActionButton,
|
||||||
|
ActionButtonDefault,
|
||||||
|
ActionButtonError,
|
||||||
|
ActionButtonLoading,
|
||||||
|
ActionButtonSuccess,
|
||||||
|
} from "@/components/action-button";
|
||||||
|
import ComboBox from "@/components/ui/combobox";
|
||||||
|
|
||||||
|
export type BlogState = Partial<Blog> & {
|
||||||
|
[K in keyof Required<Blog>]?: Blog[K] | null; // Allows null for resetting or un-setting fields
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BlogEditor({ blog }: { blog?: Blog }) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const setItem = (b: BlogState) =>
|
||||||
|
blog
|
||||||
|
? localStorage.setItem(`draft_${blog.blogID}`, JSON.stringify(b))
|
||||||
|
: localStorage.setItem("draft", JSON.stringify(b));
|
||||||
|
|
||||||
|
const [draft, setDraft] = useState<BlogState>(() => {
|
||||||
|
if (blog) {
|
||||||
|
setItem(blog);
|
||||||
|
return blog;
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = {
|
||||||
|
slug: "",
|
||||||
|
content: "",
|
||||||
|
title: "",
|
||||||
|
category: "",
|
||||||
|
image: "/placeholder.svg",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const _draft = localStorage.getItem("draft");
|
||||||
|
if (_draft) {
|
||||||
|
const draft: BlogState = JSON.parse(_draft);
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange =
|
||||||
|
(field: keyof BlogState) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement> | string) => {
|
||||||
|
const value = typeof e === "string" ? e : e.target.value;
|
||||||
|
setDraft((prev) => {
|
||||||
|
const n: typeof prev = {
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
setItem(n);
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = useApi<Category[]>(
|
||||||
|
"/blogs/categories",
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [categories, setCategories] = useState<string[]>(
|
||||||
|
draft.category ? [draft.category] : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) setCategories(data.map((c) => c.category) ?? []);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const slug = useMemo(() => {
|
||||||
|
const slug = draft.title ? sluggify(draft.title) : "";
|
||||||
|
handleChange("slug")(slug);
|
||||||
|
return slug;
|
||||||
|
}, [draft.title]);
|
||||||
|
|
||||||
|
const newBlog = useApiMutation<undefined, NewBlog>(
|
||||||
|
"/blogs/new",
|
||||||
|
{},
|
||||||
|
"POST",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateBlog = useApiMutation(
|
||||||
|
`/blogs/${blog?.blogID}/update`,
|
||||||
|
{},
|
||||||
|
"PATCH",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNewBlog = async () => {
|
||||||
|
try {
|
||||||
|
if (!draft.content) return;
|
||||||
|
if (!draft.title || draft.title.length < 1) return;
|
||||||
|
const res = await newBlog.trigger({
|
||||||
|
title: draft.title,
|
||||||
|
summary: draft.summary,
|
||||||
|
image: draft.image,
|
||||||
|
slug,
|
||||||
|
content: draft.content,
|
||||||
|
category: draft.category,
|
||||||
|
});
|
||||||
|
if (!res) {
|
||||||
|
toast({ title: "Aucune réponse du serveur." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === "Error") {
|
||||||
|
toast({
|
||||||
|
title: "Erreur.",
|
||||||
|
content: "Une erreur est survenue.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (res.data) console.log(res.data);
|
||||||
|
return res;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur.",
|
||||||
|
content: "Une erreur est survenue.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateBlog = async () => {
|
||||||
|
try {
|
||||||
|
if (!draft.content) return;
|
||||||
|
if (!draft.title || draft.title.length < 1) return;
|
||||||
|
const res = await updateBlog.trigger({
|
||||||
|
title: draft.title,
|
||||||
|
summary: draft.summary,
|
||||||
|
image: draft.image,
|
||||||
|
slug,
|
||||||
|
content: draft.content,
|
||||||
|
category: draft.category,
|
||||||
|
});
|
||||||
|
if (!res) {
|
||||||
|
toast({ title: "Aucune réponse du serveur." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === "Error") {
|
||||||
|
toast({
|
||||||
|
title: "Erreur.",
|
||||||
|
content: "Une erreur est survenue.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (res.data) console.log(res.data);
|
||||||
|
return res;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur.",
|
||||||
|
content: "Une erreur est survenue.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="m-10 flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<EditableText onChange={handleChange("title")}>
|
||||||
|
<h1>
|
||||||
|
{draft.title && draft.title.length > 0
|
||||||
|
? draft.title
|
||||||
|
: "Un titre doit-être fourni"}
|
||||||
|
</h1>
|
||||||
|
</EditableText>
|
||||||
|
<p className="italic">{slug}</p>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger>
|
||||||
|
<img
|
||||||
|
src={draft.image}
|
||||||
|
alt="Blog cover"
|
||||||
|
className="w-full h-60 object-cover cursor-pointer rounded-lg"
|
||||||
|
/>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogTitle></DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter image URL"
|
||||||
|
value={draft.image}
|
||||||
|
onChange={handleChange("image")}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter a summary"
|
||||||
|
value={draft.summary}
|
||||||
|
onChange={handleChange("summary")}
|
||||||
|
/>
|
||||||
|
<ComboBox
|
||||||
|
value={draft.category ?? ""}
|
||||||
|
onValueChange={handleChange("category")}
|
||||||
|
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];
|
||||||
|
});
|
||||||
|
handleChange("category")(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(Item, element) => (
|
||||||
|
<Item value={element} key={element} label={element}>
|
||||||
|
{element}
|
||||||
|
</Item>
|
||||||
|
)}
|
||||||
|
</ComboBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1">
|
||||||
|
<LocalEditor
|
||||||
|
onChange={handleChange("content")}
|
||||||
|
content={draft.content ?? ""}
|
||||||
|
onTitleChange={handleChange("title")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
isLoading={newBlog.isMutating || updateBlog.isMutating}
|
||||||
|
isSuccess={newBlog.isSuccess || updateBlog.isSuccess}
|
||||||
|
error={newBlog.error || updateBlog.error}
|
||||||
|
onClick={blog ? handleUpdateBlog : handleNewBlog}
|
||||||
|
>
|
||||||
|
<ActionButtonDefault>
|
||||||
|
{blog ? "Modifier" : "Publier"}
|
||||||
|
</ActionButtonDefault>
|
||||||
|
<ActionButtonSuccess />
|
||||||
|
<ActionButtonError />
|
||||||
|
<ActionButtonLoading />
|
||||||
|
</ActionButton>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,14 +18,14 @@ interface EditorProps {
|
|||||||
content: string;
|
content: string;
|
||||||
onChange?: (content: string) => void;
|
onChange?: (content: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
setTitle?: React.Dispatch<React.SetStateAction<string>>;
|
onTitleChange?: (t: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LocalEditor({
|
export function LocalEditor({
|
||||||
content,
|
content,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
setTitle,
|
onTitleChange: setTitle,
|
||||||
}: EditorProps) {
|
}: EditorProps) {
|
||||||
const getTitle = (editor: Editor) => {
|
const getTitle = (editor: Editor) => {
|
||||||
const firstNode = editor.state.doc.firstChild;
|
const firstNode = editor.state.doc.firstChild;
|
||||||
@@ -87,7 +87,7 @@ export function LocalEditor({
|
|||||||
setTitle?.(title ?? "");
|
setTitle?.(title ?? "");
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
localStorage.setItem("blog_draft", editor.getHTML());
|
onChange?.(editor.getHTML());
|
||||||
const title = getTitle(editor);
|
const title = getTitle(editor);
|
||||||
setTitle?.(title ?? "");
|
setTitle?.(title ?? "");
|
||||||
},
|
},
|
||||||
|
|||||||
141
frontend/components/ui/alert-dialog.tsx
Normal file
141
frontend/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root;
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
));
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@ interface ComboBoxProps<T> {
|
|||||||
trigger: (value?: string) => React.ReactNode;
|
trigger: (value?: string) => React.ReactNode;
|
||||||
onSubmit?: (value: string) => void;
|
onSubmit?: (value: string) => void;
|
||||||
value: string;
|
value: string;
|
||||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
onValueChange: (v: string) => void;
|
||||||
children: (
|
children: (
|
||||||
ItemComponent: (
|
ItemComponent: (
|
||||||
props: React.ComponentProps<typeof CommandItem> & { label: string },
|
props: React.ComponentProps<typeof CommandItem> & { label: string },
|
||||||
@@ -39,7 +39,7 @@ const ComboBox = <T,>({
|
|||||||
children,
|
children,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
value,
|
value,
|
||||||
setValue,
|
onValueChange,
|
||||||
}: ComboBoxProps<T>) => {
|
}: ComboBoxProps<T>) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
@@ -47,7 +47,7 @@ const ComboBox = <T,>({
|
|||||||
const handleSubmit = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleSubmit = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key !== "Enter") return;
|
if (e.key !== "Enter") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setValue(searchValue);
|
onValueChange(searchValue);
|
||||||
onSubmit?.(searchValue);
|
onSubmit?.(searchValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ const ComboBox = <T,>({
|
|||||||
key={index}
|
key={index}
|
||||||
value={elementValue ?? ""}
|
value={elementValue ?? ""}
|
||||||
onSelect={(_value) => {
|
onSelect={(_value) => {
|
||||||
setValue(_value);
|
onValueChange(_value);
|
||||||
console.log(elementValue);
|
console.log(elementValue);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
onSelect?.(_value);
|
onSelect?.(_value);
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"unstable": ["unsafe-proto"]
|
|
||||||
}
|
|
||||||
3111
frontend/deno.lock
generated
3111
frontend/deno.lock
generated
File diff suppressed because it is too large
Load Diff
175
frontend/package-lock.json
generated
175
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@radix-ui/react-checkbox": "^1.1.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
@@ -873,6 +874,57 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-alert-dialog": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "1.1.6",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||||
@@ -1057,25 +1109,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog": {
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||||
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
|
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.1",
|
"@radix-ui/primitive": "1.1.1",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.1",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||||
"@radix-ui/react-focus-guards": "1.1.1",
|
"@radix-ui/react-focus-guards": "1.1.1",
|
||||||
"@radix-ui/react-focus-scope": "1.1.1",
|
"@radix-ui/react-focus-scope": "1.1.2",
|
||||||
"@radix-ui/react-id": "1.1.0",
|
"@radix-ui/react-id": "1.1.0",
|
||||||
"@radix-ui/react-portal": "1.1.3",
|
"@radix-ui/react-portal": "1.1.4",
|
||||||
"@radix-ui/react-presence": "1.1.2",
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.0.1",
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
"@radix-ui/react-slot": "1.1.1",
|
"@radix-ui/react-slot": "1.1.2",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.2.4",
|
||||||
"react-remove-scroll": "^2.6.1"
|
"react-remove-scroll": "^2.6.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1092,21 +1144,102 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.1"
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7385,16 +7518,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
||||||
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
|
"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-remove-scroll-bar": "^2.3.7",
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
"react-style-singleton": "^2.2.1",
|
"react-style-singleton": "^2.2.3",
|
||||||
"tslib": "^2.1.0",
|
"tslib": "^2.1.0",
|
||||||
"use-callback-ref": "^1.3.3",
|
"use-callback-ref": "^1.3.3",
|
||||||
"use-sidecar": "^1.1.2"
|
"use-sidecar": "^1.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@radix-ui/react-checkbox": "^1.1.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user