mirror of
https://github.com/jordienr/zenblog.git
synced 2026-09-27 09:35:47 -04:00
wip
This commit is contained in:
@@ -6,7 +6,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const token_hash = searchParams.get("token_hash");
|
||||
const type = searchParams.get("type") as EmailOtpType | null;
|
||||
const next = searchParams.get("next") ?? "/";
|
||||
const next = searchParams.get("next") ?? "/blogs";
|
||||
|
||||
const redirectTo = request.nextUrl.clone();
|
||||
redirectTo.pathname = next;
|
||||
|
||||
@@ -22,20 +22,20 @@ const LoggedInUserChecks = (props: PropsWithChildren<Props>) => {
|
||||
subscription?.status &&
|
||||
validSubscriptionStatus.includes(subscription.status);
|
||||
|
||||
if (!isLoading && !isValidSubscription) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center bg-zinc-50">
|
||||
<div className="max-w-xl">
|
||||
<SubscribeSection />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Link href="/sign-out" className="text-slate-500 underline">
|
||||
Sign out
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if (!isLoading && !isValidSubscription) {
|
||||
// return (
|
||||
// <div className="flex h-screen flex-col items-center justify-center bg-zinc-50">
|
||||
// <div className="max-w-3xl">
|
||||
// <SubscribeSection />
|
||||
// </div>
|
||||
// <div className="mt-4">
|
||||
// <Link href="/sign-out" className="text-slate-500 underline">
|
||||
// Sign out
|
||||
// </Link>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
@@ -80,6 +80,7 @@ export default function AppLayout({
|
||||
|
||||
return (
|
||||
<div className={`flex min-h-screen flex-col border-b bg-zinc-50 font-sans`}>
|
||||
{IS_DEV && <ZenblogToolbar />}
|
||||
<Head>
|
||||
<title>Zenblog</title>
|
||||
<meta name="description" content="Simple, headless, blogging CMS." />
|
||||
@@ -87,7 +88,6 @@ export default function AppLayout({
|
||||
</Head>
|
||||
<TooltipProvider>
|
||||
<AppChecks>
|
||||
{IS_DEV && <ZenblogToolbar />}
|
||||
<nav
|
||||
className={cn("mx-auto w-full bg-white", {
|
||||
"border-b shadow-sm": !selectedBlog,
|
||||
|
||||
@@ -196,7 +196,7 @@ const AccountPage = () => {
|
||||
</Section>
|
||||
|
||||
{subscription.data?.status !== "active" && (
|
||||
<Section className="my-4 px-4">
|
||||
<Section className="my-4 px-4 pb-4">
|
||||
<SubscribeSection />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function CreateCategoryDialog() {
|
||||
export default function CategoriesPage() {
|
||||
const blogId = useBlogId();
|
||||
|
||||
const { data: categories, isLoading } = useCategoriesWithPostCount();
|
||||
const { data: categories, isLoading } = useCategoriesWithPostCount(blogId);
|
||||
const [selectedCategory, setSelectedCategory] = useState<{
|
||||
category_id: number | null;
|
||||
category_name: string | null;
|
||||
@@ -109,7 +109,7 @@ export default function CategoriesPage() {
|
||||
title="Categories"
|
||||
loading={isLoading}
|
||||
actions={<CreateCategoryDialog />}
|
||||
description="Categories help you group your posts. Posts can have one category."
|
||||
description="Posts can have one category."
|
||||
>
|
||||
<Section>
|
||||
<Table>
|
||||
@@ -124,6 +124,16 @@ export default function CategoriesPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{categories?.data?.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center font-mono text-slate-500"
|
||||
>
|
||||
No categories found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{categories?.data?.map((category) => (
|
||||
<TableRow key={category.category_id}>
|
||||
<TableCell className="font-medium">
|
||||
|
||||
@@ -8,6 +8,14 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useBlogId } from "@/hooks/use-blog-id";
|
||||
import AppLayout, { Section } from "@/layouts/AppLayout";
|
||||
import {
|
||||
@@ -15,7 +23,7 @@ import {
|
||||
useTagsWithUsageQuery,
|
||||
useUpdateTagMutation,
|
||||
} from "@/queries/tags";
|
||||
import { MoreVertical, Pen, Trash } from "lucide-react";
|
||||
import { MoreHorizontal, MoreVertical, Pen, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { PiTag } from "react-icons/pi";
|
||||
import { toast } from "sonner";
|
||||
@@ -38,49 +46,46 @@ export default function TagsPage() {
|
||||
<AppLayout
|
||||
title="Tags"
|
||||
actions={<CreateTagDialog blogId={blogId} />}
|
||||
description="Tags help you group your posts. Posts can have multiple tags."
|
||||
description="Posts can have multiple tags."
|
||||
loading={tags.isLoading}
|
||||
>
|
||||
<Section>
|
||||
{tags.data?.length === 0 && (
|
||||
<div className="p-12 py-32 text-center">
|
||||
<div className="text-2xl">🏷️</div>
|
||||
<div className="text-lg text-zinc-500">Nothing here yet</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid divide-y">
|
||||
{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>
|
||||
<div className="text-right">Posts with tag</div>
|
||||
<div></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tags.data?.map((tag) => {
|
||||
return (
|
||||
<div
|
||||
key={tag.tag_id}
|
||||
className="grid grid-cols-4 items-center p-4 hover:bg-zinc-50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <PiTag className="text-orange-500" size="16" /> */}
|
||||
<span className="font-medium">{tag.tag_name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<p className="font-mono text-sm text-zinc-500">{tag.slug}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right font-mono">{tag.post_count}</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Tag</TableHead>
|
||||
<TableHead>Slug</TableHead>
|
||||
<TableHead className="text-right">Posts with tag</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<div className="sr-only">Action</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tags.data?.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center font-mono text-slate-500"
|
||||
>
|
||||
No tags found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{tags.data?.map((tag) => (
|
||||
<TableRow key={tag.tag_id}>
|
||||
<TableCell>{tag.tag_name}</TableCell>
|
||||
<TableCell className="font-mono text-slate-500">
|
||||
{tag.slug}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{tag.post_count}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant={"ghost"}>
|
||||
<MoreVertical size="16" />
|
||||
<MoreHorizontal size="16" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
@@ -93,7 +98,6 @@ export default function TagsPage() {
|
||||
setUpdateTagDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pen size="16" className="mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -105,16 +109,16 @@ export default function TagsPage() {
|
||||
setDeleteTagDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash size="16" className="mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<UpdateTagDialog
|
||||
tag={selectedTag}
|
||||
onSubmit={async (newTag) => {
|
||||
|
||||
@@ -42,8 +42,7 @@ export default function CreateBlog() {
|
||||
|
||||
const createBlog = useCreateBlogMutation();
|
||||
const blogsQuery = useBlogsQuery({ enabled: true });
|
||||
|
||||
const hasOneBlogAlready = blogsQuery.data?.length === 1;
|
||||
const totalBlogs = blogsQuery.data?.length;
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
// validate slug is url friendly, allow only lowercase letters, numbers and hyphens
|
||||
@@ -92,7 +91,7 @@ export default function CreateBlog() {
|
||||
}
|
||||
};
|
||||
|
||||
if (subscription?.status !== "active" && hasOneBlogAlready && !isLoading) {
|
||||
if (!subscription?.plan && totalBlogs === 1 && !isLoading) {
|
||||
return (
|
||||
<AppLayout loading={createBlog.isPending || createBlog.isSuccess}>
|
||||
<div className="section mx-auto my-12 max-w-xl py-12">
|
||||
|
||||
@@ -157,7 +157,7 @@ export function PricingCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl border bg-white px-5 py-3 pb-5",
|
||||
"flex h-full flex-col rounded-2xl border bg-white px-5 py-3 pb-5",
|
||||
highlight && "border-orange-500 ring-4 ring-orange-200"
|
||||
)}
|
||||
>
|
||||
@@ -165,7 +165,7 @@ export function PricingCard({
|
||||
<div className="h-16">
|
||||
<PricingText />
|
||||
</div>
|
||||
<ul className="mt-5 space-y-2 font-medium">
|
||||
<ul className="mt-5 h-full flex-1 space-y-2 font-medium">
|
||||
{features.map((feature) => (
|
||||
<li key={feature} className="flex items-center">
|
||||
<CheckIcon className="mr-2 h-4 w-4 text-orange-500" />
|
||||
@@ -174,6 +174,11 @@ export function PricingCard({
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-5">
|
||||
{monthlyPrice !== 0 && (
|
||||
<div className="mb-2 text-center font-mono text-xs text-slate-500">
|
||||
Try it free for {TRIAL_PERIOD_DAYS} days!
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="w-full"
|
||||
variant={getButtonVariant()}
|
||||
@@ -181,11 +186,6 @@ export function PricingCard({
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
{monthlyPrice !== 0 && (
|
||||
<div className="mt-2 text-center font-mono text-xs text-slate-500">
|
||||
Try it free for {TRIAL_PERIOD_DAYS} days!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ type Category = {
|
||||
blog_id: string;
|
||||
};
|
||||
|
||||
export function useCategoriesWithPostCount() {
|
||||
export function useCategoriesWithPostCount(blogId: string) {
|
||||
return useQuery({
|
||||
queryKey: keys.list,
|
||||
queryFn: async () =>
|
||||
@@ -25,17 +25,19 @@ export function useCategoriesWithPostCount() {
|
||||
.select(
|
||||
"category_id, category_name, category_slug, post_count, created_at"
|
||||
)
|
||||
.eq("blog_id", blogId)
|
||||
.throwOnError(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCategories() {
|
||||
export function useCategories(blogId: string) {
|
||||
return useQuery({
|
||||
queryKey: keys.list,
|
||||
queryFn: async () => {
|
||||
const { data, error } = await sb
|
||||
.from("categories")
|
||||
.select("id, slug, name, created_at")
|
||||
.eq("blog_id", blogId)
|
||||
.throwOnError();
|
||||
if (error) {
|
||||
throw error;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { PricingPlan, PricingPlanIntervalType } from "@/lib/pricing.constants";
|
||||
import { PricingPlan } from "@/lib/pricing.constants";
|
||||
import { createSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { QueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const SUBSCRIPTION_KEYS = ["subscription"];
|
||||
|
||||
/**
|
||||
* Returns the user's subscription data.
|
||||
* @returns
|
||||
* - plan: The user's subscription plan.
|
||||
* - interval: The user's subscription interval.
|
||||
* - status: The user's subscription status.
|
||||
* - isValidSubscription: Whether the user's subscription is valid (active, trialing, or past due).
|
||||
*/
|
||||
export function useSubscriptionQuery() {
|
||||
const sb = createSupabaseBrowserClient();
|
||||
const user = useUser();
|
||||
@@ -29,10 +36,16 @@ export function useSubscriptionQuery() {
|
||||
| Stripe.Plan.Interval
|
||||
| undefined;
|
||||
const status = res?.status;
|
||||
|
||||
const validSubscriptionStatus = ["active", "trialing", "past_due"];
|
||||
const isValidSubscription =
|
||||
status && validSubscriptionStatus.includes(status);
|
||||
|
||||
return {
|
||||
plan,
|
||||
interval,
|
||||
status,
|
||||
isValidSubscription,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user