This commit is contained in:
Jordi Enric
2024-10-28 23:21:24 +01:00
parent 37be2484ef
commit 901b512a0d
10 changed files with 107 additions and 79 deletions
+1 -1
View File
@@ -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;
+14 -14
View File
@@ -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}</>;
};
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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">
+49 -45
View File
@@ -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) => {
+2 -3
View File
@@ -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">
+7 -7
View File
@@ -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>
);
+4 -2
View File
@@ -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;
+16 -3
View File
@@ -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,
};
},
});