mirror of
https://github.com/jordienr/zenblog.git
synced 2026-10-06 05:47:38 -04:00
wip
This commit is contained in:
@@ -9,5 +9,6 @@ const config = {
|
||||
reactStrictMode: true,
|
||||
appDir: true,
|
||||
transpilePackages: [ "react-syntax-highlighter" ],
|
||||
appRootSelector: "main"
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"supabase:typegen": "npx supabase gen types typescript --project-id ppfseefimhneysnokffx --schema public > src/types/supabase.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@builder.io/react-hydration-overlay": "^0.0.8",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@groundcontrolsh/groundcontrol": "^0.3.0",
|
||||
@@ -35,6 +36,7 @@
|
||||
"@tiptap/extension-horizontal-rule": "^2.1.12",
|
||||
"@tiptap/extension-image": "^2.1.12",
|
||||
"@tiptap/extension-link": "^2.2.2",
|
||||
"@tiptap/extension-placeholder": "^2.2.2",
|
||||
"@tiptap/pm": "^2.0.3",
|
||||
"@tiptap/react": "^2.0.3",
|
||||
"@tiptap/starter-kit": "^2.0.3",
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { HiTrash } from "react-icons/hi";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
export function Invitations({ blog }: { blog: Blog }) {
|
||||
const router = useRouter();
|
||||
const blogId = router.query.blogId as string;
|
||||
@@ -61,7 +63,7 @@ export function Invitations({ blog }: { blog: Blog }) {
|
||||
{invitations.data?.map((invitation) => (
|
||||
<li
|
||||
key={invitation.id}
|
||||
className="flex justify-between gap-4 rounded-md border"
|
||||
className="flex items-center justify-between gap-4 rounded-xl border px-1"
|
||||
>
|
||||
<ul className="flex flex-grow items-center gap-4">
|
||||
<li className="w-3/4 p-2 font-semibold">{invitation.name}</li>
|
||||
@@ -72,24 +74,25 @@ export function Invitations({ blog }: { blog: Blog }) {
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
className="btn btn-icon"
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
onClick={() => onDeleteClick(invitation.id)}
|
||||
>
|
||||
<HiTrash />
|
||||
</button>
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<form className="flex flex-col gap-2" onSubmit={onSubmit}>
|
||||
<h3 className="mt-4 font-medium">Invite people to {blog.title}</h3>
|
||||
<div className="flex gap-4">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
{...invitationForm.register("name")}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
{...invitationForm.register("email")}
|
||||
@@ -97,9 +100,7 @@ export function Invitations({ blog }: { blog: Blog }) {
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Invite team member
|
||||
</button>
|
||||
<Button type="submit">Invite team member</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -45,12 +45,12 @@ function EditorMenuButton({
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
tabIndex={-1}
|
||||
variant={"ghost"}
|
||||
type="button"
|
||||
className={cn(
|
||||
"rounded-md p-2 text-zinc-400",
|
||||
active ? "text-zinc-500" : "",
|
||||
active ? "bg-zinc-100 text-zinc-800" : "text-zinc-400",
|
||||
props.disabled ? "" : "hover:bg-zinc-100/80 hover:text-zinc-600"
|
||||
)}
|
||||
{...props}
|
||||
@@ -67,16 +67,6 @@ function EditorMenuButton({
|
||||
export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
const SIZE = 18;
|
||||
|
||||
function getOSMetaKey() {
|
||||
const isMac = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
||||
return isMac ? "Cmd" : "Ctrl";
|
||||
}
|
||||
|
||||
function getMenuTooltip(command: string) {
|
||||
const key = getOSMetaKey();
|
||||
return command.replace("Meta", key);
|
||||
}
|
||||
|
||||
const Separator = {
|
||||
icon: <Dot size={SIZE} />,
|
||||
disabled: true,
|
||||
@@ -88,35 +78,41 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
command: () => void;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
active?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
tooltip: getMenuTooltip("Bold (Meta+B)"),
|
||||
tooltip: "Bold (Cmd+B)",
|
||||
icon: <BoldIcon size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleBold().run(),
|
||||
active: editor?.isActive("bold"),
|
||||
},
|
||||
{
|
||||
tooltip: getMenuTooltip("Italic (Meta+I)"),
|
||||
tooltip: "Italic (Cmd+I)",
|
||||
icon: <ItalicIcon size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleItalic().run(),
|
||||
active: editor?.isActive("italic"),
|
||||
},
|
||||
{
|
||||
tooltip: getMenuTooltip("Strikethrough"),
|
||||
tooltip: "Strikethrough",
|
||||
icon: <Strikethrough size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleStrike().run(),
|
||||
active: editor?.isActive("strike"),
|
||||
},
|
||||
{
|
||||
tooltip: getMenuTooltip("Code (Meta+E)"),
|
||||
tooltip: "Code (Cmd+E)",
|
||||
icon: <CodeIcon size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleCode().run(),
|
||||
active: editor?.isActive("code"),
|
||||
},
|
||||
{
|
||||
tooltip: getMenuTooltip("Code Block"),
|
||||
tooltip: "Code Block",
|
||||
icon: <PiCodeBlock size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleCodeBlock().run(),
|
||||
active: editor?.isActive("codeBlock"),
|
||||
},
|
||||
Separator,
|
||||
{
|
||||
tooltip: getMenuTooltip("Link"),
|
||||
tooltip: "Link",
|
||||
icon: <Link size={SIZE} />,
|
||||
command: () => {
|
||||
const url = window.prompt("Enter the URL");
|
||||
@@ -124,16 +120,19 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
editor?.chain().focus().setLink({ href: url }).run();
|
||||
}
|
||||
},
|
||||
active: editor?.isActive("link"),
|
||||
},
|
||||
{
|
||||
tooltip: getMenuTooltip("List"),
|
||||
tooltip: "List",
|
||||
icon: <ListIcon size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleBulletList().run(),
|
||||
active: editor?.isActive("bulletList"),
|
||||
},
|
||||
{
|
||||
tooltip: getMenuTooltip("Numbered List"),
|
||||
tooltip: "Numbered List",
|
||||
icon: <PiListNumbers size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleOrderedList().run(),
|
||||
active: editor?.isActive("orderedList"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -171,15 +170,18 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex rounded-lg border border-zinc-200 bg-white p-1 shadow-sm">
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className="inline-flex rounded-xl border border-zinc-200 bg-white p-1 shadow-sm"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger tabIndex={-1} asChild>
|
||||
<Button variant={"ghost"} className="w-24 text-zinc-500">
|
||||
{editor?.isFocused ||
|
||||
editor?.isActive("paragraph") ||
|
||||
editor?.isActive("heading")
|
||||
? editor.isActive("heading")
|
||||
? "Heading"
|
||||
? `Heading ${editor.getAttributes("heading").level}`
|
||||
: "Paragraph"
|
||||
: "Type"}
|
||||
</Button>
|
||||
@@ -197,10 +199,10 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{menuButtons.map(({ icon, command, disabled, tooltip }, i) => (
|
||||
{menuButtons.map(({ icon, command, disabled, tooltip, active }, i) => (
|
||||
<EditorMenuButton
|
||||
disabled={disabled}
|
||||
active={editor?.isActive(command) || false}
|
||||
active={active || false}
|
||||
key={i + "menu-btn"}
|
||||
onClick={command}
|
||||
tooltip={tooltip}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { IoClose } from "react-icons/io5";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "../ui/sheet";
|
||||
import EditorSettings from "./EditorSettings";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string(),
|
||||
@@ -63,6 +64,7 @@ type Props = {
|
||||
published: boolean;
|
||||
cover_image?: string;
|
||||
content?: any;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -122,6 +124,9 @@ export const ZendoEditor = (props: Props) => {
|
||||
},
|
||||
}),
|
||||
TiptapLink,
|
||||
Placeholder.configure({
|
||||
placeholder: "Start writing...",
|
||||
}),
|
||||
TiptapImage.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [UploadImagesPlugin()];
|
||||
@@ -132,6 +137,8 @@ export const ZendoEditor = (props: Props) => {
|
||||
|
||||
const formSubmit = handleSubmit(async (data) => {
|
||||
const content = editor?.getJSON() || {};
|
||||
const slugHasChanged = data.slug !== props.post?.slug;
|
||||
|
||||
props.onSave({
|
||||
content,
|
||||
title: data.title,
|
||||
@@ -139,6 +146,10 @@ export const ZendoEditor = (props: Props) => {
|
||||
cover_image: data.cover_image || "",
|
||||
published: data.published,
|
||||
});
|
||||
|
||||
if (slugHasChanged) {
|
||||
router.push(`/blogs/${blogId}/post/${data.slug}`);
|
||||
}
|
||||
});
|
||||
|
||||
function onCoverImageSelect(image: string) {
|
||||
@@ -147,10 +158,10 @@ export const ZendoEditor = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-zinc-50 pb-40">
|
||||
<form
|
||||
onSubmit={formSubmit}
|
||||
className="sticky top-0 z-20 flex w-full items-center justify-between border-b bg-white px-3 py-1.5"
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center gap-1 rounded-xl text-sm font-medium tracking-tight text-zinc-800">
|
||||
{blogsQuery.isLoading ? null : (
|
||||
@@ -202,10 +213,10 @@ export const ZendoEditor = (props: Props) => {
|
||||
|
||||
{postsQuery.isLoading ? null : (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={"ghost"}>
|
||||
<div className="flex items-center gap-1.5 rounded-md p-1.5 hover:bg-zinc-100">
|
||||
<span>{props.post?.title || title || ""}</span>
|
||||
<span>{props.post?.title || title || "New post"}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -242,7 +253,7 @@ export const ZendoEditor = (props: Props) => {
|
||||
</Label>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button type="button" title="post settings" variant="secondary">
|
||||
<Button type="button" title="post settings" variant="ghost">
|
||||
<Settings2 size={16} /> Settings
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
@@ -260,7 +271,7 @@ export const ZendoEditor = (props: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mx-auto mt-2 flex w-full max-w-2xl flex-col px-2">
|
||||
<div className="mx-auto mt-2 flex w-full max-w-2xl flex-col px-2 pb-40">
|
||||
<div className="relative mt-2 flex items-center justify-center bg-zinc-100">
|
||||
{coverImgUrl && (
|
||||
<button
|
||||
@@ -283,7 +294,7 @@ export const ZendoEditor = (props: Props) => {
|
||||
placeholder="a-great-title"
|
||||
type="text"
|
||||
{...register("slug")}
|
||||
className="w-full rounded-lg p-2 font-mono text-xs outline-none hover:bg-zinc-50 focus-visible:bg-zinc-100"
|
||||
className="w-full rounded-lg bg-transparent p-2 font-mono text-xs text-zinc-700 outline-none transition-all hover:bg-white focus-visible:bg-white focus-visible:shadow-sm"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</label>
|
||||
@@ -296,36 +307,41 @@ export const ZendoEditor = (props: Props) => {
|
||||
}}
|
||||
onCancel={() => {}}
|
||||
>
|
||||
<Button asChild variant={"secondary"} className="h-8">
|
||||
<span className="btn btn-text whitespace-nowrap !text-xs">
|
||||
<BsFillImageFill className="h-4 w-4" />
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className="text-zinc-600 hover:bg-white"
|
||||
>
|
||||
<span className="flex gap-1.5 whitespace-nowrap !text-xs">
|
||||
<BsFillImageFill className="h-4 w-4 text-zinc-400" />
|
||||
Cover image
|
||||
</span>
|
||||
</Button>
|
||||
</ImagePicker>
|
||||
</div>
|
||||
|
||||
<input
|
||||
<textarea
|
||||
placeholder="A great title"
|
||||
type="text"
|
||||
{...register("title")}
|
||||
className="mt-1 w-full max-w-2xl whitespace-break-spaces rounded-xl border-none bg-transparent p-2
|
||||
style={{ resize: "none" }}
|
||||
className="mt-1 w-full max-w-2xl rounded-xl border-none bg-transparent p-2
|
||||
text-4xl
|
||||
font-medium outline-none transition-all hover:bg-zinc-50 focus-visible:bg-zinc-100"
|
||||
font-medium text-zinc-800 outline-none transition-all hover:bg-white focus-visible:bg-white focus-visible:shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="prose prose-h2:font-medium group">
|
||||
<div className="group">
|
||||
<div className="sticky top-14 z-10">
|
||||
<EditorMenu editor={editor} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => editor?.chain().focus().toggleBold().run()}
|
||||
className="prose prose-p:text-lg -mt-2 min-h-[700px] cursor-text rounded-lg transition-all focus-within:bg-zinc-50 hover:bg-zinc-50"
|
||||
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-tight transition-all"
|
||||
>
|
||||
<EditorContent className="" editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ export function ImagePicker({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger>{children}</DialogTrigger>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
|
||||
@@ -16,7 +16,7 @@ const buttonVariants = cva(
|
||||
outline:
|
||||
"border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
|
||||
secondary:
|
||||
"bg-zinc-100 text-zinc-700 hover:bg-zinc-200/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80",
|
||||
"bg-zinc-100 text-zinc-700 hover:bg-orange-100/70 hover:text-orange-600 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80",
|
||||
ghost:
|
||||
"hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
|
||||
link: "text-zinc-800 underline-offset-4 hover:underline dark:text-zinc-50",
|
||||
|
||||
@@ -23,14 +23,11 @@ export function createAPIClient() {
|
||||
const URL_START = "/api/v1";
|
||||
const res = await fetch(URL_START + input, { ...init, headers });
|
||||
|
||||
console.log(`[🛫 Req ${init.method} ${input}] `, init.body);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`[🚨 ${init.method} ${input}] `, res.statusText);
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
const data = await res.json();
|
||||
console.log(`[✅ Res ${init.method} ${input}] `, data);
|
||||
|
||||
const result = await type.safeParseAsync(data);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { AppProps } from "next/app";
|
||||
import { Inter, IBM_Plex_Mono } from "next/font/google";
|
||||
import "@/styles/globals.css";
|
||||
import { useRouter } from "next/router";
|
||||
import { motion } from "framer-motion";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
import {
|
||||
Hydrate,
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function Post() {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col bg-white pb-32">
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<ZendoEditor
|
||||
onSave={async (content) => {
|
||||
try {
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
FaChartLine,
|
||||
FaCode,
|
||||
FaImage,
|
||||
FaImages,
|
||||
FaRegChartBar,
|
||||
FaTwitter,
|
||||
} from "react-icons/fa";
|
||||
import { SiTypescript } from "react-icons/si";
|
||||
import { Command } from "lucide-react";
|
||||
import { CgTrees } from "react-icons/cg";
|
||||
import { FaTwitter } from "react-icons/fa";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { getClientClient } from "@/lib/supabase";
|
||||
@@ -22,52 +12,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const Home = () => {
|
||||
const features = [
|
||||
{
|
||||
title: "Type safety",
|
||||
description:
|
||||
"Fully typed API client that you can use in your app to fetch your content. No GraphQL.",
|
||||
icon: <SiTypescript size="24" className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "Cmd + V Images",
|
||||
description:
|
||||
"Imagine having to open the system file picker just to upload an image.",
|
||||
icon: <FaImages size="24" className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "SEO",
|
||||
description: "Easily manage SEO metadata for your posts.",
|
||||
icon: <FaChartLine size="24" className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "Open graph images",
|
||||
description: "Automatic generation of open graph images for your posts.",
|
||||
icon: <FaImage size="24" className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "Cmd + K",
|
||||
description: "A modern dashboard that you can navigate easily.",
|
||||
icon: <Command className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "Unlimited blogs",
|
||||
description:
|
||||
"It doesn't matter how many blogs you have. Simple usage based pricing.",
|
||||
icon: <CgTrees size="24" className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
description: "Easily find out how many page views your blog posts have.",
|
||||
icon: <FaRegChartBar size="24" className="text-orange-500" />,
|
||||
},
|
||||
{
|
||||
title: "Open Source",
|
||||
description: "Built with Next.js and Supabase.",
|
||||
icon: <FaCode size="24" className="text-orange-500" />,
|
||||
},
|
||||
];
|
||||
|
||||
const user = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
}
|
||||
|
||||
.ProseMirror p > code {
|
||||
background-color: theme("colors.orange.50");
|
||||
background-color: theme("colors.zinc.100");
|
||||
border: 1px solid theme("colors.zinc.200");
|
||||
border-radius: 4px;
|
||||
padding: 0.2em 0.4em;
|
||||
color: theme("colors.orange.500");
|
||||
padding: 0.2em;
|
||||
color: theme("colors.zinc.700");
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
.ProseMirror p > code::after,
|
||||
@@ -59,3 +60,11 @@ textarea:active {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tiptap .is-editor-empty:first-child::before {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
Generated
+1335
-85
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,21 @@
|
||||
# To Do
|
||||
|
||||
## IMPORTANT
|
||||
|
||||
- [] First time a user logs in, they should be prompted to add a password and reload the session.
|
||||
- [] Make layout work on mobile
|
||||
- [] Make inputs not zoom in on mobile
|
||||
|
||||
## Ideas
|
||||
|
||||
- [] Allow users to add default metadata for posts in a blog.
|
||||
- [] Add PolyScale cache to Supabase for faster queries?
|
||||
- [] Categories
|
||||
- [] Tags
|
||||
- [] Featured Images
|
||||
- [] Excerpts
|
||||
- [] Authors
|
||||
- [] Analytics
|
||||
|
||||
## Blogs
|
||||
|
||||
@@ -59,8 +67,8 @@
|
||||
|
||||
## Docs
|
||||
|
||||
- [] Add documentation for API client
|
||||
- [] Copypastable Zenblog Renderer component
|
||||
- [] Add documentation for integrating with Zenblog
|
||||
- [] Copypastable Zenblog Renderer component similar to shadcn
|
||||
|
||||
## User Feedback
|
||||
|
||||
|
||||
Reference in New Issue
Block a user