Update + Delete articles
This commit is contained in:
@@ -3,18 +3,25 @@ package blogs
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
core "fr.latosa-escrima/core"
|
||||
"fr.latosa-escrima/core/models"
|
||||
)
|
||||
|
||||
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
|
||||
_, err := core.DB.NewSelect().
|
||||
Model(&blog).
|
||||
Where("slug = ?", slug).
|
||||
Where(query, identifier).
|
||||
Relation("Author").
|
||||
ScanAndCount(context.Background())
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,17 @@ var BlogsRoutes = map[string]core.Handler{
|
||||
Handler: blogs.HandleCategories,
|
||||
Middlewares: []core.Middleware{Methods("GET")},
|
||||
},
|
||||
"/blogs/{slug}": {
|
||||
"/blogs/{identifier}": {
|
||||
Handler: blogs.HandleBlog,
|
||||
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 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,
|
||||
loading: () => <Loader2 className="animate-spin" />,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return <BlogEditor />;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import BlogArticle from "@/components/article";
|
||||
import getMe from "@/lib/getMe";
|
||||
import request from "@/lib/request";
|
||||
import { Blog } from "@/types/types";
|
||||
import { notFound } from "next/navigation";
|
||||
@@ -20,5 +21,7 @@ export default async function HistoryDetails({
|
||||
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 { CalendarIcon } from "lucide-react";
|
||||
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 (
|
||||
<article className="mx-auto max-w-3xl px-4 py-8 md:py-12">
|
||||
<div className="space-y-6">
|
||||
@@ -28,6 +46,7 @@ const BlogArticle: React.FC<{ blog: Blog }> = ({ blog }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
@@ -48,14 +67,17 @@ const BlogArticle: React.FC<{ blog: Blog }> = ({ blog }) => {
|
||||
<p className="text-sm text-muted-foreground">idk</p>
|
||||
</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">
|
||||
<Image
|
||||
<img
|
||||
src={blog.image ?? "/placeholder.svg"}
|
||||
alt={blog.title}
|
||||
className="object-cover"
|
||||
fill
|
||||
priority
|
||||
className="object-contain"
|
||||
/>
|
||||
</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;
|
||||
onChange?: (content: string) => void;
|
||||
className?: string;
|
||||
setTitle?: React.Dispatch<React.SetStateAction<string>>;
|
||||
onTitleChange?: (t: string) => void;
|
||||
}
|
||||
|
||||
export function LocalEditor({
|
||||
content,
|
||||
onChange,
|
||||
className,
|
||||
setTitle,
|
||||
onTitleChange: setTitle,
|
||||
}: EditorProps) {
|
||||
const getTitle = (editor: Editor) => {
|
||||
const firstNode = editor.state.doc.firstChild;
|
||||
@@ -87,7 +87,7 @@ export function LocalEditor({
|
||||
setTitle?.(title ?? "");
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
localStorage.setItem("blog_draft", editor.getHTML());
|
||||
onChange?.(editor.getHTML());
|
||||
const title = getTitle(editor);
|
||||
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;
|
||||
onSubmit?: (value: string) => void;
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
onValueChange: (v: string) => void;
|
||||
children: (
|
||||
ItemComponent: (
|
||||
props: React.ComponentProps<typeof CommandItem> & { label: string },
|
||||
@@ -39,7 +39,7 @@ const ComboBox = <T,>({
|
||||
children,
|
||||
onSubmit,
|
||||
value,
|
||||
setValue,
|
||||
onValueChange,
|
||||
}: ComboBoxProps<T>) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
@@ -47,7 +47,7 @@ const ComboBox = <T,>({
|
||||
const handleSubmit = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== "Enter") return;
|
||||
e.preventDefault();
|
||||
setValue(searchValue);
|
||||
onValueChange(searchValue);
|
||||
onSubmit?.(searchValue);
|
||||
};
|
||||
|
||||
@@ -82,7 +82,7 @@ const ComboBox = <T,>({
|
||||
key={index}
|
||||
value={elementValue ?? ""}
|
||||
onSelect={(_value) => {
|
||||
setValue(_value);
|
||||
onValueChange(_value);
|
||||
console.log(elementValue);
|
||||
setOpen(false);
|
||||
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": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@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-checkbox": "^1.1.3",
|
||||
"@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": {
|
||||
"version": "1.1.1",
|
||||
"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": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
|
||||
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"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-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-scope": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@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-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.6.1"
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -1092,21 +1144,102 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"@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": {
|
||||
"@types/react": {
|
||||
"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"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
|
||||
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
||||
"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.2"
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@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-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
|
||||
Reference in New Issue
Block a user