mirror of
https://github.com/jordienr/zenblog.git
synced 2026-09-28 01:56:07 -04:00
wip
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
type UseTags = {
|
||||
blogId: string;
|
||||
|
||||
@@ -67,9 +67,9 @@ const EditorSettings = (props: Props) => {
|
||||
<div>Value</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="ml-auto" variant={"outline"} size={"sm"}>
|
||||
<Button className="ml-auto" variant={"ghost"}>
|
||||
<Code size={16} />
|
||||
Preview
|
||||
<div>Preview</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
|
||||
@@ -38,9 +38,13 @@ const TagSelect = (props: Props) => {
|
||||
<h2 className="">Tags</h2>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="icon" variant={"ghost"} title="Create tag">
|
||||
<div className="sr-only">Create tag</div>
|
||||
<Button
|
||||
className="text-slate-500"
|
||||
variant={"ghost"}
|
||||
title="Create tag"
|
||||
>
|
||||
<Plus />
|
||||
<div>Create tag</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-xs">
|
||||
@@ -85,7 +89,6 @@ const TagSelect = (props: Props) => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<pre>{JSON.stringify(selectedTags)}</pre>
|
||||
{tags.isLoading ? (
|
||||
<></>
|
||||
) : (
|
||||
|
||||
@@ -15,14 +15,14 @@ const Footer = (props: Props) => {
|
||||
// label: "Blog",
|
||||
// href: "/blog",
|
||||
// },
|
||||
{
|
||||
label: "Docs",
|
||||
href: "/docs/getting-started",
|
||||
},
|
||||
{
|
||||
label: "Pricing",
|
||||
href: "/pricing",
|
||||
},
|
||||
// {
|
||||
// label: "Docs",
|
||||
// href: "/docs/getting-started",
|
||||
// },
|
||||
// {
|
||||
// label: "Pricing",
|
||||
// href: "/pricing",
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
|
||||
const api = createAPIClient();
|
||||
|
||||
export const useImages = (blogId: string) =>
|
||||
useQuery(["images"], () => api.images.getAll(blogId));
|
||||
useQuery({
|
||||
queryKey: ["images", blogId],
|
||||
queryFn: () => api.images.getAll(blogId),
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-full bg-slate-100 p-1 text-slate-500 dark:bg-slate-800 dark:text-slate-400",
|
||||
"inline-flex h-10 items-center justify-center rounded-full border bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-medium ring-offset-white transition-all hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-slate-950 data-[state=active]:shadow-sm dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300 dark:data-[state=active]:bg-slate-950 dark:data-[state=active]:text-slate-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-medium ring-offset-white transition-all last:rounded-r-full hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=active]:bg-zinc-950 dark:data-[state=active]:text-zinc-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",
|
||||
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -104,8 +104,8 @@ export const SubscribeSection = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">Pick a subscription</h2>
|
||||
<p className="text-zinc-500">You can cancel anytime</p>
|
||||
<h2 className="text-xl font-medium">Pricing</h2>
|
||||
<p className="font-mono text-sm text-zinc-500">Cancel anytime</p>
|
||||
|
||||
<Tabs
|
||||
className="mt-4"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IoSettingsSharp } from "react-icons/io5";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoreVertical, Trash } from "lucide-react";
|
||||
import { MoreVertical, PenLine, PlusIcon, TagIcon, Trash } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -21,6 +21,8 @@ import { useBlogTags } from "@/components/Editor/Editor.queries";
|
||||
import { usePostsQuery } from "@/queries/posts";
|
||||
import { useBlogQuery } from "@/queries/blogs";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { PiGear, PiPencilLine, PiTag, PiTrash } from "react-icons/pi";
|
||||
import Spinner from "@/components/Spinner";
|
||||
|
||||
export function StatePill({ published }: { published: boolean }) {
|
||||
const text = published ? "Published" : "Draft";
|
||||
@@ -92,7 +94,7 @@ export default function BlogPosts() {
|
||||
aria-label="Settings"
|
||||
>
|
||||
<div className="sr-only">Settings</div>
|
||||
<IoSettingsSharp size="24" />
|
||||
<PiGear size="24" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -101,6 +103,7 @@ export default function BlogPosts() {
|
||||
href={`/blogs/${blog.id}/create`}
|
||||
className="btn btn-secondary max-w-[120px]"
|
||||
>
|
||||
<PiPencilLine size="16" />
|
||||
New post
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -111,7 +114,7 @@ export default function BlogPosts() {
|
||||
{posts.length === 0 && (
|
||||
<div className="p-12 py-32 text-center">
|
||||
<div className="text-2xl">✏️</div>
|
||||
<div className="text-lg text-slate-500">
|
||||
<div className="text-lg text-zinc-500">
|
||||
Nothing here yet
|
||||
</div>
|
||||
<Button asChild>
|
||||
@@ -128,7 +131,7 @@ export default function BlogPosts() {
|
||||
return (
|
||||
<Link
|
||||
href={`/blogs/${blogId}/post/${post.slug}`}
|
||||
className="flex items-center gap-4 rounded-sm p-3 hover:bg-zinc-100/60"
|
||||
className="flex items-center gap-4 rounded-sm p-3 hover:bg-zinc-100/50"
|
||||
key={post.slug}
|
||||
>
|
||||
{post.cover_image && (
|
||||
@@ -140,23 +143,28 @@ export default function BlogPosts() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{post.title}</h2>
|
||||
</div>
|
||||
<StatePill published={post.published || false} />
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
{post.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-md bg-zinc-100 px-2 py-1 text-xs font-semibold text-zinc-600"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div>
|
||||
<StatePill published={post.published || false} />
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2 text-xs text-slate-500">
|
||||
<h2 className="ml-1 text-lg font-medium">
|
||||
{post.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 text-xs text-zinc-500">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
{post.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-md bg-zinc-100 px-2 py-1 font-mono text-xs font-semibold text-zinc-600"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<span>{formatDate(post.created_at || "")}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -203,22 +211,86 @@ export default function BlogPosts() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="media">
|
||||
{images.data?.map((img) => {
|
||||
return (
|
||||
<div key={img.id}>
|
||||
<img src={img.url} alt={img.name} />
|
||||
{images.isLoading && (
|
||||
<>
|
||||
<div className="flex-center p-12">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<div className="rounded-xl border bg-white py-2 shadow-sm">
|
||||
{images.data?.length === 0 && (
|
||||
<>
|
||||
<div className="p-12 py-32 text-center">
|
||||
<div className="text-2xl">📷</div>
|
||||
<div className="text-lg text-zinc-500">
|
||||
No images uploaded yet
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{images.data?.map((img) => {
|
||||
return (
|
||||
<div key={img.id} className="flex gap-4 px-4">
|
||||
<img
|
||||
className="h-24 w-32 rounded-md object-cover"
|
||||
src={img.url}
|
||||
alt={img.name}
|
||||
height={24}
|
||||
width={80}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-mono">{img.name}</h3>
|
||||
<p className="mt-1 font-mono text-xs tracking-tighter text-zinc-400">
|
||||
{img.url}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tags">
|
||||
{blogTags.data?.map((tag) => {
|
||||
return (
|
||||
<div key={tag.id}>
|
||||
<h2>{tag.name}</h2>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="rounded-xl border bg-white py-2 shadow-sm">
|
||||
{blogTags.data?.map((tag) => {
|
||||
return (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="flex items-center gap-1.5 px-4 py-2 font-mono"
|
||||
>
|
||||
<div className="rounded-md text-zinc-400">
|
||||
<PiTag size="16" />
|
||||
</div>
|
||||
<h2>{tag.name}</h2>
|
||||
<p className="text-sm text-zinc-500">/{tag.slug}</p>
|
||||
<div>
|
||||
<Button asChild size="icon" variant={"ghost"}>
|
||||
<Link
|
||||
href={`/blogs/${blogId}/tags/${tag.id}`}
|
||||
className="btn btn-icon"
|
||||
title="Edit tag"
|
||||
aria-label="Edit tag"
|
||||
>
|
||||
<PiPencilLine size="16" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="icon" variant={"ghost"}>
|
||||
<Link
|
||||
href={`/blogs/${blogId}/tags/${tag.id}`}
|
||||
className="btn btn-icon"
|
||||
title="Delete tag"
|
||||
aria-label="Delete tag"
|
||||
>
|
||||
<PiTrash size="16" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function Dashboard() {
|
||||
{data?.map((blog) => {
|
||||
return (
|
||||
<li
|
||||
className="group rounded-xl border bg-gradient-to-b from-white to-zinc-50 shadow-sm transition-all hover:border-orange-400"
|
||||
className="group rounded-xl border border-b-2 bg-white shadow-sm transition-all hover:border-orange-400"
|
||||
key={blog.id}
|
||||
>
|
||||
<Link
|
||||
@@ -62,7 +62,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">{blog.title}</h3>
|
||||
<p className="text-gray-600">{blog.description}</p>
|
||||
<p className="text-zinc-500">{blog.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions mt-4">
|
||||
|
||||
@@ -48,7 +48,7 @@ const Home = () => {
|
||||
<meta name="description" content="Open source blogging cms" />
|
||||
<link rel="icon" href="/static/favicon.ico" />
|
||||
</Head>
|
||||
<div className="bg-zinc-100">
|
||||
<div className="bg-zinc-50">
|
||||
<div className="mx-auto flex max-w-3xl flex-col">
|
||||
<nav className="flex items-center justify-between p-3">
|
||||
<div className="flex-grow cursor-default">
|
||||
@@ -82,11 +82,11 @@ const Home = () => {
|
||||
Log in
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
{/* <Button asChild>
|
||||
<Link href="/sign-up" title="Sign up" aria-label="Sign up">
|
||||
Sign up
|
||||
</Link>
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -144,7 +144,7 @@ const Home = () => {
|
||||
<hr className="my-12" />
|
||||
{!hasSubmitted && (
|
||||
<form
|
||||
className="mt-6 flex max-w-sm flex-col gap-2 py-2"
|
||||
className="mt-6 flex max-w-sm flex-col gap-2 py-2 pb-12"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<h2>
|
||||
@@ -193,11 +193,11 @@ const Home = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="my-12" />
|
||||
{/* <hr className="my-12" /> */}
|
||||
|
||||
<section className="pb-8">
|
||||
{/* <section className="pb-8">
|
||||
<SubscribeSection />
|
||||
</section>
|
||||
</section> */}
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
+8
-9
@@ -12,13 +12,12 @@
|
||||
## Bugs & Improvements
|
||||
|
||||
- [x] Fix email redirect when navigating to protected routes
|
||||
- [] Force email confirmation
|
||||
- [] Make layout work on mobile
|
||||
- [] Make inputs not zoom in on mobile
|
||||
- [x] Force email confirmation
|
||||
|
||||
## Pricing
|
||||
|
||||
- [] Add pricing page
|
||||
- [] Grace period for expired subscriptions
|
||||
- [x] Sync products between Stripe and Supabase
|
||||
- [x] Sync subscriptions between Stripe and Supabase
|
||||
|
||||
@@ -41,15 +40,15 @@
|
||||
- [x] Create post
|
||||
- [x] View post
|
||||
- [x] Edit post
|
||||
- [] Delete post
|
||||
- [x] Delete post
|
||||
|
||||
## Tags
|
||||
|
||||
- [] Create tag
|
||||
- [x] Create tag
|
||||
- [] Delete tag
|
||||
- [] View tag
|
||||
- [] View all tags
|
||||
- [] Add tags to posts
|
||||
- [] Update tag
|
||||
- [x] View all tags
|
||||
- [x] Add tags to posts
|
||||
|
||||
## Docs
|
||||
|
||||
@@ -62,4 +61,4 @@
|
||||
- [x] Add method to fetch all posts
|
||||
- [x] Add method to fetch a single post by slug
|
||||
- [] Pagination
|
||||
- [] Filtering by category?
|
||||
- [] Filtering by tag?
|
||||
|
||||
+4
-1
@@ -8,6 +8,8 @@
|
||||
const posts = await client.posts.list({ withContent: true, limit: 10 });
|
||||
```
|
||||
|
||||
- [] Allow draft/publish from post list.
|
||||
- [] Allow copy slug from post list.
|
||||
- [] Allow users to add default metadata for posts in a blog.
|
||||
- [] Add PolyScale cache to Supabase for faster queries?
|
||||
- [] Featured Images
|
||||
@@ -18,9 +20,10 @@ const posts = await client.posts.list({ withContent: true, limit: 10 });
|
||||
- [] Redesign settings page to be similar to Plausible
|
||||
- [] Use a generic on createClient to pass the custom metadata
|
||||
- [] Add files to custom metadata
|
||||
|
||||
- [] Allow "undo" for destructive actions - [] Delete post - [] Delete blog
|
||||
- Allow users to see their trash and restore items from the trash.
|
||||
- [] Make layout work on mobile
|
||||
- [] Make inputs not zoom in on mobile
|
||||
|
||||
## Teams
|
||||
|
||||
|
||||
Reference in New Issue
Block a user