-
+
{
>
{
);
};
+
+function EditorPropLabel({
+ children,
+ tooltip,
+}: {
+ children: React.ReactNode;
+ tooltip: string;
+}) {
+ return (
+
+
+ {children}
+
+
+
+
+
+
+ {tooltip}
+
+
+
+
+
+ );
+}
+function EditorPropValue({
+ children,
+ onClick,
+ className,
+}: {
+ children: React.ReactNode;
+ onClick?: () => void;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/components/Editor/editor-category-picker.tsx b/apps/web/src/components/Editor/editor-category-picker.tsx
new file mode 100644
index 0000000..7345e24
--- /dev/null
+++ b/apps/web/src/components/Editor/editor-category-picker.tsx
@@ -0,0 +1,93 @@
+import { Category } from "@zenblog/types";
+import Spinner from "../Spinner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "../ui/select";
+import { Button } from "../ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+} from "../ui/dropdown-menu";
+import { Plus, X } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { CreateCategoryDialog } from "@/pages/blogs/[blogId]/categories";
+import { useState } from "react";
+
+export function EditorCategoryPicker({
+ isLoading,
+ categories,
+ onChange,
+ value,
+}: {
+ isLoading: boolean;
+ categories: {
+ id: number;
+ name: string;
+ slug: string;
+ }[];
+ onChange: (value: number | null) => void;
+ value: number | null;
+}) {
+ const [open, setOpen] = useState(false);
+ const hasCategory = value !== null;
+ const selectedCategory = categories.find((c) => c.id === value);
+ return (
+ <>
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+ {hasCategory ? selectedCategory?.name : "Select a category"}
+
+ {hasCategory && (
+ onChange(null)}
+ >
+
+
+ )}
+
+
+
+
+ Categories
+ setOpen(true)}
+ >
+
+
+
+ {categories?.map((category) => (
+ onChange(category.id)}
+ >
+ {category.name}
+
+ ))}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/components/ui/calendar.tsx b/apps/web/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2ea47cc
--- /dev/null
+++ b/apps/web/src/components/ui/calendar.tsx
@@ -0,0 +1,65 @@
+import * as React from "react";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { DayPicker } from "react-day-picker";
+
+import { cn } from "../../lib/utils";
+import { buttonVariants } from "./button";
+
+export type CalendarProps = React.ComponentProps
;
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ );
+}
+Calendar.displayName = "Calendar";
+
+export { Calendar };
diff --git a/apps/web/src/components/ui/collapsible.tsx b/apps/web/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000..a23e7a2
--- /dev/null
+++ b/apps/web/src/components/ui/collapsible.tsx
@@ -0,0 +1,9 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
index 164298f..a400078 100644
--- a/apps/web/src/components/ui/dropdown-menu.tsx
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1.5 max-h-[300px] min-w-[8rem] overflow-hidden overflow-y-auto rounded-xl border bg-white p-1.5 shadow-sm",
+ "bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1.5 max-h-[320px] min-w-[8rem] overflow-hidden overflow-y-auto rounded-xl border bg-white p-1.5 shadow-sm",
className
)}
{...props}
diff --git a/apps/web/src/components/ui/editor-date-input.tsx b/apps/web/src/components/ui/editor-date-input.tsx
new file mode 100644
index 0000000..7b1cd4f
--- /dev/null
+++ b/apps/web/src/components/ui/editor-date-input.tsx
@@ -0,0 +1,128 @@
+import { Input } from "./input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./select";
+
+type Value = {
+ day: number;
+ month: number;
+ year: number;
+ hour: number;
+ minute: number;
+};
+
+export function EditorDateInput({
+ value,
+ onChange,
+}: {
+ value: Value;
+ onChange: (value: Value) => void;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/pages/blogs/[blogId]/categories.tsx b/apps/web/src/pages/blogs/[blogId]/categories.tsx
index 8f6a51c..16fcb73 100644
--- a/apps/web/src/pages/blogs/[blogId]/categories.tsx
+++ b/apps/web/src/pages/blogs/[blogId]/categories.tsx
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/table";
import { useBlogId } from "@/hooks/use-blog-id";
import AppLayout, { Section } from "@/layouts/AppLayout";
+import { generateSlug } from "@/lib/utils/slugs";
import {
useCategoriesWithPostCount,
useCreateCategory,
@@ -32,22 +33,28 @@ import {
useUpdateCategoryMutation,
} from "@/queries/categories";
import { MoreHorizontal, Plus } from "lucide-react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { toast } from "sonner";
-export function CreateCategoryDialog() {
+export function CreateCategoryDialog({
+ open,
+ setOpen,
+}: {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}) {
const createCategory = useCreateCategory();
const blogId = useBlogId();
- const [open, setOpen] = useState(false);
+
+ const [name, setName] = useState("");
+ const [slug, setSlug] = useState("");
+
+ useEffect(() => {
+ setSlug(generateSlug(name));
+ }, [name]);
return (