mirror of
https://github.com/jordienr/zenblog.git
synced 2026-09-28 01:56:07 -04:00
bnch of improvements
This commit is contained in:
@@ -174,7 +174,7 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className="border-zinc-2 inline-flex max-w-full overflow-x-auto rounded-xl border border-b-2 bg-white p-0.5"
|
||||
className="flex max-w-full overflow-x-auto border-b bg-white p-1.5"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger tabIndex={-1} asChild>
|
||||
|
||||
@@ -6,12 +6,10 @@ import { Code, Info, Plus, Trash } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
|
||||
import { TagManagement } from "./TagManagement";
|
||||
import { toast } from "sonner";
|
||||
import DateTimePicker from "react-datetime-picker";
|
||||
import "react-datetime-picker/dist/DateTimePicker.css";
|
||||
import "react-calendar/dist/Calendar.css";
|
||||
import "react-clock/dist/Clock.css";
|
||||
import Image from "next/image";
|
||||
import { CopyCell } from "../copy-cell";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -20,6 +18,12 @@ import {
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
|
||||
type Tag = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
type MetadataItem = {
|
||||
key: string;
|
||||
value: string;
|
||||
@@ -27,7 +31,7 @@ type MetadataItem = {
|
||||
type Props = {
|
||||
title: string;
|
||||
metadata?: MetadataItem[];
|
||||
selectedTags: string[];
|
||||
selectedTags: Tag[];
|
||||
published_at?: string;
|
||||
blogEmoji?: string;
|
||||
blogTitle?: string;
|
||||
@@ -37,7 +41,7 @@ type Props = {
|
||||
published_at,
|
||||
}: {
|
||||
metadata: MetadataItem[];
|
||||
tags: string[];
|
||||
tags: Tag[];
|
||||
published_at?: string;
|
||||
}) => void;
|
||||
};
|
||||
@@ -48,7 +52,7 @@ const EditorSettings = (props: Props) => {
|
||||
: [{ key: "", value: "" }];
|
||||
|
||||
const [metadata, setMetadata] = useState<MetadataItem[]>(defaultMetadata);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>(
|
||||
const [selectedTags, setSelectedTags] = useState<Tag[]>(
|
||||
props.selectedTags || []
|
||||
);
|
||||
const today = new Date().toISOString().split("T")[0] || "";
|
||||
@@ -122,7 +126,7 @@ const EditorSettings = (props: Props) => {
|
||||
props.onChange({ tags: selectedTags, metadata: newMetadata });
|
||||
};
|
||||
|
||||
const handleCategoryChange = (tags: string[]) => {
|
||||
const handleCategoryChange = (tags: Tag[]) => {
|
||||
setSelectedTags(tags);
|
||||
props.onChange({ metadata, tags });
|
||||
};
|
||||
|
||||
@@ -8,9 +8,14 @@ import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
|
||||
import { Plus, Wand } from "lucide-react";
|
||||
import { generateSlug } from "@/lib/utils/slugs";
|
||||
|
||||
type Tag = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
type Props = {
|
||||
selectedTags: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
selectedTags: Tag[];
|
||||
onChange: (tags: Tag[]) => void;
|
||||
};
|
||||
|
||||
export const TagManagement = (props: Props) => {
|
||||
@@ -22,11 +27,11 @@ export const TagManagement = (props: Props) => {
|
||||
const [title, setTitle] = React.useState("");
|
||||
const [slug, setSlug] = React.useState("");
|
||||
|
||||
const [selectedTags, setSelectedTags] = React.useState<string[]>(
|
||||
const [selectedTags, setSelectedTags] = React.useState<Tag[]>(
|
||||
props.selectedTags || []
|
||||
);
|
||||
|
||||
const handleTagChange = (tags: string[]) => {
|
||||
const handleTagChange = (tags: Tag[]) => {
|
||||
setSelectedTags(tags);
|
||||
props.onChange(tags);
|
||||
};
|
||||
@@ -120,14 +125,14 @@ export const TagManagement = (props: Props) => {
|
||||
<input
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
handleTagChange([...selectedTags, e.target.value]);
|
||||
handleTagChange([...selectedTags, tag]);
|
||||
} else {
|
||||
handleTagChange(
|
||||
selectedTags.filter((tag) => tag !== e.target.value)
|
||||
selectedTags.filter((t) => t.id !== tag.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
checked={selectedTags.includes(tag.id)}
|
||||
checked={selectedTags.some((t) => t.id === tag.id)}
|
||||
type="checkbox"
|
||||
key={tag.id}
|
||||
value={tag.id}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
List,
|
||||
CornerUpLeft,
|
||||
Tag,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -41,6 +42,7 @@ import { toast } from "sonner";
|
||||
import { Database } from "@/types/supabase";
|
||||
import { useBlogTags } from "./Editor.queries";
|
||||
import { TagPicker } from "../Tags/TagPicker";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string(),
|
||||
@@ -59,7 +61,11 @@ type OnSaveData = {
|
||||
cover_image?: string;
|
||||
published: boolean;
|
||||
metadata?: any;
|
||||
tags?: string[];
|
||||
tags?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}[];
|
||||
published_at?: string;
|
||||
};
|
||||
|
||||
@@ -105,6 +111,13 @@ export const ZendoEditor = (props: Props) => {
|
||||
const blogQuery = useBlogQuery(blogId);
|
||||
|
||||
const title = watch("title");
|
||||
const slug = watch("slug");
|
||||
|
||||
useEffect(() => {
|
||||
// When a user writes a space, change it for a dash
|
||||
const newSlug = slug.replace(/\s/g, "-");
|
||||
setValue("slug", newSlug);
|
||||
}, [slug, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.autoCompleteSlug) {
|
||||
@@ -267,7 +280,7 @@ export const ZendoEditor = (props: Props) => {
|
||||
)}
|
||||
<form
|
||||
onSubmit={formSubmit}
|
||||
className="sticky top-0 z-20 flex w-full items-center justify-between border-b bg-zinc-50 px-3 py-1.5 text-zinc-800"
|
||||
className="sticky top-0 z-20 flex w-full items-center justify-between bg-zinc-50 px-3 py-1.5 text-zinc-800"
|
||||
>
|
||||
<div className="hidden items-center gap-1 rounded-xl text-sm font-medium tracking-tight text-zinc-800 md:flex">
|
||||
{blogsQuery.isLoading ? null : (
|
||||
@@ -391,9 +404,9 @@ export const ZendoEditor = (props: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mx-auto mt-2 flex w-full max-w-2xl flex-col px-2 pb-6">
|
||||
<div className="relative mt-2 flex items-center justify-center bg-zinc-100">
|
||||
{coverImgUrl && (
|
||||
<div className="mx-auto mt-2 flex w-full max-w-3xl flex-col rounded-md border bg-white pb-6 pt-2 shadow-md">
|
||||
{coverImgUrl && (
|
||||
<div className="relative mt-2 flex items-center justify-center border-b">
|
||||
<button
|
||||
className="absolute -right-2 -top-2 z-10 rounded-full border bg-white p-1 shadow-sm"
|
||||
type="button"
|
||||
@@ -404,11 +417,13 @@ export const ZendoEditor = (props: Props) => {
|
||||
>
|
||||
<IoClose />
|
||||
</button>
|
||||
)}
|
||||
<img className="max-h-96" src={coverImgUrl || ""} alt="" />
|
||||
</div>
|
||||
<div className="mt-4 pb-2">
|
||||
<div className="flex w-full justify-between transition-all">
|
||||
|
||||
<img className="max-h-96" src={coverImgUrl || ""} alt="" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-8 pb-2">
|
||||
<div className="flex w-full items-end justify-between gap-4 transition-all">
|
||||
<label
|
||||
className="group flex w-full flex-col items-start justify-center gap-1"
|
||||
htmlFor="slug"
|
||||
@@ -421,7 +436,7 @@ export const ZendoEditor = (props: Props) => {
|
||||
placeholder="a-great-title"
|
||||
type="text"
|
||||
{...register("slug")}
|
||||
className="w-full rounded-lg border border-transparent bg-transparent p-2 font-mono text-sm text-zinc-700 outline-none transition-all hover:bg-white focus:border-zinc-300 focus-visible:bg-white focus-visible:shadow-sm"
|
||||
className="w-full rounded-lg border border-transparent bg-transparent p-1 font-mono text-sm text-zinc-700 outline-none transition-all hover:bg-white focus:bg-zinc-100"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</label>
|
||||
@@ -450,52 +465,63 @@ export const ZendoEditor = (props: Props) => {
|
||||
placeholder="A great title"
|
||||
{...register("title")}
|
||||
style={{ resize: "none" }}
|
||||
className="mt-1 w-full max-w-2xl rounded-xl bg-transparent p-2
|
||||
text-4xl font-medium text-zinc-800 outline-none"
|
||||
className="mt-1 w-full max-w-2xl rounded-xl bg-transparent
|
||||
p-2 text-4xl font-medium text-zinc-800 outline-none focus:bg-zinc-100"
|
||||
/>
|
||||
<div className="flex gap-0.5">
|
||||
{tags.length === 0 && (
|
||||
<TagPicker
|
||||
allTags={blogTags.data || []}
|
||||
selectedTags={tags}
|
||||
onChange={(newTags) => {
|
||||
setTags(newTags);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-zinc-500 transition-all hover:bg-zinc-100"
|
||||
onClick={() => {
|
||||
console.log("tags");
|
||||
}}
|
||||
>
|
||||
<Tag size="14" className="text-zinc-500" />
|
||||
Add tags
|
||||
</button>
|
||||
</TagPicker>
|
||||
)}
|
||||
<div className="group flex gap-0.5">
|
||||
{tags.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full border bg-zinc-100 px-2 py-1 font-mono text-xs font-medium"
|
||||
key={tag.id}
|
||||
className="flex rounded-full bg-zinc-100 font-mono text-xs font-medium"
|
||||
>
|
||||
{blogTags.data?.find((t) => t.id === tag)?.name}
|
||||
<div className="p-1 pl-2 pr-0">
|
||||
{blogTags.data?.find((t) => t.id === tag.id)?.name}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setTags(tags.filter((t) => t.id !== tag.id));
|
||||
}}
|
||||
className="rounded-full p-1 pr-2"
|
||||
>
|
||||
<X size="10" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TagPicker
|
||||
allTags={blogTags.data || []}
|
||||
selectedTags={tags}
|
||||
onChange={(newTags) => {
|
||||
setTags(newTags);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded-md px-1.5 py-1 text-xs text-zinc-400 opacity-0 transition-all hover:text-zinc-700 group-hover:opacity-100",
|
||||
{
|
||||
"opacity-100": tags.length === 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Tag size="14" className="text-zinc-300" />
|
||||
Add tags
|
||||
</button>
|
||||
</TagPicker>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group">
|
||||
<div className="sticky top-12 z-30">
|
||||
<div className="sticky top-10 z-10">
|
||||
<EditorMenu editor={editor} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
editor?.commands.focus();
|
||||
}}
|
||||
className="prose prose-p:text-lg prose-h2:font-semibold -mt-2 min-h-[700px] cursor-text rounded-lg py-1.5 font-light leading-10 tracking-normal transition-all"
|
||||
className="prose prose-p:text-lg prose-h2:font-semibold -mt-2 min-h-[700px] cursor-text rounded-lg px-8 py-1.5 font-light leading-10 tracking-normal transition-all"
|
||||
>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Label } from "../ui/label";
|
||||
|
||||
type Props = {
|
||||
tag: {
|
||||
tag_id: string;
|
||||
tag_name: string;
|
||||
slug: string;
|
||||
tag_id: string | null;
|
||||
tag_name: string | null;
|
||||
slug: string | null;
|
||||
};
|
||||
onSubmit: (tag: { tag_name: string; slug: string }) => void;
|
||||
open: boolean;
|
||||
@@ -41,9 +41,9 @@ export function UpdateTagDialog({ onSubmit, tag, open, onOpenChange }: Props) {
|
||||
}}
|
||||
>
|
||||
<Label>Name</Label>
|
||||
<Input name="tag_name" required defaultValue={tag.tag_name} />
|
||||
<Input name="tag_name" required defaultValue={tag.tag_name!} />
|
||||
<Label>Slug</Label>
|
||||
<Input name="slug" required defaultValue={tag.slug} />
|
||||
<Input name="slug" required defaultValue={tag.slug!} />
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button type="submit">Update</Button>
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,8 @@ export default function CreatePost() {
|
||||
}
|
||||
|
||||
if (content.tags) {
|
||||
const newTags = content.tags.map((tag: string) => ({
|
||||
tag_id: tag,
|
||||
const newTags = content.tags.map((tag) => ({
|
||||
tag_id: tag.id,
|
||||
blog_id: blogId,
|
||||
post_id: data.id,
|
||||
}));
|
||||
|
||||
@@ -61,8 +61,8 @@ export default function Post() {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const newTags = data?.tags?.map((tag: string) => ({
|
||||
tag_id: tag,
|
||||
const newTags = data?.tags?.map((tag) => ({
|
||||
tag_id: tag.id,
|
||||
blog_id: blogId,
|
||||
post_id: post.id,
|
||||
}));
|
||||
@@ -105,7 +105,7 @@ export default function Post() {
|
||||
}
|
||||
}}
|
||||
post={post}
|
||||
tags={post.post_tags.map((tag) => String(tag.tag_id)) || []}
|
||||
tags={post.post_tags || []}
|
||||
/>
|
||||
|
||||
{/* <Dialog open={showPubDialog} onOpenChange={setShowPubDialog}>
|
||||
|
||||
@@ -372,9 +372,7 @@ export default function BlogPosts() {
|
||||
<div className="rounded-xl border bg-white px-2 py-2 shadow-sm">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="px-3 py-1 text-lg font-medium tracking-tight">
|
||||
{tags.data?.length > 0
|
||||
? `${tags.data?.length} tags`
|
||||
: "Tags"}
|
||||
{tags.data?.length ? `${tags.data?.length} tags` : "Tags"}
|
||||
</h2>
|
||||
<CreateTagDialog blogId={blogId} />
|
||||
</div>
|
||||
@@ -388,7 +386,7 @@ export default function BlogPosts() {
|
||||
)}
|
||||
|
||||
<div className="grid divide-y">
|
||||
{tags.data?.length > 0 && (
|
||||
{tags.data?.length && (
|
||||
<div className="grid grid-cols-4 items-center p-2 text-sm font-medium text-zinc-600">
|
||||
<div>Tag</div>
|
||||
<div>Slug</div>
|
||||
@@ -400,7 +398,7 @@ export default function BlogPosts() {
|
||||
{tags.data?.map((tag) => {
|
||||
return (
|
||||
<div
|
||||
key={tag.id}
|
||||
key={tag.tag_id}
|
||||
className="grid grid-cols-4 items-center px-2 py-1.5 hover:bg-zinc-50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -449,7 +447,7 @@ export default function BlogPosts() {
|
||||
const res = await updateTagMutation.mutateAsync({
|
||||
name: newTag.tag_name,
|
||||
slug: newTag.slug,
|
||||
id: tag.id,
|
||||
id: tag.tag_id!,
|
||||
});
|
||||
if (res.error) {
|
||||
toast.error("Failed to update tag");
|
||||
@@ -467,7 +465,7 @@ export default function BlogPosts() {
|
||||
onOpenChange={setDeleteTagDialogOpen}
|
||||
onConfirm={async () => {
|
||||
const res = await deleteTagMutation.mutateAsync(
|
||||
tag.tag_id
|
||||
tag.tag_id!
|
||||
);
|
||||
if (res.error) {
|
||||
toast.error("Failed to delete tag");
|
||||
|
||||
@@ -32,11 +32,31 @@ export const usePostQuery = (postSlug: string) => {
|
||||
queryFn: async () => {
|
||||
const { data, error } = await sb
|
||||
.from("posts")
|
||||
.select("*, post_tags(*)")
|
||||
.select("*")
|
||||
.eq("slug", postSlug)
|
||||
.single();
|
||||
|
||||
return data;
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { data: tagsData, error: tagsError } = await sb
|
||||
.from("post_tags")
|
||||
.select("*")
|
||||
.eq("post_id", data.id);
|
||||
|
||||
if (tagsError) {
|
||||
console.error(tagsError);
|
||||
throw tagsError;
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
tags: tagsData,
|
||||
},
|
||||
};
|
||||
},
|
||||
enabled: !!postSlug,
|
||||
});
|
||||
|
||||
@@ -201,6 +201,12 @@ export interface Database {
|
||||
columns: ["tag_id"]
|
||||
referencedRelation: "blog_tags"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "post_tags_tag_id_fkey"
|
||||
columns: ["tag_id"]
|
||||
referencedRelation: "tag_usage_count_v1"
|
||||
referencedColumns: ["tag_id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -516,6 +522,25 @@ export interface Database {
|
||||
}
|
||||
]
|
||||
}
|
||||
tag_usage_count_v1: {
|
||||
Row: {
|
||||
blog_id: string | null
|
||||
created_at: string | null
|
||||
post_count: number | null
|
||||
slug: string | null
|
||||
tag_id: string | null
|
||||
tag_name: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "blog_tags_blog_id_fkey"
|
||||
columns: ["blog_id"]
|
||||
referencedRelation: "blogs"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
users: {
|
||||
Row: {
|
||||
aud: string | null
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"dev": "turbo run dev",
|
||||
"start": "concurrently --names \"APP,STRIPE\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run dev\" \"npm run stripe:webhook\"",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"db:typegen": "npx supabase gen types typescript --local --schema public > apps/zendo/src/types/supabase.ts",
|
||||
"db:typegen": "npx supabase gen types typescript --local --schema public > apps/web/src/types/supabase.ts",
|
||||
"db:start": "npx supabase start",
|
||||
"db:stop": "npx supabase stop",
|
||||
"db:open": "open http://localhost:54323",
|
||||
|
||||
Reference in New Issue
Block a user