mirror of
https://github.com/jordienr/zenblog.git
synced 2026-09-28 01:56:07 -04:00
yolo
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { PropsWithChildren, useState } from "react";
|
||||
import { ImageUploader } from "./ImageUploader";
|
||||
@@ -249,13 +248,13 @@ export function ImageItem({ image, selected, onClick }: ImageItem) {
|
||||
<div className="relative w-full">
|
||||
<img
|
||||
className={cn(
|
||||
"h-32 w-full rounded-xl border border-zinc-100 object-cover shadow-sm",
|
||||
"h-28 w-full rounded-xl border border-zinc-100 object-cover shadow-sm",
|
||||
{
|
||||
"bg-blend-hue": selected,
|
||||
"opacity-70": selected,
|
||||
}
|
||||
)}
|
||||
width="48"
|
||||
height="48"
|
||||
width="32"
|
||||
height="32"
|
||||
src={image.url}
|
||||
/>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import Feedback from "@/components/Feedback";
|
||||
import Footer from "@/components/Footer";
|
||||
import { usePlan } from "@/queries/subscription";
|
||||
import AppChecks from "@/components/LoggedInUserChecks";
|
||||
import { ChevronDown, Loader } from "lucide-react";
|
||||
import { Loader } from "lucide-react";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
@@ -58,36 +58,38 @@ export default function AppLayout({ children, loading = false }: Props) {
|
||||
</Link>
|
||||
{selectedBlog && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="ml-6 flex items-center gap-2 rounded-lg border bg-white px-2 py-1 text-xs font-medium focus-visible:ring-0"
|
||||
disabled={blogsLoading}
|
||||
>
|
||||
<span className="mx-1 -mt-1 flex items-center text-lg">
|
||||
{selectedBlog?.emoji}
|
||||
</span>
|
||||
{selectedBlog?.title}
|
||||
{blogsLoading ? (
|
||||
<Loader
|
||||
className="animate-spin text-orange-500"
|
||||
size={16}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-full p-1 px-2 text-xs font-medium text-blue-500">
|
||||
Pro
|
||||
</div>
|
||||
)}
|
||||
{plan === "free" && (
|
||||
<Link
|
||||
title="Upgrade to Pro"
|
||||
href="/account"
|
||||
className=" rounded-full
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenuTrigger
|
||||
className="ml-6 flex items-center gap-2 rounded-lg border bg-white px-2 py-1 text-xs font-medium focus-visible:ring-0"
|
||||
disabled={blogsLoading}
|
||||
>
|
||||
<span className="mx-1 -mt-1 flex items-center text-lg">
|
||||
{selectedBlog?.emoji}
|
||||
</span>
|
||||
{selectedBlog?.title}
|
||||
{blogsLoading ? (
|
||||
<Loader
|
||||
className="animate-spin text-orange-500"
|
||||
size={16}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-full p-1 px-2 text-xs font-medium text-blue-500">
|
||||
Pro
|
||||
</div>
|
||||
)}
|
||||
{plan === "free" && (
|
||||
<Link
|
||||
title="Upgrade to Pro"
|
||||
href="/account"
|
||||
className=" rounded-full
|
||||
bg-emerald-100 p-1
|
||||
px-2 text-center text-xs font-medium text-emerald-600"
|
||||
>
|
||||
Free
|
||||
</Link>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
>
|
||||
Free
|
||||
</Link>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="-ml-1 w-48 rounded-lg"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useCategories } from "@/queries/categories";
|
||||
|
||||
export function CategoriesPage() {
|
||||
const categories = useCategories();
|
||||
|
||||
if (categories.isLoading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{categories.data?.map((category) => (
|
||||
<div key={category.id}>{category.name}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,7 +46,8 @@ import { ImageUploader } from "@/components/Images/ImageUploader";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { IntegrationGuide } from "@/components/integration-guide";
|
||||
import { getHostedBlogUrl } from "@/utils/get-hosted-blog-url";
|
||||
|
||||
import { CategoriesPage } from "./categories";
|
||||
import { motion } from "framer-motion";
|
||||
export function StatePill({ published }: { published: boolean }) {
|
||||
const text = published ? "Published" : "Draft";
|
||||
if (published) {
|
||||
@@ -74,7 +75,15 @@ export default function BlogPosts() {
|
||||
const [confirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = useState(false);
|
||||
|
||||
const { data: blog, isLoading: blogLoading } = useBlogQuery(blogId);
|
||||
const { data: posts, isLoading: postsLoading } = usePostsQuery();
|
||||
|
||||
const POST_PAGE_SIZE = 10;
|
||||
const {
|
||||
data: posts,
|
||||
isLoading: postsLoading,
|
||||
fetchNextPage,
|
||||
} = usePostsQuery({
|
||||
pageSize: POST_PAGE_SIZE,
|
||||
});
|
||||
|
||||
const isLoading = blogLoading || postsLoading;
|
||||
|
||||
@@ -136,17 +145,22 @@ export default function BlogPosts() {
|
||||
<TabsTrigger onClick={() => {}} value="media">
|
||||
Media
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => {}} value="categories">
|
||||
Categories
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => {}} value="tags">
|
||||
Tags
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
onClick={() => {
|
||||
router.push(`/blogs/${blogId}/settings`);
|
||||
}}
|
||||
value="settings"
|
||||
>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
|
||||
<div className="ml-auto flex flex-1 items-center justify-end gap-2">
|
||||
<Link
|
||||
href={`/blogs/${blog.id}/settings`}
|
||||
className="px-3 py-1.5 text-sm font-medium"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
<TabsContent asChild value="posts">
|
||||
<Button asChild>
|
||||
<Link href={`/blogs/${blog.id}/create`}>
|
||||
@@ -232,8 +246,11 @@ export default function BlogPosts() {
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="posts">
|
||||
<TabSection title={`${posts.length} Posts`} actions={<></>}>
|
||||
{posts.length === 0 && (
|
||||
<TabSection
|
||||
title={`${posts.pages.length * 4} Posts`}
|
||||
actions={<></>}
|
||||
>
|
||||
{posts.pages.length === 0 && (
|
||||
<div className="p-12 py-32 text-center">
|
||||
<div className="text-2xl">✏️</div>
|
||||
<div className="text-lg text-zinc-500">
|
||||
@@ -249,33 +266,44 @@ export default function BlogPosts() {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{posts.map((post) => {
|
||||
return (
|
||||
<PostItem
|
||||
showClicks={true}
|
||||
key={post.post_id}
|
||||
post={post}
|
||||
blogId={blogId}
|
||||
onDeleteClick={async () => {
|
||||
const confirmed = window.confirm(
|
||||
"Are you sure you want to delete this post?"
|
||||
);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await deletePostMutation.mutateAsync(
|
||||
post.post_id || ""
|
||||
{posts.pages
|
||||
.flatMap((page) => page)
|
||||
.map((post) => {
|
||||
return (
|
||||
<PostItem
|
||||
showClicks={true}
|
||||
key={post?.post_id}
|
||||
post={post}
|
||||
blogId={blogId}
|
||||
onDeleteClick={async () => {
|
||||
const confirmed = window.confirm(
|
||||
"Are you sure you want to delete this post?"
|
||||
);
|
||||
toast.success("Post deleted");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to delete post");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="px-3 pt-2 text-center font-mono text-xs text-zinc-500">
|
||||
Total posts: {posts.length}
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await deletePostMutation.mutateAsync(
|
||||
post?.post_id || ""
|
||||
);
|
||||
toast.success("Post deleted");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to delete post");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 px-3 pt-2 text-center font-mono text-xs text-zinc-500">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
fetchNextPage();
|
||||
}}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
Showing {posts.pages.flatMap((page) => page).length} posts
|
||||
</div>
|
||||
</TabSection>
|
||||
</TabsContent>
|
||||
@@ -420,6 +448,11 @@ export default function BlogPosts() {
|
||||
</>
|
||||
</TabSection>
|
||||
</TabsContent>
|
||||
<TabsContent asChild value="categories">
|
||||
<TabSection title="Categories" actions={<></>}>
|
||||
<CategoriesPage />
|
||||
</TabSection>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<UpdateTagDialog
|
||||
tag={selectedTag}
|
||||
@@ -481,9 +514,9 @@ function PostItem({
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blogs/${blogId}/post/${post.slug}`}
|
||||
className="group flex flex-col gap-4 border-b px-3 py-1.5 transition-all md:flex-row md:items-center"
|
||||
key={post.slug}
|
||||
>
|
||||
<div className="hidden h-16 w-24 rounded-md bg-zinc-100 md:block ">
|
||||
{post.cover_image ? (
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import { createSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const usePostsQuery = () => {
|
||||
export const usePostsQuery = ({
|
||||
pageSize = 10,
|
||||
}: { pageSize?: number } = {}) => {
|
||||
const sb = createSupabaseBrowserClient();
|
||||
const { query } = useRouter();
|
||||
const blogId = query.blogId || "";
|
||||
|
||||
return useQuery({
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["posts", blogId],
|
||||
enabled: !!blogId,
|
||||
queryFn: async () => {
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage: any, pages: any) => {
|
||||
return lastPage.length > 0 ? pages.length * pageSize : undefined;
|
||||
},
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const { data, error } = await sb
|
||||
.from("posts_with_tags_v3")
|
||||
.from("posts_v5")
|
||||
.select("*")
|
||||
.eq("blog_id", blogId)
|
||||
.eq("deleted", false)
|
||||
.limit(50)
|
||||
.range(pageParam, pageParam + pageSize)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
return data;
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@
|
||||
## Backlog
|
||||
|
||||
- [] finish pagination in usePostsQuery
|
||||
- [] FAQ on homepage
|
||||
- [] Category management page
|
||||
- [] Categories in API + API Client
|
||||
- [x] FAQ on homepage
|
||||
- [] Nextjs example in homepage
|
||||
- [] Add tags to posts API
|
||||
- [] Fix deleting tags with posts
|
||||
|
||||
Reference in New Issue
Block a user