bnch of improvements

This commit is contained in:
Jordi Enric
2024-05-02 23:19:10 +02:00
parent ea501f33dc
commit 86252656a1
11 changed files with 150 additions and 72 deletions
@@ -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}
+64 -38
View File
@@ -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>
+2 -2
View File
@@ -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}>
+5 -7
View File
@@ -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");
+22 -2
View File
@@ -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,
});
+25
View File
@@ -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
View File
@@ -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",