improve tag mgmgt

This commit is contained in:
Jordi Enric
2024-05-04 16:28:35 +02:00
parent 531ccf8e62
commit ed4b2bbb11
12 changed files with 690 additions and 331 deletions
+2 -2
View File
@@ -19,7 +19,7 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "1.0.5",
"@radix-ui/react-popover": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.2",
@@ -53,7 +53,7 @@
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"cmdk": "^0.2.1",
"dotenv": "^16.4.4",
"embla-carousel-react": "^8.0.0",
"formidable": "^3.5.1",
@@ -134,7 +134,7 @@ const EditorSettings = (props: Props) => {
const ogImageUrl = `/api/og?title=${props.title}&emoji=${props.blogEmoji}&url=${props.blogTitle}`;
return (
<div className="[&_h2]:font-mono [&_h2]:text-sm [&_h2]:font-medium">
<div className="overflow-y-auto [&_h2]:font-mono [&_h2]:text-sm [&_h2]:font-medium">
<section className="mt-2">
<h2 className="mt-2 pb-2 font-mono">Published date</h2>
<div className="flex gap-4">
@@ -344,12 +344,12 @@ const EditorSettings = (props: Props) => {
Add metadata
</Button>
</div>
<div className="mt-4">
{/* <div className="mt-4">
<TagManagement
selectedTags={selectedTags}
onChange={handleCategoryChange}
/>
</div>
</div> */}
</section>
<section className="mt-8">
@@ -511,6 +511,7 @@ export const ZendoEditor = (props: Props) => {
onChange={(newTags) => {
setTags(newTags);
}}
blogId={blogId}
>
<button
tabIndex={-1}
@@ -0,0 +1,116 @@
import { Extension } from "@tiptap/core";
import type { Editor, Range } from "@tiptap/core";
import { ReactRenderer } from "@tiptap/react";
import Suggestion, { type SuggestionOptions } from "@tiptap/suggestion";
import type { ReactNode } from "react";
import tippy, {
type GetReferenceClientRect,
type Instance,
type Props,
} from "tippy.js";
const EditorCommandOut = () => {
return <div>EditorCommandOut</div>;
};
const Command = Extension.create({
name: "slash-command",
addOptions() {
return {
suggestion: {
char: "/",
command: ({ editor, range, props }) => {
props.command({ editor, range });
},
} as SuggestionOptions,
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
];
},
});
const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: Instance<Props>[] | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(EditorCommandOut, {
props,
editor: props.editor,
});
const { selection } = props.editor.state;
const parentNode = selection.$from.node(selection.$from.depth);
const blockType = parentNode.type.name;
if (blockType === "codeBlock") {
return false;
}
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: {
editor: Editor;
clientRect: GetReferenceClientRect;
}) => {
component?.updateProps(props);
popup?.[0]?.setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0]?.hide();
return true;
}
// @ts-ignore
return component?.ref?.onKeyDown(props);
},
onExit: () => {
popup?.[0]?.destroy();
component?.destroy();
},
};
};
export interface SuggestionItem {
title: string;
description: string;
icon: ReactNode;
searchTerms?: string[];
command?: (props: { editor: Editor; range: Range }) => void;
}
export const createSuggestionItems = (items: SuggestionItem[]) => items;
export const handleCommandNavigation = (event: KeyboardEvent) => {
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
}
};
export { Command, renderItems };
+64 -5
View File
@@ -1,5 +1,20 @@
import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
import { PropsWithChildren } from "react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { CreateTagDialog } from "./CreateTagDialog";
type Tag = {
id: string;
@@ -11,6 +26,7 @@ type Props = {
allTags: Tag[];
onChange: (tag: Tag[]) => void;
selectedTags: Tag[];
blogId: string;
};
export function TagPicker({
@@ -18,19 +34,59 @@ export function TagPicker({
onChange,
selectedTags,
children,
blogId,
}: PropsWithChildren<Props>) {
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogContent className="max-w-sm md:max-w-sm">
<h2 className="font-medium">Select tags</h2>
<div className="grid grid-cols-3 gap-2">
{/* <Popover>
<PopoverTrigger>pick tags</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search tags" />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{allTags.map((tag) => (
<CommandItem
key={tag.id}
value={tag.name}
onSelect={() => {
if (selectedTags.includes(tag)) {
onChange(selectedTags.filter((t) => t.id !== tag.id));
} else {
onChange([...selectedTags, tag]);
}
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedTags.find((t) => tag.id === t.id)
? "opacity-100"
: "opacity-0"
)}
/>
{tag.name}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover> */}
<div className="flex flex-wrap gap-1">
{allTags.map((tag) => (
<button
key={tag.id}
className={`${
selectedTags.includes(tag) ? "bg-slate-100" : ""
} rounded-md p-2`}
className={cn(
"rounded-full border border-zinc-200 bg-zinc-50 px-3 py-0.5 font-mono text-sm font-medium tracking-tight text-zinc-500 transition-all",
{
"border-orange-600 bg-orange-500 text-white":
selectedTags.includes(tag),
}
)}
onClick={() => {
if (selectedTags.includes(tag)) {
onChange(selectedTags.filter((t) => t.id !== tag.id));
@@ -42,6 +98,9 @@ export function TagPicker({
{tag.name}
</button>
))}
<div className="flex w-full justify-end">
<CreateTagDialog blogId={blogId} />
</div>
</div>
</DialogContent>
</Dialog>
+153
View File
@@ -0,0 +1,153 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
+29
View File
@@ -0,0 +1,29 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
// <PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
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 w-72 rounded-md border p-4 shadow-md outline-none",
className
)}
{...props}
/>
// </PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
+1 -1
View File
@@ -63,7 +63,7 @@ const SheetContent = React.forwardRef<
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-3 top-3 overflow-auto rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-3 top-3 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
+1 -1
View File
@@ -36,7 +36,7 @@ module.exports = {
},
fontFamily: {
default: ["var(--font-inter)"],
mono: ["var(--font-ibm-plex-mono)"],
mono: ["var(--font-ibm-plex-mono)", "monospace"],
},
colors: {
slate: {
+314 -316
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -34,6 +34,7 @@
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@supabase/ssr": "^0.1.0",
"@tiptap/suggestion": "^2.3.1",
"@types/inquirer": "^9.0.3",
"next": "^14.1.0",
"tsx": "^4.7.1"
+5 -3
View File
@@ -8,7 +8,7 @@
- [x] Update README
- [] Homepage use cases examples: personal blog, docs, careers page, product listing pages, help center, changelogs, directory websites, etc.
- [] email provider
- [] Integration guide tab
- [x] Integration guide tab
- [] Slash commands in editor (like notion or craft)
- [] Improve tag picker
@@ -19,9 +19,11 @@
## Blogs
- Integration guide
- [] instruct users to keep the blog id private.
- [] allow users to rotate the blog id?
- [x] instruct users to keep the id private.
- [] create a different column for the connection string
- [] allow users to rotate the connection string
- [] Recommend users to cache content
- [] create tinybird endpoint to get all views for a blog
## Posts