mirror of
https://github.com/jordienr/zenblog.git
synced 2026-08-24 10:14:46 -05:00
wip
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
import { type EmailOtpType } from "@supabase/supabase-js";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
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 redirectTo = request.nextUrl.clone();
|
||||
redirectTo.pathname = next;
|
||||
|
||||
if (token_hash && type) {
|
||||
const cookieStore = cookies();
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
cookieStore.set({ name, value, ...options });
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
cookieStore.delete({ name, ...options });
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
type,
|
||||
token_hash,
|
||||
});
|
||||
if (!error) {
|
||||
return NextResponse.redirect(redirectTo);
|
||||
}
|
||||
}
|
||||
|
||||
// return the user to an error page with some instructions
|
||||
redirectTo.pathname = "/auth/auth-code-error";
|
||||
return NextResponse.redirect(redirectTo);
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
"dev": "NODE_OPTIONS='--inspect' next dev -p 3000",
|
||||
"lint": "next lint",
|
||||
"start": "next start",
|
||||
"supabase:typegen": "npx supabase gen types typescript --project-id ppfseefimhneysnokffx --schema public > src/types/supabase.ts"
|
||||
"supabase:typegen": "npx supabase gen types typescript --local --schema public > src/types/supabase.ts",
|
||||
"stripe:sync": "tsx ./src/scripts/stripe-sync.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@builder.io/react-hydration-overlay": "^0.0.8",
|
||||
@@ -26,9 +27,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@stripe/react-stripe-js": "^2.4.0",
|
||||
"@stripe/stripe-js": "^2.4.0",
|
||||
"@supabase/auth-helpers-nextjs": "^0.8.1",
|
||||
"@supabase/auth-helpers-react": "^0.3.1",
|
||||
"@supabase/supabase-js": "^2.21.0",
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@supabase/supabase-js": "^2.39.6",
|
||||
"@t3-oss/env-nextjs": "^0.2.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
@@ -51,6 +51,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"formidable": "^3.5.1",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^10.12.12",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useUser } from "@supabase/auth-helpers-react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const AppChecks = (props: PropsWithChildren<Props>) => {
|
||||
// Global checks here
|
||||
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default AppChecks;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { Blog } from "@/lib/models/blogs/Blogs";
|
||||
import { useBlogQuery } from "@/queries/blogs";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import {
|
||||
BoldIcon,
|
||||
CodeIcon,
|
||||
Dot,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading5,
|
||||
Heading6,
|
||||
ItalicIcon,
|
||||
Link,
|
||||
ListIcon,
|
||||
Pilcrow,
|
||||
Strikethrough,
|
||||
} from "lucide-react";
|
||||
import { PiCodeBlock, PiListNumbers } from "react-icons/pi";
|
||||
|
||||
const SIZE = 18;
|
||||
|
||||
const Separator = {
|
||||
id: "separator",
|
||||
icon: <Dot size={SIZE} />,
|
||||
disabled: true,
|
||||
command: () => {},
|
||||
};
|
||||
|
||||
const BOLD_BTN = {
|
||||
id: "bold",
|
||||
tooltip: "Bold (Cmd+B)",
|
||||
icon: <BoldIcon size={SIZE} />,
|
||||
command: (editor: Editor) => editor?.chain().focus().toggleBold().run(),
|
||||
};
|
||||
|
||||
const ITALIC_BTN = {
|
||||
id: "italic",
|
||||
tooltip: "Italic (Cmd+I)",
|
||||
icon: <ItalicIcon size={SIZE} />,
|
||||
command: (editor: Editor) => editor?.chain().focus().toggleItalic().run(),
|
||||
};
|
||||
|
||||
const STRIKETHROUGH_BTN = {
|
||||
id: "strike",
|
||||
tooltip: "Strikethrough",
|
||||
icon: <Strikethrough size={SIZE} />,
|
||||
command: (editor: Editor) => editor?.chain().focus().toggleStrike().run(),
|
||||
};
|
||||
|
||||
const CODE_BTN = {
|
||||
id: "code",
|
||||
tooltip: "Code (Cmd+E)",
|
||||
icon: <CodeIcon size={SIZE} />,
|
||||
command: (editor: Editor) => editor?.chain().focus().toggleCode().run(),
|
||||
};
|
||||
|
||||
const CODE_BLOCK_BTN = {
|
||||
id: "codeBlock",
|
||||
tooltip: "Code Block",
|
||||
icon: <PiCodeBlock size={SIZE} />,
|
||||
command: (editor: Editor) => editor?.chain().focus().toggleCodeBlock().run(),
|
||||
};
|
||||
|
||||
const LINK_BTN = {
|
||||
id: "link",
|
||||
tooltip: "Link",
|
||||
icon: <Link size={SIZE} />,
|
||||
command: (editor: Editor) => {
|
||||
const url = window.prompt("Enter the URL");
|
||||
if (url) {
|
||||
editor?.chain().focus().setLink({ href: url }).run();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const LIST_BTN = {
|
||||
id: "list",
|
||||
tooltip: "List",
|
||||
icon: <ListIcon size={SIZE} />,
|
||||
command: (editor: Editor) => editor?.chain().focus().toggleBulletList().run(),
|
||||
};
|
||||
|
||||
const NUMBERED_LIST_BTN = {
|
||||
id: "numberedList",
|
||||
tooltip: "Numbered List",
|
||||
icon: <PiListNumbers size={SIZE} />,
|
||||
command: (editor: Editor) =>
|
||||
editor?.chain().focus().toggleOrderedList().run(),
|
||||
};
|
||||
|
||||
const PARAGRAPH_BTN = {
|
||||
id: "paragraph",
|
||||
icon: <Pilcrow size={SIZE} />,
|
||||
label: "Paragraph",
|
||||
command: (editor: Editor) => editor?.chain().focus().setParagraph().run(),
|
||||
};
|
||||
|
||||
const HEADING2_BTN = {
|
||||
id: "heading2",
|
||||
icon: <Heading2 size={SIZE} />,
|
||||
label: "Heading 2",
|
||||
command: (editor: Editor) =>
|
||||
editor?.chain().focus().setHeading({ level: 2 }).run(),
|
||||
};
|
||||
|
||||
const HEADING3_BTN = {
|
||||
icon: <Heading3 size={SIZE} />,
|
||||
label: "Heading 3",
|
||||
command: (editor: Editor) =>
|
||||
editor?.chain().focus().setHeading({ level: 3 }).run(),
|
||||
};
|
||||
|
||||
const HEADING4_BTN = {
|
||||
icon: <Heading4 size={SIZE} />,
|
||||
label: "Heading 4",
|
||||
command: (editor: Editor) =>
|
||||
editor?.chain().focus().setHeading({ level: 4 }).run(),
|
||||
};
|
||||
|
||||
const HEADING5_BTN = {
|
||||
icon: <Heading5 size={SIZE} />,
|
||||
label: "Heading 5",
|
||||
command: (editor: Editor) =>
|
||||
editor?.chain().focus().setHeading({ level: 5 }).run(),
|
||||
};
|
||||
|
||||
const HEADING6_BTN = {
|
||||
icon: <Heading6 size={SIZE} />,
|
||||
label: "Heading 6",
|
||||
command: (editor: Editor) =>
|
||||
editor?.chain().focus().setHeading({ level: 6 }).run(),
|
||||
};
|
||||
|
||||
export const TOP_MENU_BUTTONS = [
|
||||
BOLD_BTN,
|
||||
ITALIC_BTN,
|
||||
STRIKETHROUGH_BTN,
|
||||
CODE_BTN,
|
||||
CODE_BLOCK_BTN,
|
||||
Separator,
|
||||
LINK_BTN,
|
||||
LIST_BTN,
|
||||
NUMBERED_LIST_BTN,
|
||||
];
|
||||
|
||||
export const BUBBLE_MENU_BUTTONS = [
|
||||
BOLD_BTN,
|
||||
ITALIC_BTN,
|
||||
STRIKETHROUGH_BTN,
|
||||
CODE_BTN,
|
||||
CODE_BLOCK_BTN,
|
||||
Separator,
|
||||
LINK_BTN,
|
||||
LIST_BTN,
|
||||
NUMBERED_LIST_BTN,
|
||||
];
|
||||
|
||||
export const MENU_TYPE_BUTTONS = [
|
||||
PARAGRAPH_BTN,
|
||||
HEADING2_BTN,
|
||||
HEADING3_BTN,
|
||||
HEADING4_BTN,
|
||||
HEADING5_BTN,
|
||||
HEADING6_BTN,
|
||||
];
|
||||
|
||||
export const NEW_LINE_BUTTONS = [
|
||||
HEADING2_BTN,
|
||||
HEADING3_BTN,
|
||||
HEADING4_BTN,
|
||||
HEADING5_BTN,
|
||||
HEADING6_BTN,
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { BubbleMenu, Editor, FloatingMenu } from "@tiptap/react";
|
||||
import {
|
||||
BoldIcon,
|
||||
CodeIcon,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} from "../ui/tooltip";
|
||||
import { NEW_LINE_BUTTONS, TOP_MENU_BUTTONS } from "./Editor.constants";
|
||||
|
||||
function EditorMenuButton({
|
||||
children,
|
||||
@@ -123,13 +124,13 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
active: editor?.isActive("link"),
|
||||
},
|
||||
{
|
||||
tooltip: "List",
|
||||
tooltip: "Bullet list",
|
||||
icon: <ListIcon size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleBulletList().run(),
|
||||
active: editor?.isActive("bulletList"),
|
||||
},
|
||||
{
|
||||
tooltip: "Numbered List",
|
||||
tooltip: "Numbered list",
|
||||
icon: <PiListNumbers size={SIZE} />,
|
||||
command: () => editor?.chain().focus().toggleOrderedList().run(),
|
||||
active: editor?.isActive("orderedList"),
|
||||
@@ -210,6 +211,48 @@ export function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
{icon}
|
||||
</EditorMenuButton>
|
||||
))}
|
||||
|
||||
{editor && (
|
||||
<BubbleMenu
|
||||
className="flex items-center gap-0.5 rounded-xl bg-zinc-800 p-1.5 text-xs text-white shadow-md"
|
||||
tippyOptions={{ duration: 100 }}
|
||||
editor={editor}
|
||||
>
|
||||
{TOP_MENU_BUTTONS.map(({ icon, command, id }, i) => (
|
||||
<button
|
||||
className={cn("rounded-md p-1 text-xs text-white", {
|
||||
"hover:bg-zinc-600": !editor.isActive(id),
|
||||
"bg-zinc-600": editor.isActive(id),
|
||||
})}
|
||||
key={i + "menu-btn"}
|
||||
onClick={() => command(editor)}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
))}
|
||||
</BubbleMenu>
|
||||
)}
|
||||
|
||||
{/* {editor && (
|
||||
<FloatingMenu
|
||||
className="rounded-md bg-zinc-50 p-0.5"
|
||||
tippyOptions={{ duration: 100 }}
|
||||
editor={editor}
|
||||
>
|
||||
{NEW_LINE_BUTTONS.map((item) => {
|
||||
const { icon, label, command } = item;
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
onClick={() => command(editor)}
|
||||
className="rounded-md p-0.5 text-xs text-zinc-400 "
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</FloatingMenu>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { Plus, Trash } from "lucide-react";
|
||||
import { Code, Info, Plus, Trash } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
|
||||
|
||||
type MetadataItem = {
|
||||
key: string;
|
||||
@@ -9,7 +10,13 @@ type MetadataItem = {
|
||||
};
|
||||
type Props = {
|
||||
metadata?: MetadataItem[];
|
||||
onSave: (metadata: MetadataItem[]) => void;
|
||||
onChange: ({
|
||||
metadata,
|
||||
categories,
|
||||
}: {
|
||||
metadata: MetadataItem[];
|
||||
categories: string[];
|
||||
}) => void;
|
||||
};
|
||||
|
||||
const EditorSettings = (props: Props) => {
|
||||
@@ -23,56 +30,79 @@ const EditorSettings = (props: Props) => {
|
||||
setMetadata([...metadata, { key: "", value: "" }]);
|
||||
}
|
||||
|
||||
const handleMetadataChange = (
|
||||
type: "key" | "value",
|
||||
index: number,
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
let newMetadata = [...metadata];
|
||||
|
||||
if (!newMetadata[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
newMetadata[index]![type] = event.target.value;
|
||||
setMetadata(newMetadata);
|
||||
props.onChange({ metadata: newMetadata, categories: [] });
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const data = formData.values();
|
||||
const newMetadata = [];
|
||||
|
||||
for (let i = 0; i < metadata.length; i++) {
|
||||
const key = data.next().value;
|
||||
const value = data.next().value;
|
||||
if (key && value) {
|
||||
newMetadata.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
console.log("DEBUG", newMetadata);
|
||||
|
||||
setMetadata(newMetadata);
|
||||
props.onSave(metadata);
|
||||
}}
|
||||
className="prose-sm prose-h2:font-bold prose-h2:text-sm"
|
||||
>
|
||||
<div className="prose-sm prose-h2:font-bold prose-h2:text-sm">
|
||||
<section>
|
||||
<h2 className="m-0 border-b pb-2">Custom metadata</h2>
|
||||
|
||||
<div className="text-sm font-medium text-slate-700">
|
||||
<div className="grid grid-cols-2 text-xs text-slate-500 *:p-1">
|
||||
<div>Key</div>
|
||||
<div className="flex items-center py-2 text-xs text-slate-500 *:p-1">
|
||||
<div className="w-48">Key</div>
|
||||
<div>Value</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="ml-auto" variant={"outline"} size={"sm"}>
|
||||
<Code size={16} />
|
||||
Preview
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<div>
|
||||
<h2 className="flex items-center gap-1 text-lg font-medium">
|
||||
<Info size={16} />
|
||||
Metadata preview
|
||||
</h2>
|
||||
<p className="text-sm text-zinc-500">
|
||||
This is the object that you will receive when you fetch the
|
||||
post from your website.
|
||||
</p>
|
||||
</div>
|
||||
<pre className="rounded-lg border p-2 text-zinc-600">
|
||||
<code>{JSON.stringify(metadata, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{metadata.map((item, index) => (
|
||||
<div className="flex gap-1" key={`${item.key}-metadata-${index}`}>
|
||||
<div className="flex gap-1" key={`metadata-${index}`}>
|
||||
<div className="grid flex-grow grid-cols-2 gap-1 *:rounded-md *:border *:p-1">
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="key"
|
||||
id={index + "-metadata-key"}
|
||||
name={index + "-metadata-key"}
|
||||
name={"key"}
|
||||
className="outline-none"
|
||||
defaultValue={item.value}
|
||||
value={item.key}
|
||||
required
|
||||
onChange={(e) => handleMetadataChange("key", index, e)}
|
||||
/>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="value"
|
||||
id={index + "-metadata-value"}
|
||||
name={index + "-metadata-value"}
|
||||
name={"value"}
|
||||
className="outline-none"
|
||||
defaultValue={item.value}
|
||||
value={item.value}
|
||||
required
|
||||
onChange={(e) => handleMetadataChange("value", index, e)}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink">
|
||||
@@ -86,6 +116,7 @@ const EditorSettings = (props: Props) => {
|
||||
const newMetadata = [...metadata];
|
||||
newMetadata.splice(index, 1);
|
||||
setMetadata(newMetadata);
|
||||
props.onChange({ metadata: newMetadata, categories: [] });
|
||||
}}
|
||||
>
|
||||
<Trash size={16} />
|
||||
@@ -110,12 +141,8 @@ const EditorSettings = (props: Props) => {
|
||||
<div>
|
||||
<h2>Categories</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { Sheet, SheetContent, SheetTrigger } from "../ui/sheet";
|
||||
import EditorSettings from "./EditorSettings";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { useSubscriptionQuery } from "@/queries/subscription";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string(),
|
||||
@@ -52,6 +53,7 @@ type EditorContent = {
|
||||
slug: string;
|
||||
cover_image?: string;
|
||||
published: boolean;
|
||||
metadata?: any;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -64,7 +66,7 @@ type Props = {
|
||||
published: boolean;
|
||||
cover_image?: string;
|
||||
content?: any;
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: any;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,6 +82,12 @@ export const ZendoEditor = (props: Props) => {
|
||||
});
|
||||
const router = useRouter();
|
||||
const blogId = (router.query.blogId as string) || "demo";
|
||||
const subscription = useSubscriptionQuery();
|
||||
|
||||
const [metadata, setMetadata] = React.useState(props.post?.metadata || []);
|
||||
|
||||
const isSubscribed = subscription.data?.status === "active";
|
||||
|
||||
const [coverImgUrl, setCoverImgUrl] = React.useState<string | undefined>(
|
||||
props.post?.cover_image || ""
|
||||
);
|
||||
@@ -136,6 +144,10 @@ export const ZendoEditor = (props: Props) => {
|
||||
});
|
||||
|
||||
const formSubmit = handleSubmit(async (data) => {
|
||||
if (!isSubscribed) {
|
||||
alert("You need an active subscription to publish more posts.");
|
||||
return;
|
||||
}
|
||||
const content = editor?.getJSON() || {};
|
||||
const slugHasChanged = data.slug !== props.post?.slug;
|
||||
|
||||
@@ -145,6 +157,7 @@ export const ZendoEditor = (props: Props) => {
|
||||
slug: data.slug,
|
||||
cover_image: data.cover_image || "",
|
||||
published: data.published,
|
||||
metadata,
|
||||
});
|
||||
|
||||
if (slugHasChanged) {
|
||||
@@ -158,7 +171,25 @@ export const ZendoEditor = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-zinc-50 pb-40">
|
||||
<div className="bg-zinc-50 pb-24">
|
||||
{!isSubscribed && (
|
||||
<>
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center overflow-hidden bg-zinc-100/80">
|
||||
<div className="max-w-xs rounded-lg border bg-white p-3 shadow-sm">
|
||||
<span className="text-lg">🙏</span>
|
||||
<h2 className="text-lg font-medium">
|
||||
You need an active subscription to publish more posts.
|
||||
</h2>
|
||||
<Link
|
||||
className="mt-4 inline-block py-2 text-orange-500 underline"
|
||||
href="/account"
|
||||
>
|
||||
Manage your subscription
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<form
|
||||
onSubmit={formSubmit}
|
||||
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"
|
||||
@@ -259,8 +290,9 @@ export const ZendoEditor = (props: Props) => {
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<EditorSettings
|
||||
onSave={() => {
|
||||
// TO DO - save settings
|
||||
metadata={metadata}
|
||||
onChange={(data) => {
|
||||
setMetadata(data.metadata);
|
||||
}}
|
||||
></EditorSettings>
|
||||
</SheetContent>
|
||||
@@ -271,7 +303,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 pb-40">
|
||||
<div className="mx-auto mt-2 flex w-full max-w-2xl flex-col px-2 pb-6">
|
||||
<div className="relative mt-2 flex items-center justify-center bg-zinc-100">
|
||||
{coverImgUrl && (
|
||||
<button
|
||||
@@ -334,14 +366,14 @@ export const ZendoEditor = (props: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="group">
|
||||
<div className="sticky top-10 z-30">
|
||||
<div className="sticky top-12 z-30">
|
||||
<EditorMenu editor={editor} />
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
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-normal transition-all"
|
||||
>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toast } from "sonner";
|
||||
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||
import { getSupabaseClient } from "../../lib/supabase";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
|
||||
const uploadKey = new PluginKey("upload-image");
|
||||
|
||||
@@ -10,7 +10,6 @@ const UploadImagesPlugin = () =>
|
||||
key: uploadKey,
|
||||
state: {
|
||||
init() {
|
||||
console.log("init");
|
||||
return DecorationSet.empty;
|
||||
},
|
||||
apply(tr, set) {
|
||||
@@ -59,7 +58,6 @@ function findPlaceholder(state: EditorState, id: {}) {
|
||||
const decos = uploadKey.getState(state);
|
||||
const found = decos.find(null, null, (spec: any) => spec.id == id);
|
||||
const pos = found.length ? found[0].from : null;
|
||||
console.log(pos);
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -134,7 +132,7 @@ export function startImageUpload(
|
||||
});
|
||||
}
|
||||
|
||||
const supa = getSupabaseClient();
|
||||
const supa = getSupabaseBrowserClient();
|
||||
|
||||
export const handleImageUpload = async (file: File, blogId: string) => {
|
||||
console.log("Uploading image to Supabase storage...");
|
||||
|
||||
@@ -8,12 +8,12 @@ import { Label } from "./ui/label";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import { Button } from "./ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const Feedback = (props: Props) => {
|
||||
const sb = useSupabaseClient();
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -11,10 +11,10 @@ const Footer = (props: Props) => {
|
||||
label: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
label: "Blog",
|
||||
href: "/blog",
|
||||
},
|
||||
// {
|
||||
// label: "Blog",
|
||||
// href: "/blog",
|
||||
// },
|
||||
{
|
||||
label: "Docs",
|
||||
href: "/docs/getting-started",
|
||||
@@ -26,7 +26,7 @@ const Footer = (props: Props) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="mt-32 border-t bg-zinc-100 p-12 text-xs text-zinc-700">
|
||||
<footer className="mt-6 border-t bg-zinc-100 p-12 text-xs text-zinc-700">
|
||||
<div className="mx-auto flex max-w-4xl justify-between">
|
||||
<div>
|
||||
<ZendoLogo />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { PropsWithChildren, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -13,12 +13,19 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog";
|
||||
import { BlogImage } from "@/lib/types/BlogImage";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { Button } from "../ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type BlogImage = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function ImagePicker({
|
||||
children,
|
||||
onSelect,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import imageCompression from "browser-image-compression";
|
||||
import { Loader } from "lucide-react";
|
||||
import { Input } from "../ui/input";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
|
||||
type Props = {
|
||||
blogId: string;
|
||||
@@ -17,7 +17,7 @@ export const ImageUploader = ({ blogId, onSuccessfulUpload }: Props) => {
|
||||
const [imageInfo, setImageInfo] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const supa = useSupabaseClient();
|
||||
const supa = getSupabaseBrowserClient();
|
||||
|
||||
useEffect(() => {
|
||||
// on mount, listen for paste events
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useUser } from "@supabase/auth-helpers-react";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export function LoggedInUser({ children }: PropsWithChildren) {
|
||||
const sb = getSupabaseBrowserClient();
|
||||
const user = useUser();
|
||||
|
||||
if (!user?.id) {
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCreateTeamMutation, useTeamsQuery } from "@/queries/teams";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { Loader } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { PropsWithChildren, useEffect } from "react";
|
||||
|
||||
type Props = {};
|
||||
|
||||
export const GlobalAppLoading = () => {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-zinc-50">
|
||||
<Loader className="animate-spin text-orange-500" size={32} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoggedInUserChecks = (props: PropsWithChildren<Props>) => {
|
||||
// Check notifications and other user related stuff here
|
||||
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default LoggedInUserChecks;
|
||||
@@ -6,15 +6,14 @@ import {
|
||||
DropdownMenuItem,
|
||||
} from "./ui/dropdown-menu";
|
||||
import { Button } from "./ui/button";
|
||||
import { IoNotificationsCircle } from "react-icons/io5";
|
||||
import { Bell, Loader } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getSupabaseClient } from "@/lib/supabase";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
|
||||
type Props = {};
|
||||
|
||||
function useNotifications() {
|
||||
const sb = getSupabaseClient();
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["notifications"],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useUser } from "@supabase/auth-helpers-react";
|
||||
import React from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -8,8 +7,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { Button } from "./ui/button";
|
||||
import { User2 } from "lucide-react";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
|
||||
type Props = {};
|
||||
|
||||
@@ -19,7 +17,7 @@ const UserButton = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger className="rounded-full">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-slate-800 font-bold text-white">
|
||||
{user?.email?.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import React from "react";
|
||||
import ZendoLogo from "../ZendoLogo";
|
||||
import Link from "next/link";
|
||||
import { LoggedInUser } from "../LoggedInUser";
|
||||
import { FaTwitter } from "react-icons/fa";
|
||||
import { useUser } from "@supabase/auth-helpers-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
|
||||
type Props = {};
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1.5 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 shadow-sm",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1.5 min-w-[8rem] overflow-hidden rounded-lg border bg-white p-1 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm text-slate-600 outline-none transition-colors hover:bg-slate-50 hover:text-slate-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm text-zinc-500 outline-none transition-colors hover:bg-zinc-100 hover:text-zinc-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"border-input bg-background placeholder:text-muted-foreground flex h-9 w-full rounded-lg border px-2 py-1.5 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium hover:border-orange-300 focus-visible:border-orange-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-200 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input bg-background placeholder:text-muted-foreground flex h-8 w-full rounded-xl border px-2 py-1.5 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium hover:border-orange-300 focus-visible:border-orange-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-200 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -1,65 +1,95 @@
|
||||
import UserButton from "@/components/UserButton";
|
||||
import ZendoLogo from "@/components/ZendoLogo";
|
||||
import { Github, Twitter } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import Notifications from "@/components/Notifications";
|
||||
import Feedback from "@/components/Feedback";
|
||||
import { IoLogoGithub, IoLogoTwitter } from "react-icons/io5";
|
||||
import Footer from "@/components/Footer";
|
||||
import { useIsSubscribed, useSubscriptionQuery } from "@/queries/subscription";
|
||||
import { useIsSubscribed } from "@/queries/subscription";
|
||||
import AppChecks from "@/components/LoggedInUserChecks";
|
||||
import { Loader } from "lucide-react";
|
||||
import { HiOutlineInformationCircle } from "react-icons/hi";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
};
|
||||
export default function AppLayout({ children }: Props) {
|
||||
export default function AppLayout({ children, loading = false }: Props) {
|
||||
const isSubscribed = useIsSubscribed();
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user && !loading) {
|
||||
router.push("/sign-in");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`flex min-h-screen flex-col border-b bg-zinc-50 font-sans`}>
|
||||
<nav className="sticky top-0 z-20 mx-auto w-full max-w-5xl border-b bg-zinc-50">
|
||||
<div className="mx-auto flex h-full items-center justify-between p-4">
|
||||
<div className="z-20 flex h-full items-center gap-2">
|
||||
<Link href="/blogs" className="rounded-md px-1 text-lg font-medium">
|
||||
<ZendoLogo />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Feedback />
|
||||
<Link
|
||||
className="rounded-full px-3 py-1 text-sm font-medium text-slate-600 hover:bg-orange-50 hover:text-orange-600"
|
||||
href="/docs/getting-started"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Notifications />
|
||||
<span className="ml-2">
|
||||
<UserButton />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isSubscribed && (
|
||||
<div className="bg-red-500">
|
||||
<div className="mx-auto max-w-5xl p-4 text-center font-medium text-white">
|
||||
<p>
|
||||
You are not subscribed to a plan. Please{" "}
|
||||
<Link className="underline" href="/account">
|
||||
subscribe to a plan
|
||||
</Link>{" "}
|
||||
to keep using Zenblog.
|
||||
</p>
|
||||
<AppChecks>
|
||||
<nav className="sticky top-0 z-20 mx-auto w-full max-w-5xl border-b bg-zinc-50">
|
||||
<div className="mx-auto flex h-full items-center justify-between p-4">
|
||||
<div className="z-20 flex h-full items-center gap-2">
|
||||
<Link
|
||||
href="/blogs"
|
||||
className="rounded-md px-1 text-lg font-medium"
|
||||
>
|
||||
<ZendoLogo />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Feedback />
|
||||
<Link
|
||||
className="rounded-full px-3 py-1 text-sm font-medium text-slate-600 hover:bg-orange-50 hover:text-orange-600"
|
||||
href="/docs/getting-started"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Notifications />
|
||||
<span className="ml-2">
|
||||
<UserButton />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{!isSubscribed && (
|
||||
<div className="sticky top-[67px] z-30 flex items-center justify-center">
|
||||
<Link
|
||||
href="/account"
|
||||
className="
|
||||
mx-auto flex max-w-5xl items-center rounded-b-2xl border-x border-b-2 border-yellow-400 bg-yellow-100
|
||||
p-1.5 px-4 text-center font-medium text-yellow-600"
|
||||
>
|
||||
<HiOutlineInformationCircle
|
||||
className="mr-1 text-yellow-600"
|
||||
size={20}
|
||||
/>
|
||||
You don't have an active subscription. Please{" "}
|
||||
<span className="ml-1.5 underline"> subscribe to a plan</span>.
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="min-h-screen bg-zinc-50 pb-24"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
<Footer />
|
||||
{loading ? (
|
||||
<div className="flex h-[600px] items-center justify-center">
|
||||
<Loader className="animate-spin text-orange-500" size={32} />
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="min-h-screen bg-zinc-50 pb-24"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
<Footer />
|
||||
</AppChecks>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AuthError, AuthUser, User } from "@supabase/supabase-js";
|
||||
import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs";
|
||||
|
||||
type UseAuthStatus = {
|
||||
loading: boolean;
|
||||
user: User | null;
|
||||
error: AuthError | null;
|
||||
};
|
||||
|
||||
export function useAuth() {
|
||||
const [supabaseClient] = useState(() => createPagesBrowserClient());
|
||||
const [isSignedIn, setIsSignedIn] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
supabaseClient.auth.getUser().then(({ data, error }) => {
|
||||
if (data) {
|
||||
setIsSignedIn(true);
|
||||
setUser(data.user);
|
||||
} else {
|
||||
setIsSignedIn(false);
|
||||
setUser(null);
|
||||
}
|
||||
});
|
||||
|
||||
const { data: authListener } = supabaseClient.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
if (event === "SIGNED_IN") {
|
||||
setIsSignedIn(true);
|
||||
}
|
||||
if (event === "SIGNED_OUT") {
|
||||
setIsSignedIn(false);
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
authListener?.subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
auth: supabaseClient.auth,
|
||||
isSignedIn,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL as string;
|
||||
@@ -215,7 +215,6 @@ export function createAPIClient() {
|
||||
const body = new FormData();
|
||||
body.append("file", file);
|
||||
|
||||
console.log("APIC: ", body);
|
||||
return _fetch(
|
||||
`/upload`,
|
||||
{ method: "POST", body: JSON.stringify(body) },
|
||||
@@ -13,7 +13,8 @@ export const getPostBySlugRes = z.object({
|
||||
updated_at: z.string(),
|
||||
blog_id: z.string(),
|
||||
user_id: z.string(),
|
||||
cover_image: z.string().nullable(),
|
||||
cover_image: z.string().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
|
||||
export const getPostsRes = z.object({
|
||||
@@ -47,4 +48,5 @@ export const PatchPost = z.object({
|
||||
content: z.any(),
|
||||
cover_image: z.string().nullable(),
|
||||
published: z.boolean(),
|
||||
metadata: z.any().nullable(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSupabaseClient } from "../../supabase";
|
||||
|
||||
export async function getApiClientDB(
|
||||
req: NextApiRequest,
|
||||
@@ -7,7 +7,7 @@ export async function getApiClientDB(
|
||||
) {
|
||||
const key = req.headers["znd-api-key"];
|
||||
|
||||
const db = getSupabaseClient();
|
||||
const db = getSupabaseBrowserClient();
|
||||
|
||||
const { data, error } = await db
|
||||
.from("api_keys")
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Database } from "@/types/supabase";
|
||||
import { createPagesServerClient } from "@supabase/auth-helpers-nextjs";
|
||||
import {
|
||||
createServerClient,
|
||||
type CookieOptions,
|
||||
serialize,
|
||||
} from "@supabase/ssr";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export async function getServerClient(
|
||||
@@ -7,15 +11,28 @@ export async function getServerClient(
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const supabaseServerClient = createPagesServerClient<Database>({
|
||||
req,
|
||||
res,
|
||||
});
|
||||
const supabase = createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return req.cookies[name];
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
res.appendHeader("Set-Cookie", serialize(name, value, options));
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
res.appendHeader("Set-Cookie", serialize(name, "", options));
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const userRes = await supabaseServerClient.auth.getUser();
|
||||
const userRes = await supabase.auth.getUser();
|
||||
return {
|
||||
user: userRes?.data.user,
|
||||
db: supabaseServerClient,
|
||||
db: supabase,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// ONLY FOR DEVELOPMENT
|
||||
export const STRIPE_CONSTANTS = {
|
||||
products: {
|
||||
proPlan: {
|
||||
productId: "prod_PXkoPOxUafT0Ig",
|
||||
monthlyPriceId: "price_1OinIFJfDYgxbs7Z6AQ8toxS",
|
||||
yearlyPriceId: "price_1OifJ1JfDYgxbs7ZfZ0grcxW",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,8 +22,6 @@ export async function createOrRetrieveCustomer({
|
||||
query: "metadata['userId']:'" + userId + "'",
|
||||
});
|
||||
|
||||
console.log(customers.data);
|
||||
|
||||
if (customers.data.length > 0) {
|
||||
const customer = customers.data[0];
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Database } from "@/types/supabase";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import {
|
||||
createServerClient as _createServerClient,
|
||||
type CookieOptions,
|
||||
} from "@supabase/ssr";
|
||||
import { type cookies } from "next/headers";
|
||||
|
||||
export function createAdminClient() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
@@ -13,3 +18,35 @@ export function createAdminClient() {
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export function createServerClient(cookieStore: ReturnType<typeof cookies>) {
|
||||
return _createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value, ...options });
|
||||
} catch (error) {
|
||||
// The `set` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value: "", ...options });
|
||||
} catch (error) {
|
||||
// The `delete` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "@/types/supabase";
|
||||
import { env } from "@/env.mjs";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
|
||||
const supabaseUrl = env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
@@ -9,11 +9,11 @@ if (!supabaseKey) {
|
||||
throw new Error("Missing supabaseKey");
|
||||
}
|
||||
|
||||
export function getSupabaseClient() {
|
||||
export function getSupabaseBrowserClient() {
|
||||
if (!supabaseKey) {
|
||||
throw new Error("Missing supabaseKey");
|
||||
}
|
||||
const supabase = createClient<Database>(supabaseUrl, supabaseKey);
|
||||
const supabase = createBrowserClient<Database>(supabaseUrl, supabaseKey);
|
||||
|
||||
return supabase;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export type BlogImage = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -8,12 +8,10 @@ import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { Toaster } from "sonner";
|
||||
import { useState } from "react";
|
||||
import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { SessionContextProvider } from "@supabase/auth-helpers-react";
|
||||
import AppChecks from "@/components/AppChecks";
|
||||
import AppChecks from "@/components/LoggedInUserChecks";
|
||||
import { UserProvider } from "@/utils/supabase/browser";
|
||||
|
||||
// Fonts
|
||||
const inter = Inter({
|
||||
@@ -33,31 +31,19 @@ const ibmPlexMono = IBM_Plex_Mono({
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const { pathname } = useRouter();
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [supabaseClient] = useState(() =>
|
||||
createPagesBrowserClient({
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${ibmPlexMono.variable} ${inter.variable}`}>
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<UserProvider>
|
||||
<PlausibleProvider domain="zendo.blog">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Hydrate state={pageProps.dehydratedState}>
|
||||
<AppChecks>
|
||||
<Component key={pathname} {...pageProps} />
|
||||
<Toaster />
|
||||
</AppChecks>
|
||||
<Component key={pathname} {...pageProps} />
|
||||
<Toaster />
|
||||
</Hydrate>
|
||||
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</QueryClientProvider>
|
||||
</PlausibleProvider>
|
||||
</SessionContextProvider>
|
||||
</UserProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,170 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsTrigger, TabsList } from "@/components/ui/tabs";
|
||||
import AppLayout from "@/layouts/AppLayout";
|
||||
import { usePricesQuery } from "@/queries/prices";
|
||||
import { useProductsQuery } from "@/queries/products";
|
||||
import { useSubscriptionQuery } from "@/queries/subscription";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { useTeamsQuery } from "@/queries/teams";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { Landmark, Loader } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Stripe from "stripe";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const AccountPage = (props: Props) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
export const SubscribeSection = () => {
|
||||
const products = useProductsQuery();
|
||||
const prices = usePricesQuery();
|
||||
const [interval, setInterval] = React.useState<"year" | "month">("year");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
async function onSubscribeClick() {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/create-checkout-session", {
|
||||
function openCheckoutPage(product_id: string) {
|
||||
setIsLoading(true);
|
||||
|
||||
const pricesForProduct = prices.data?.filter(
|
||||
(p) => p.price.product === product_id
|
||||
);
|
||||
if (!pricesForProduct) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const price = pricesForProduct.find(
|
||||
(p) => p.price.recurring?.interval === interval
|
||||
);
|
||||
|
||||
if (!price) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = fetch("/api/create-checkout-session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ plan: "pro" }),
|
||||
body: JSON.stringify({
|
||||
price_id: price.price.id,
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
res.then(async (res) => {
|
||||
const json = await res.json();
|
||||
if (json.error) {
|
||||
console.error(json.error);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json.error) {
|
||||
console.error(json.error);
|
||||
if (json.url) {
|
||||
window.location.href = json.url;
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error("Error creating session");
|
||||
console.error("Error creating session");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const loading = products.isLoading || prices.isLoading || isLoading;
|
||||
|
||||
function formatAmount(price: number) {
|
||||
if (!price) {
|
||||
return "";
|
||||
}
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(price / 100);
|
||||
}
|
||||
|
||||
function getAmountFromProduct(prodId: string) {
|
||||
const price = prices.data?.find(
|
||||
(p) =>
|
||||
p.price.product === prodId && p.price.recurring?.interval === interval
|
||||
);
|
||||
if (!price) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(json);
|
||||
if (json.url) {
|
||||
window.location.href = json.url;
|
||||
} else {
|
||||
console.error("Error creating session");
|
||||
const amount = price.price.unit_amount;
|
||||
if (!amount) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
return formatAmount(amount);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center py-24">
|
||||
<Loader className="animate-spin text-orange-500" size={24} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">Pick a subscription</h2>
|
||||
<p className="text-zinc-500">You can cancel anytime</p>
|
||||
|
||||
<Tabs
|
||||
className="mt-4"
|
||||
value={interval}
|
||||
onValueChange={setInterval as any}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="year">
|
||||
Yearly
|
||||
<span className="ml-2 rounded-full bg-orange-100 px-1.5 py-0.5 text-xs text-orange-600">
|
||||
Best
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="month">Monthly</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-4 ">
|
||||
{products.data?.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="relative max-w-sm rounded-xl border border-b-2 border-orange-500 p-3"
|
||||
>
|
||||
{interval === "year" && (
|
||||
<span className="absolute -top-3 right-4 rounded-full border border-b-2 border-orange-500 bg-orange-100 px-2 py-1 text-xs font-medium text-orange-600">
|
||||
2 Months Free!
|
||||
</span>
|
||||
)}
|
||||
<h3 className="text-lg font-semibold">{product.product.name}</h3>
|
||||
<p className="text-zinc-500">{product.product.description}</p>
|
||||
<ul>
|
||||
{product.product.features.map((feat, featIdx) => (
|
||||
<li key={product.id + "-feat-" + featIdx}>{feat.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h4 className="mt-4 text-sm font-bold text-zinc-500">Price</h4>
|
||||
<div className="text-xl font-semibold">
|
||||
{getAmountFromProduct(product.product.id)}
|
||||
<span className="text-sm text-zinc-400">/{interval}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button onClick={() => openCheckoutPage(product.product.id)}>
|
||||
Subscribe to {product.product.name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountPage = (props: Props) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const user = useUser();
|
||||
const teams = useTeamsQuery();
|
||||
|
||||
async function onManageSubscriptionClick() {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/customer-portal", {
|
||||
@@ -45,26 +174,55 @@ const AccountPage = (props: Props) => {
|
||||
const json = await response.json();
|
||||
|
||||
if (json.error) {
|
||||
toast.error(json.error);
|
||||
console.error(json.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json.session) {
|
||||
window.location.href = json.session;
|
||||
} else {
|
||||
toast.error(json.error);
|
||||
console.error("Error creating session");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const subscription = useSubscriptionQuery();
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppLayout loading={loading || subscription.isLoading}>
|
||||
<div className="mx-auto max-w-5xl px-4 py-12">
|
||||
<h1 className="text-xl font-medium">Account settings</h1>
|
||||
<section className="my-4 rounded-xl border bg-white p-4 shadow-sm">
|
||||
|
||||
<section className="my-4 rounded-xl border border-b-2 bg-white p-4">
|
||||
<h2 className="text-lg font-medium">Account</h2>
|
||||
<div className="mt-4 max-w-lg divide-y *:grid *:grid-cols-2 *:p-2">
|
||||
<div>
|
||||
<div>Email</div>
|
||||
<div className="font-mono">{user?.email}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Created at</div>
|
||||
<div className="font-mono">
|
||||
{formatDate(user?.created_at || "")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <h3 className="mt-8 font-medium">Teams</h3>
|
||||
<ul>
|
||||
{teams.data?.map((team) => (
|
||||
<li key={team.id}>{team.name}</li>
|
||||
))}
|
||||
</ul> */}
|
||||
</section>
|
||||
<section className="my-4 rounded-xl border border-b-2 bg-white p-4">
|
||||
<h2 className="text-lg font-medium">Subscription details</h2>
|
||||
{subscription.isLoading ? (
|
||||
<></>
|
||||
@@ -78,32 +236,46 @@ const AccountPage = (props: Props) => {
|
||||
) : (
|
||||
<span>
|
||||
<span className="rounded-md bg-zinc-100 px-3 py-1 font-mono">
|
||||
{subscription.data?.status}
|
||||
{subscription.data?.status || "Not found"}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-8 flex gap-3">
|
||||
|
||||
<div className="">
|
||||
{loading ? (
|
||||
<pre>Loading...</pre>
|
||||
) : (
|
||||
<>
|
||||
{subscription.data?.status !== "active" ? (
|
||||
<Button onClick={onSubscribeClick}>
|
||||
Subcribe to Pro plan
|
||||
</Button>
|
||||
<>
|
||||
<hr className="my-8 max-w-lg" />
|
||||
<SubscribeSection />
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onManageSubscriptionClick}
|
||||
>
|
||||
Manage subscription
|
||||
</Button>
|
||||
<>
|
||||
{/* <pre>{JSON.stringify(subscription.data, null, 2)}</pre> */}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr className="my-6 max-w-lg" />
|
||||
|
||||
<h3 className="text-lg font-medium">Manage your subscription</h3>
|
||||
<p className="text-zinc-500">
|
||||
Check invoices, billing and payment information.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant="secondary"
|
||||
onClick={onManageSubscriptionClick}
|
||||
>
|
||||
<Landmark />
|
||||
Manage subscription
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
createOrRetrieveCustomer,
|
||||
createStripeClient,
|
||||
} from "@/lib/server/stripe";
|
||||
import { STRIPE_CONSTANTS } from "@/lib/server/stripe.constants";
|
||||
import { BASE_URL } from "@/lib/config";
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
try {
|
||||
const stripe = createStripeClient();
|
||||
const { user } = await getServerClient(req, res);
|
||||
const price_id = req.body.price_id;
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
@@ -19,6 +20,10 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
return res.status(400).json({ error: "User email not found" });
|
||||
}
|
||||
|
||||
if (!price_id) {
|
||||
return res.status(400).json({ error: "Product id not found" });
|
||||
}
|
||||
|
||||
const customer = await createOrRetrieveCustomer({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
@@ -28,14 +33,14 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
customer: customer.id,
|
||||
line_items: [
|
||||
{
|
||||
price: STRIPE_CONSTANTS.products.proPlan.yearlyPriceId,
|
||||
price: price_id,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: `http://localhost:3000/account?success=true`,
|
||||
cancel_url: `http://localhost:3000/account?canceled=true`,
|
||||
success_url: `${BASE_URL}/account?success=true`,
|
||||
cancel_url: `${BASE_URL}/account?canceled=true`,
|
||||
});
|
||||
|
||||
if (!session.url) {
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
createOrRetrieveCustomer,
|
||||
createStripeClient,
|
||||
} from "@/lib/server/stripe";
|
||||
import { STRIPE_CONSTANTS } from "@/lib/server/stripe.constants";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
try {
|
||||
const stripe = createStripeClient();
|
||||
const { user } = await getServerClient(req, res);
|
||||
const { user, db } = await getServerClient(req, res);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
@@ -24,6 +24,26 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
const products = await db.from("products").select("*");
|
||||
const prices = await db.from("prices").select("*");
|
||||
|
||||
if (!products.data || !prices.data) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Error fetching products and prices" });
|
||||
}
|
||||
|
||||
const subscriptionUpdateConfig = products.data?.map((product) => ({
|
||||
product: product.stripe_product_id,
|
||||
prices: prices.data
|
||||
?.filter(
|
||||
(price) =>
|
||||
(price.price as unknown as Stripe.Price).product ===
|
||||
product.stripe_product_id
|
||||
)
|
||||
.map((price) => price.stripe_price_id),
|
||||
}));
|
||||
|
||||
const configuration = await stripe.billingPortal.configurations.create({
|
||||
features: {
|
||||
customer_update: {
|
||||
@@ -56,16 +76,11 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
subscription_update: {
|
||||
enabled: true,
|
||||
proration_behavior: "create_prorations",
|
||||
default_allowed_updates: ["price"],
|
||||
products: [
|
||||
{
|
||||
product: STRIPE_CONSTANTS.products.proPlan.productId,
|
||||
prices: [
|
||||
STRIPE_CONSTANTS.products.proPlan.monthlyPriceId,
|
||||
STRIPE_CONSTANTS.products.proPlan.yearlyPriceId,
|
||||
],
|
||||
},
|
||||
],
|
||||
default_allowed_updates: ["price", "promotion_code"],
|
||||
products: subscriptionUpdateConfig.map((config) => ({
|
||||
product: config.product!,
|
||||
prices: config.prices,
|
||||
})),
|
||||
},
|
||||
},
|
||||
business_profile: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createAdminClient } from "@/lib/server/supabase";
|
||||
import { getSupabaseClient } from "@/lib/supabase";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createAdminClient } from "@/lib/server/supabase";
|
||||
import { getSupabaseClient } from "@/lib/supabase";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
|
||||
@@ -9,6 +9,9 @@ export default async function handler(
|
||||
const { db, user } = await getServerClient(req, res);
|
||||
|
||||
const blogId = req.query.blogId as string;
|
||||
if (!user?.id) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
const data = PatchBlog.safeParse(JSON.parse(req.body));
|
||||
|
||||
@@ -11,23 +11,34 @@ export default async function handler(
|
||||
const { db, user } = await getServerClient(req, res);
|
||||
const blogId = req.query.blogId as string;
|
||||
const postSlug = req.query.postSlug as string;
|
||||
const userId = user?.id;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
const data = PatchPost.safeParse(JSON.parse(req.body));
|
||||
|
||||
console.log("----> data", data);
|
||||
|
||||
if (!data.success) {
|
||||
return res.status(400).json({ error: data.error.message });
|
||||
}
|
||||
|
||||
const { title, slug, content, published, cover_image } = data.data;
|
||||
const { title, slug, content, published, cover_image, metadata } =
|
||||
data.data;
|
||||
|
||||
const updated_at = new Date().toISOString();
|
||||
|
||||
const { data: post, error } = await db
|
||||
.from("posts")
|
||||
.update({ title, content, slug, published, cover_image, updated_at })
|
||||
.update({
|
||||
title,
|
||||
content,
|
||||
slug,
|
||||
published,
|
||||
cover_image,
|
||||
updated_at,
|
||||
metadata,
|
||||
})
|
||||
.eq("blog_id", blogId)
|
||||
.eq("user_id", user?.id)
|
||||
.eq("slug", postSlug)
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function handler(
|
||||
) {
|
||||
const { db } = await getServerClient(req, res);
|
||||
// const { userId } = getAuth(req);
|
||||
const blogId = req.query.blogId;
|
||||
const blogId = req.query.blogId as string;
|
||||
|
||||
if (!db) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
|
||||
@@ -1,57 +1,74 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { NextApiHandler } from "next";
|
||||
import { createStripeClient } from "@/lib/server/stripe";
|
||||
import getRawBody from "raw-body";
|
||||
import { createAdminClient } from "@/lib/server/supabase";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const stripe = createStripeClient();
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
console.log("----");
|
||||
|
||||
async function updateSubscription(event: Stripe.Event) {
|
||||
const stripe = createStripeClient();
|
||||
const supabase = createAdminClient();
|
||||
|
||||
async function upsertSubscription(event: Stripe.Event) {
|
||||
if (
|
||||
event.type !== "customer.subscription.updated" &&
|
||||
event.type !== "customer.subscription.deleted"
|
||||
event.type === "customer.subscription.created" ||
|
||||
event.type === "customer.subscription.updated" ||
|
||||
event.type === "customer.subscription.deleted"
|
||||
) {
|
||||
const cusId = event.data.object.customer;
|
||||
|
||||
if (typeof cusId !== "string" || !cusId) {
|
||||
console.error("Invalid customer id");
|
||||
throw new Error("Invalid customer id");
|
||||
}
|
||||
|
||||
const customer = await stripe.customers.retrieve(cusId);
|
||||
|
||||
const userId = customer.deleted ? null : customer.metadata.userId;
|
||||
|
||||
if (!userId) {
|
||||
console.error("Invalid user id");
|
||||
throw new Error("Invalid user id");
|
||||
}
|
||||
|
||||
const res = await supabase.from("subscriptions").upsert(
|
||||
{
|
||||
stripe_subscription_id: event.data.object.id,
|
||||
user_id: userId,
|
||||
status: event.data.object.status,
|
||||
subscription: event.data.object as any,
|
||||
},
|
||||
{
|
||||
onConflict: "user_id",
|
||||
}
|
||||
);
|
||||
|
||||
console.log(res);
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertProduct(event: Stripe.Event) {
|
||||
if (
|
||||
event.type !== "product.created" &&
|
||||
event.type !== "product.updated" &&
|
||||
event.type !== "product.deleted"
|
||||
) {
|
||||
throw new Error("Invalid event type");
|
||||
}
|
||||
|
||||
const cusId = event.data.object.customer;
|
||||
|
||||
if (typeof cusId !== "string" || !cusId) {
|
||||
throw new Error("Invalid customer id");
|
||||
}
|
||||
|
||||
const customer = await stripe.customers.retrieve(cusId);
|
||||
|
||||
const userId = customer.deleted ? null : customer.metadata.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("Invalid user id");
|
||||
}
|
||||
|
||||
// Update customer in database
|
||||
const supabase = createAdminClient();
|
||||
|
||||
console.log(
|
||||
"🟢 UPDATING SUBSCRIPTION STATUS: ",
|
||||
event.data.object.cancellation_details,
|
||||
userId
|
||||
);
|
||||
|
||||
await supabase.from("subscriptions").upsert({
|
||||
user_id: userId,
|
||||
status: event.data.object.status,
|
||||
event: event,
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertProduct(event: Stripe.Event) {
|
||||
if (event.type !== "product.created") {
|
||||
throw new Error("Invalid event type");
|
||||
}
|
||||
|
||||
const product = event.data.object;
|
||||
|
||||
if (!product.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof product.id !== "string" || !product.id) {
|
||||
throw new Error("Invalid product id");
|
||||
}
|
||||
@@ -59,12 +76,27 @@ async function upsertProduct(event: Stripe.Event) {
|
||||
// Update product in database
|
||||
const supabase = createAdminClient();
|
||||
|
||||
console.log("🟢 CREATING PRODUCT: ", product.id);
|
||||
|
||||
await supabase.from("products").upsert({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
active: product.active,
|
||||
stripe_product_id: product.id,
|
||||
product: product as any,
|
||||
});
|
||||
}
|
||||
|
||||
async function upserCustomer(event: Stripe.Event) {
|
||||
if (event.type !== "customer.created") {
|
||||
throw new Error("Invalid event type");
|
||||
}
|
||||
|
||||
const customer = event.data.object;
|
||||
|
||||
if (typeof customer.id !== "string" || !customer.id) {
|
||||
throw new Error("Invalid customer id");
|
||||
}
|
||||
|
||||
// Update customer in database
|
||||
await supabase.from("customers").upsert({
|
||||
id: customer.id,
|
||||
email: customer.email,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,6 +107,10 @@ async function upsertPrice(event: Stripe.Event) {
|
||||
|
||||
const price = event.data.object;
|
||||
|
||||
if (!price.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof price.id !== "string" || !price.id) {
|
||||
throw new Error("Invalid price id");
|
||||
}
|
||||
@@ -82,14 +118,9 @@ async function upsertPrice(event: Stripe.Event) {
|
||||
// Update price in database
|
||||
const supabase = createAdminClient();
|
||||
|
||||
console.log("🟢 CREATING PRICE: ", price.id);
|
||||
|
||||
await supabase.from("prices").upsert({
|
||||
id: price.id,
|
||||
product_id: price.product,
|
||||
currency: price.currency,
|
||||
unit_amount: price.unit_amount,
|
||||
active: price.active,
|
||||
stripe_price_id: price.id,
|
||||
price: price as any,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,23 +157,35 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "customer.subscription.updated") {
|
||||
console.log("🟢 UPDATED");
|
||||
await updateSubscription(event);
|
||||
res.status(200).send("success");
|
||||
} else if (event.type === "customer.subscription.deleted") {
|
||||
console.log("🟢 DELETED");
|
||||
await updateSubscription(event);
|
||||
res.status(200).send("success");
|
||||
} else if (event.type === "product.created") {
|
||||
console.log("🟢 PRODUCT CREATED");
|
||||
type EventHandler = (event: Stripe.Event) => Promise<void>;
|
||||
type EventKey = Stripe.Event["type"];
|
||||
type EventMap = {
|
||||
[key in EventKey]?: EventHandler;
|
||||
};
|
||||
|
||||
res.status(200).send("success");
|
||||
} else {
|
||||
return res
|
||||
.status(200)
|
||||
.send(`Unhandled event type: ${event.type} ${event.id}`);
|
||||
const eventMap: EventMap = {
|
||||
"customer.subscription.created": upsertSubscription,
|
||||
"customer.subscription.updated": upsertSubscription,
|
||||
"customer.subscription.deleted": upsertSubscription,
|
||||
"product.created": upsertProduct,
|
||||
"product.updated": upsertProduct,
|
||||
"product.deleted": upsertProduct,
|
||||
"price.created": upsertPrice,
|
||||
"price.updated": upsertPrice,
|
||||
"price.deleted": upsertPrice,
|
||||
};
|
||||
|
||||
const handler = eventMap[event.type];
|
||||
|
||||
if (!handler) {
|
||||
// If there isnt a handler just return 200, we dont need to handle this event
|
||||
res.status(200).send("OK");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🟢 Stripe: ", event.type);
|
||||
await handler(event);
|
||||
res.status(200).send("OK");
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
res.status(500).json({ error: "Error updating subscription" });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BlogSelector } from "@/components/Blogs/BlogSelector";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { ZendoEditor } from "@/components/Editor/ZendoEditor";
|
||||
import { toast } from "sonner";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
|
||||
export default function CreatePost() {
|
||||
const router = useRouter();
|
||||
const blogId = router.query.blogId as string;
|
||||
const supa = useSupabaseClient();
|
||||
const supa = getSupabaseBrowserClient();
|
||||
|
||||
return (
|
||||
<ZendoEditor
|
||||
|
||||
@@ -1,97 +1,10 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { Editor, EditorContent, JSONContent, useEditor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BoldIcon,
|
||||
CodeIcon,
|
||||
ItalicIcon,
|
||||
PenLine,
|
||||
Pencil,
|
||||
SaveIcon,
|
||||
Strikethrough,
|
||||
Trash,
|
||||
Trash2Icon,
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
import { PiArrowBendUpLeftBold, PiCodeBlock } from "react-icons/pi";
|
||||
import Heading from "@tiptap/extension-heading";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { ContentRenderer } from "@/components/Content/ContentRenderer";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { CgArrowTopLeft, CgWebsite } from "react-icons/cg";
|
||||
import { BsFillImageFill } from "react-icons/bs";
|
||||
import Link from "next/link";
|
||||
import { ImagePicker } from "@/components/Images/ImagePicker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BlogImage } from "@/lib/types/BlogImage";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ZendoEditor } from "@/components/Editor/ZendoEditor";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// function EditorMenuButton({
|
||||
// children,
|
||||
// active,
|
||||
// ...props
|
||||
// }: {
|
||||
// children: React.ReactNode;
|
||||
// active: boolean;
|
||||
// } & React.ComponentPropsWithoutRef<"button">) {
|
||||
// const className = `p-2 rounded-md hover:bg-slate-100/80 text-slate-400 hover:text-slate-600 ${
|
||||
// active ? "text-orange-500" : ""
|
||||
// }`;
|
||||
|
||||
// return (
|
||||
// <button type="button" className={className} {...props}>
|
||||
// {children}
|
||||
// </button>
|
||||
// );
|
||||
// }
|
||||
|
||||
// function EditorMenu({ editor }: { editor: Editor | null }) {
|
||||
// const SIZE = 18;
|
||||
// const menuButtons = [
|
||||
// {
|
||||
// icon: <BoldIcon size={SIZE} />,
|
||||
// command: () => editor?.chain().focus().toggleBold().run(),
|
||||
// },
|
||||
// {
|
||||
// icon: <ItalicIcon size={SIZE} />,
|
||||
// command: () => editor?.chain().focus().toggleItalic().run(),
|
||||
// },
|
||||
// {
|
||||
// icon: <Strikethrough size={SIZE} />,
|
||||
// command: () => editor?.chain().focus().toggleStrike().run(),
|
||||
// },
|
||||
// {
|
||||
// icon: <CodeIcon size={SIZE} />,
|
||||
// command: () => editor?.chain().focus().toggleCode().run(),
|
||||
// },
|
||||
// {
|
||||
// icon: <PiCodeBlock size={SIZE} />,
|
||||
// command: () => editor?.chain().focus().toggleCodeBlock().run(),
|
||||
// },
|
||||
// ];
|
||||
|
||||
// return (
|
||||
// <div className="flex rounded-2xl bg-white p-1">
|
||||
// {menuButtons.map(({ icon, command }, i) => (
|
||||
// <EditorMenuButton
|
||||
// active={editor?.isActive(command) || false}
|
||||
// key={i}
|
||||
// onClick={() => command()}
|
||||
// >
|
||||
// {icon}
|
||||
// </EditorMenuButton>
|
||||
// ))}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
|
||||
export default function Post() {
|
||||
const api = createAPIClient();
|
||||
@@ -101,21 +14,11 @@ export default function Post() {
|
||||
const blogId = router.query.blogId as string;
|
||||
const postSlug = router.query.postSlug as string;
|
||||
|
||||
const {
|
||||
data: post,
|
||||
isLoading,
|
||||
error: postError,
|
||||
} = useQuery(["posts", router.query.blogId, router.query.postSlug], () =>
|
||||
api.posts.get(blogId, postSlug)
|
||||
const { data: post, isLoading } = useQuery(
|
||||
["posts", router.query.blogId, router.query.postSlug],
|
||||
() => api.posts.get(blogId, postSlug)
|
||||
);
|
||||
|
||||
const deletePost = useMutation({
|
||||
mutationFn: () => api.posts.delete(blogId, postSlug),
|
||||
onSuccess: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const updatePost = useMutation({
|
||||
mutationFn: (
|
||||
data: Partial<{
|
||||
@@ -124,6 +27,7 @@ export default function Post() {
|
||||
slug: string;
|
||||
cover_image?: string;
|
||||
content?: any;
|
||||
metadata?: any;
|
||||
}>
|
||||
) => api.posts.update(blogId, postSlug, data),
|
||||
onSuccess: () => {
|
||||
@@ -143,6 +47,7 @@ export default function Post() {
|
||||
<ZendoEditor
|
||||
onSave={async (content) => {
|
||||
try {
|
||||
console.log(content.metadata);
|
||||
await updatePost.mutateAsync(content);
|
||||
toast.success("Post saved!");
|
||||
} catch (error) {
|
||||
@@ -150,7 +55,7 @@ export default function Post() {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
post={post as any} // TODO: rm any
|
||||
post={post}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Spinner from "@/components/Spinner";
|
||||
import AppLayout from "@/layouts/AppLayout";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { IoSettingsSharp } from "react-icons/io5";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoreVertical, Trash } from "lucide-react";
|
||||
@@ -14,6 +13,8 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function StatePill({ published }: { published: boolean }) {
|
||||
const text = published ? "Published" : "Draft";
|
||||
@@ -33,12 +34,27 @@ export function StatePill({ published }: { published: boolean }) {
|
||||
export default function BlogPosts() {
|
||||
const router = useRouter();
|
||||
const blogId = router.query.blogId as string;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const api = createAPIClient();
|
||||
const { isLoading, data, error } = useQuery(["posts", blogId], () =>
|
||||
api.posts.getAll(blogId)
|
||||
);
|
||||
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
|
||||
const deletePostMutation = useMutation({
|
||||
mutationFn: async (postId: string) => {
|
||||
await supabase.from("posts").update({ deleted: true }).eq("id", postId);
|
||||
},
|
||||
onMutate: async (postId: string) => {
|
||||
await queryClient.cancelQueries(["posts", blogId]);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(["posts", blogId]);
|
||||
},
|
||||
});
|
||||
|
||||
function getFormattedPosts() {
|
||||
if (!data || !data.posts) return [];
|
||||
|
||||
@@ -60,14 +76,6 @@ export default function BlogPosts() {
|
||||
return sortedPosts;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<AppLayout>
|
||||
<Spinner />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AppLayout>
|
||||
@@ -79,7 +87,7 @@ export default function BlogPosts() {
|
||||
if (data) {
|
||||
const { blog, posts } = data;
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppLayout loading={isLoading}>
|
||||
<div className="mx-auto mt-8 max-w-5xl p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-semibold">
|
||||
@@ -128,7 +136,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-slate-100/60"
|
||||
className="flex items-center gap-4 rounded-sm p-3 hover:bg-zinc-100/60"
|
||||
key={post.slug}
|
||||
>
|
||||
{post.cover_image && (
|
||||
@@ -160,7 +168,23 @@ export default function BlogPosts() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => {}}>
|
||||
<DropdownMenuItem
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const confirmed = window.confirm(
|
||||
"Are you sure you want to delete this post?"
|
||||
);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
toast.success("Post deleted");
|
||||
await deletePostMutation.mutateAsync(post.id);
|
||||
} catch (error) {
|
||||
toast.error("Failed to delete post");
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash size="16" />
|
||||
<span className="ml-2">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AppLayout from "@/layouts/AppLayout";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { PatchBlog } from "@/lib/models/blogs/Blogs";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -188,11 +188,14 @@ const cms = createClient({
|
||||
</h3>
|
||||
</section>
|
||||
|
||||
<section className="section border border-red-300 bg-gradient-to-b from-white to-red-50 p-3 text-red-600">
|
||||
<h2 className="mb-4 text-lg font-medium">🚨 Danger zone</h2>
|
||||
<p className="text-sm">
|
||||
This action cannot be undone. This will permanently delete the blog.
|
||||
This will also delete all posts in the blog.
|
||||
<section className="section border border-red-500 bg-white p-3 ">
|
||||
<h2 className="mb-4 text-lg font-medium text-red-600">
|
||||
🚨 Danger zone
|
||||
</h2>
|
||||
<p className="space-y-4">
|
||||
<div>This action cannot be undone.</div>
|
||||
<div>This will permanently delete the blog.</div>
|
||||
<div>This will also delete all posts in the blog.</div>
|
||||
</p>
|
||||
<div className="actions">
|
||||
<Button
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function CreateBlog() {
|
||||
if (!isSubscribed) {
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="section mx-8 my-12 py-12">
|
||||
<div className="section mx-8 mx-auto my-12 max-w-xl py-12">
|
||||
<div className="text-center text-4xl">🚫</div>
|
||||
<h2 className="mt-2">
|
||||
<span className="block text-center text-3xl font-semibold">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Spinner from "@/components/Spinner";
|
||||
import AppLayout from "@/layouts/AppLayout";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useBlogsQuery } from "@/queries/blogs";
|
||||
import Link from "next/link";
|
||||
import { IoSettingsSharp, IoAdd } from "react-icons/io5";
|
||||
@@ -11,17 +11,9 @@ import { useEffect } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
export default function Dashboard() {
|
||||
const api = createAPIClient();
|
||||
const { data, error, isLoading } = useBlogsQuery();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (data?.length === 0) {
|
||||
router.push("/blogs/create");
|
||||
}
|
||||
}, [router, data, isLoading]);
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="mt-8 min-h-screen">
|
||||
@@ -64,7 +56,7 @@ export default function Dashboard() {
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-orange-100 text-3xl transition-all group-hover:scale-105">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-orange-100 text-3xl transition-all group-hover:scale-110">
|
||||
{blog.emoji}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BlogSelector } from "@/components/Blogs/BlogSelector";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -3,30 +3,20 @@ import Link from "next/link";
|
||||
import { FaTwitter } from "react-icons/fa";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { getSupabaseClient } from "@/lib/supabase";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import ZendoLogo from "@/components/ZendoLogo";
|
||||
import { useUser } from "@supabase/auth-helpers-react";
|
||||
import { LoggedInUser } from "@/components/LoggedInUser";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StarIcon } from "lucide-react";
|
||||
import Footer from "@/components/Footer";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { SubscribeSection } from "./account";
|
||||
|
||||
const Home = () => {
|
||||
const user = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
const client = getSupabaseClient();
|
||||
client.auth.getSession().then((res) => {
|
||||
console.log("sess", res);
|
||||
});
|
||||
|
||||
client.auth.getUser().then((res) => {
|
||||
console.log("user", res);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -41,7 +31,7 @@ const Home = () => {
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const formData = formSchema.parse(data);
|
||||
|
||||
const sb = getSupabaseClient();
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
await sb.from("homepage_signup").insert(formData);
|
||||
|
||||
@@ -65,7 +55,7 @@ const Home = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow items-center justify-end gap-4 font-medium text-zinc-600">
|
||||
<Link href="/blog">Blog</Link>
|
||||
{/* <Link href="/blog">Blog</Link> */}
|
||||
<LoggedInUser>
|
||||
<Link
|
||||
className="rounded-full px-3 py-1.5 hover:text-zinc-800"
|
||||
@@ -85,16 +75,18 @@ const Home = () => {
|
||||
</Link>
|
||||
|
||||
{!user && (
|
||||
<Button asChild>
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="btn btn-primary inline-block"
|
||||
title="Sign in"
|
||||
aria-label="Sign in"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="space-x-1.5">
|
||||
<Button asChild variant={"outline"}>
|
||||
<Link href="/sign-in" title="Sign in" aria-label="Sign in">
|
||||
Log in
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href="/sign-up" title="Sign up" aria-label="Sign up">
|
||||
Sign up
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user && (
|
||||
@@ -133,15 +125,28 @@ const Home = () => {
|
||||
in 2 minutes
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-2 text-lg font-light text-zinc-500">
|
||||
Open source, headless, blogging CMS.
|
||||
</p>
|
||||
<div className="mt-4 text-lg font-light text-zinc-500">
|
||||
<ul className="space-y-3">
|
||||
<li>Open source.</li>
|
||||
<li>Headless, works with any stack.</li>
|
||||
<li>Type safe content.</li>
|
||||
<li>Great editing experience.</li>
|
||||
<li>Easy to extend.</li>
|
||||
<li>Gets you up and running in 2 minutes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-12" />
|
||||
{!hasSubmitted && (
|
||||
<form
|
||||
className="mt-6 flex max-w-sm flex-col gap-2"
|
||||
className="mt-6 flex max-w-sm flex-col gap-2 pb-12"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<h2>
|
||||
<span className="font-serif text-2xl font-extralight italic text-zinc-800">
|
||||
Be the first to try it.
|
||||
</span>
|
||||
</h2>
|
||||
<div className="flex gap-2 [&>*]:w-full">
|
||||
<label htmlFor="name">
|
||||
<Input
|
||||
@@ -182,20 +187,13 @@ const Home = () => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<div className="mx-4 mt-12 flex max-w-xl flex-col gap-4 py-6 font-mono text-zinc-800">
|
||||
<h2 className="text-lg font-medium"># what you get</h2>
|
||||
<ul className="flex flex-col gap-3">
|
||||
<li>- Open source.</li>
|
||||
<li>- Type safe content with the TS client.</li>
|
||||
<li>- Great editing experience.</li>
|
||||
<li>- Works with any stack.</li>
|
||||
<li>- Easy to extend.</li>
|
||||
<li>- Gets you up and running in 2 minutes.</li>
|
||||
<li>- Have as many blogs as you want.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr className="my-12" />
|
||||
|
||||
<section className="pb-8">
|
||||
<SubscribeSection />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ResetPasswordConfirmation() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const supabase = useSupabaseClient();
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
const router = useRouter();
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ResetPassword() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [step1Success, setStep1Success] = useState(false);
|
||||
const supabase = useSupabaseClient();
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
const router = useRouter();
|
||||
|
||||
async function onSubmitStep1(e: React.FormEvent<HTMLFormElement>) {
|
||||
@@ -26,6 +28,8 @@ export default function ResetPassword() {
|
||||
|
||||
if (error) {
|
||||
alert(error.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep1Success(true);
|
||||
@@ -38,8 +42,9 @@ export default function ResetPassword() {
|
||||
<div className="py-40 text-center">
|
||||
<h2 className="text-2xl font-medium">Reset password</h2>
|
||||
<p className="text-slate-500">
|
||||
We`ve sent you a link to reset your password.
|
||||
We have sent you a link to reset your password.
|
||||
</p>
|
||||
<Link href="https://gmail.com">Open gmail</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -53,11 +58,19 @@ export default function ResetPassword() {
|
||||
>
|
||||
<h2 className="text-2xl font-medium">Reset password</h2>
|
||||
<p className="text-slate-500">
|
||||
We`ll send you a link to reset your password.
|
||||
We will send you a link to reset your password.
|
||||
</p>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input type="email" name="email" id="email" />
|
||||
<Button type="submit">Send reset link</Button>
|
||||
{loading ? (
|
||||
<p>
|
||||
<Spinner />
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input type="email" name="email" id="email" />
|
||||
<Button type="submit">Send reset link</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import ZendoLogo from "@/components/ZendoLogo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useUser } from "@/utils/supabase/browser";
|
||||
import { TabsContent } from "@radix-ui/react-tabs";
|
||||
import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react";
|
||||
import { CornerUpLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HiArrowLeft } from "react-icons/hi";
|
||||
|
||||
export default function SignIn() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const supabase = useSupabaseClient();
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.email) {
|
||||
router.push("/blogs");
|
||||
}
|
||||
}, [user, router]);
|
||||
supabase.auth.getSession().then((res) => {
|
||||
if (res.data.session?.user) {
|
||||
router.push("/blogs");
|
||||
}
|
||||
});
|
||||
}, [router, supabase]);
|
||||
|
||||
async function onSubmitMagicLink(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
@@ -49,9 +52,9 @@ export default function SignIn() {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
const form = e.currentTarget;
|
||||
const email = form.email.value;
|
||||
const password = form.password.value;
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("email") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
@@ -70,48 +73,33 @@ export default function SignIn() {
|
||||
return (
|
||||
<div className="mx-auto my-32 flex max-w-sm flex-col gap-4">
|
||||
<div>
|
||||
<Link className="text-slate-400" href="/">
|
||||
<Link className="text-zinc-400" href="/">
|
||||
<CornerUpLeft size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
<Tabs defaultValue="password">
|
||||
<TabsList>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
<TabsTrigger value="magic-link">Magic Link</TabsTrigger>
|
||||
</TabsList>
|
||||
<form className="mt-4 flex flex-col gap-2" onSubmit={onSubmit}>
|
||||
<h1 className="text-2xl font-medium">Sign in with Password</h1>
|
||||
|
||||
<TabsContent value="password">
|
||||
<form className="mt-4 flex flex-col gap-4" onSubmit={onSubmit}>
|
||||
<h1 className="text-2xl font-medium">Sign in with Password</h1>
|
||||
<div>
|
||||
<Label htmlFor="password">Email</Label>
|
||||
<Input required type="email" name="email" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input required type="password" name="password" />
|
||||
</div>
|
||||
<Button type="submit">Sign in</Button>
|
||||
</form>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="magic-link">
|
||||
<form
|
||||
className="mt-4 flex flex-col gap-4"
|
||||
onSubmit={onSubmitMagicLink}
|
||||
>
|
||||
<h1 className="text-2xl font-medium">Sign in with Magic Link</h1>
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input required type="email" name="email" />
|
||||
</div>
|
||||
<Button type="submit">Sign in</Button>
|
||||
</form>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<Link className="link" href="/reset-password">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
<div className="mt-4">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input required type="email" name="email" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input required type="password" name="password" />
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col">
|
||||
<Button type="submit">Sign in</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="grid gap-4">
|
||||
<Link className="text-zinc-500" href="/reset-password">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
<Link className="text-zinc-500" href="/sign-up">
|
||||
Don't have an account?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function SignOut() {
|
||||
const supa = useSupabaseClient();
|
||||
const supa = getSupabaseBrowserClient();
|
||||
|
||||
useEffect(() => {
|
||||
supa.auth.signOut().then((res) => {
|
||||
@@ -12,5 +12,5 @@ export default function SignOut() {
|
||||
});
|
||||
}, [supa]);
|
||||
|
||||
return <>Signing out...</>;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { CornerUpLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function SignIn() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then((res) => {
|
||||
if (res.data.session?.user) {
|
||||
router.push("/blogs");
|
||||
}
|
||||
});
|
||||
}, [router, supabase]);
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const form = e.currentTarget;
|
||||
const email = form.email.value;
|
||||
const password = form.password.value;
|
||||
|
||||
const sb = getSupabaseBrowserClient();
|
||||
const { data, error } = await sb.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
} catch (error) {
|
||||
console.error("Error creating account", error);
|
||||
toast.error("Error creating account");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="mx-auto my-32 flex max-w-sm flex-col gap-4">
|
||||
<p className="text-4xl">🚀</p>
|
||||
<h1 className="text-2xl font-medium">Account created!</h1>
|
||||
<p className="bg-white">
|
||||
Please, <span className="underline">check your email</span> 📧 to
|
||||
confirm your account.
|
||||
</p>
|
||||
<div className="">
|
||||
<p className="text-sm">You can close this tab.</p>
|
||||
</div>
|
||||
<div className="text-zinc-200">
|
||||
<p className="text-xs">or not, i don't care.</p>
|
||||
<p className="text-xs">it's your computer.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto my-32 flex max-w-sm flex-col gap-4">
|
||||
<div>
|
||||
<Link className="text-slate-400" href="/">
|
||||
<CornerUpLeft size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
<form className="mt-4 flex flex-col gap-4" onSubmit={onSubmit}>
|
||||
<h1 className="text-2xl font-medium">Create your account</h1>
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input required type="email" name="email" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input required type="password" name="password" />
|
||||
</div>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Button type="submit">Create account</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<Link className="text-zinc-500" href="/sign-in">
|
||||
Already have an account?
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
const api = createAPIClient();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const api = createAPIClient();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAPIClient } from "@/lib/app/api";
|
||||
import { createAPIClient } from "@/lib/http/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const PRICES_KEYS = ["prices"];
|
||||
|
||||
export function usePricesQuery() {
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return useQuery(PRICES_KEYS, async () => {
|
||||
const { data, error } = await sb.from("prices").select("*");
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
type DataItemType = (typeof data)[0] & { price: Stripe.Price };
|
||||
|
||||
return data as DataItemType[];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export function useProductsQuery() {
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return useQuery(["products"], async () => {
|
||||
const { data, error } = await sb.from("products").select("*");
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
type DataItemType = (typeof data)[0] & { product: Stripe.Product };
|
||||
|
||||
return data as DataItemType[];
|
||||
});
|
||||
}
|
||||
@@ -1,38 +1,20 @@
|
||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const SUBSCRIPTION_KEYS = ["subscription"];
|
||||
|
||||
export function useSubscriptionQuery() {
|
||||
const sb = useSupabaseClient();
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return useQuery(SUBSCRIPTION_KEYS, async () => {
|
||||
const { data, error } = await sb
|
||||
.from("subscriptions")
|
||||
.select("status")
|
||||
.single();
|
||||
const { data, error } = await sb.from("subscriptions").select("*").limit(1);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { ...data, keys: SUBSCRIPTION_KEYS };
|
||||
});
|
||||
}
|
||||
|
||||
export function useProductsQuery() {
|
||||
const sb = useSupabaseClient();
|
||||
|
||||
return useQuery(["products"], async () => {
|
||||
const { data, error } = await sb.from("products").select("*");
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
return data[0];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
const keys = {
|
||||
teams: () => ["teams"],
|
||||
team: (teamId: string) => ["team", teamId],
|
||||
};
|
||||
|
||||
export function useTeamsQuery() {
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return useQuery(keys.teams(), async () => {
|
||||
const { data, error } = await sb.from("teams").select("*");
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateTeamMutation() {
|
||||
const sb = getSupabaseBrowserClient();
|
||||
|
||||
return useMutation(
|
||||
async ({ owner_id, name }: { owner_id: string; name: string }) => {
|
||||
const { data, error } = await sb.from("teams").insert({
|
||||
owner_id,
|
||||
name,
|
||||
});
|
||||
|
||||
console.log("create team", data, error);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Sync stripe data with supabase database
|
||||
|
||||
import "dotenv/config";
|
||||
import { createStripeClient } from "@/lib/server/stripe";
|
||||
import { createAdminClient } from "@/lib/server/supabase";
|
||||
|
||||
const supabase = createAdminClient();
|
||||
const stripe = createStripeClient();
|
||||
|
||||
async function wait(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// async function upsertSubscriptions() {
|
||||
// const subscriptions = await stripe.subscriptions.list({ limit: 100 });
|
||||
// for (const subscription of subscriptions.data) {
|
||||
// await supabase.from("subscriptions").upsert({
|
||||
// user_id: subscription.customer,
|
||||
// status: subscription.status,
|
||||
// stripe_subscription_id: subscription.id,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
async function upsertProducts() {
|
||||
const products = await stripe.products.list({ limit: 100, active: true });
|
||||
for (const product of products.data) {
|
||||
await supabase.from("products").upsert({
|
||||
stripe_product_id: product.id,
|
||||
product: product as any,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertPrices() {
|
||||
const prices = await stripe.prices.list({ limit: 100, active: true });
|
||||
for (const price of prices.data) {
|
||||
await supabase.from("prices").upsert({
|
||||
price: price as any,
|
||||
stripe_price_id: price.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function syncStripe() {
|
||||
await upsertProducts();
|
||||
console.log("Updated products");
|
||||
await wait(500);
|
||||
await upsertPrices();
|
||||
console.log("Updated prices");
|
||||
}
|
||||
|
||||
syncStripe().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
+228
-122
@@ -6,35 +6,9 @@ export type Json =
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
admin_users: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
user_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
user_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
user_id?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "admin_users_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
blogs: {
|
||||
Row: {
|
||||
created_at: string
|
||||
@@ -94,7 +68,6 @@ export type Database = {
|
||||
{
|
||||
foreignKeyName: "categories_blog_id_fkey"
|
||||
columns: ["blog_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "blogs"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
@@ -168,37 +141,41 @@ export type Database = {
|
||||
{
|
||||
foreignKeyName: "invitations_blog_id_fkey"
|
||||
columns: ["blog_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "blogs"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
members: {
|
||||
post_categories: {
|
||||
Row: {
|
||||
blog_id: string
|
||||
category_id: string
|
||||
created_at: string
|
||||
id: number
|
||||
user_id: string
|
||||
post_id: string
|
||||
}
|
||||
Insert: {
|
||||
blog_id: string
|
||||
category_id: string
|
||||
created_at?: string
|
||||
id?: number
|
||||
user_id: string
|
||||
post_id: string
|
||||
}
|
||||
Update: {
|
||||
blog_id?: string
|
||||
category_id?: string
|
||||
created_at?: string
|
||||
id?: number
|
||||
user_id?: string
|
||||
post_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "members_blog_id_fkey"
|
||||
columns: ["blog_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "blogs"
|
||||
foreignKeyName: "post_categories_category_id_fkey"
|
||||
columns: ["category_id"]
|
||||
referencedRelation: "categories"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "post_categories_post_id_fkey"
|
||||
columns: ["post_id"]
|
||||
referencedRelation: "posts"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
@@ -209,6 +186,7 @@ export type Database = {
|
||||
content: Json
|
||||
cover_image: string | null
|
||||
created_at: string
|
||||
deleted: boolean | null
|
||||
id: string
|
||||
metadata: Json[] | null
|
||||
published: boolean
|
||||
@@ -222,6 +200,7 @@ export type Database = {
|
||||
content?: Json
|
||||
cover_image?: string | null
|
||||
created_at?: string
|
||||
deleted?: boolean | null
|
||||
id?: string
|
||||
metadata?: Json[] | null
|
||||
published?: boolean
|
||||
@@ -235,6 +214,7 @@ export type Database = {
|
||||
content?: Json
|
||||
cover_image?: string | null
|
||||
created_at?: string
|
||||
deleted?: boolean | null
|
||||
id?: string
|
||||
metadata?: Json[] | null
|
||||
published?: boolean
|
||||
@@ -247,33 +227,122 @@ export type Database = {
|
||||
{
|
||||
foreignKeyName: "posts_blog_id_fkey"
|
||||
columns: ["blog_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "blogs"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
prices: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
price: Json
|
||||
stripe_price_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
price: Json
|
||||
stripe_price_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
price?: Json
|
||||
stripe_price_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
products: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
product: Json
|
||||
stripe_product_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
product: Json
|
||||
stripe_product_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
product?: Json
|
||||
stripe_product_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
subscriptions: {
|
||||
Row: {
|
||||
created_at: string
|
||||
status: string
|
||||
stripe_subscription_id: string
|
||||
subscription: Json
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
status: string
|
||||
stripe_subscription_id: string
|
||||
subscription: Json
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
status?: string
|
||||
stripe_subscription_id?: string
|
||||
subscription?: Json
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "subscriptions_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "subscriptions_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
teams: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
name: string
|
||||
owner_id: string | null
|
||||
ref: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
name: string
|
||||
owner_id?: string | null
|
||||
ref?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
name?: string
|
||||
owner_id?: string | null
|
||||
ref?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "public_teams_owner_id_fkey"
|
||||
columns: ["owner_id"]
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "public_teams_owner_id_fkey"
|
||||
columns: ["owner_id"]
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
@@ -281,9 +350,125 @@ export type Database = {
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
users: {
|
||||
Row: {
|
||||
aud: string | null
|
||||
banned_until: string | null
|
||||
confirmation_sent_at: string | null
|
||||
confirmation_token: string | null
|
||||
confirmed_at: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
email: string | null
|
||||
email_change: string | null
|
||||
email_change_confirm_status: number | null
|
||||
email_change_sent_at: string | null
|
||||
email_change_token_current: string | null
|
||||
email_change_token_new: string | null
|
||||
email_confirmed_at: string | null
|
||||
encrypted_password: string | null
|
||||
id: string | null
|
||||
instance_id: string | null
|
||||
invited_at: string | null
|
||||
is_sso_user: boolean | null
|
||||
is_super_admin: boolean | null
|
||||
last_sign_in_at: string | null
|
||||
phone: string | null
|
||||
phone_change: string | null
|
||||
phone_change_sent_at: string | null
|
||||
phone_change_token: string | null
|
||||
phone_confirmed_at: string | null
|
||||
raw_app_meta_data: Json | null
|
||||
raw_user_meta_data: Json | null
|
||||
reauthentication_sent_at: string | null
|
||||
reauthentication_token: string | null
|
||||
recovery_sent_at: string | null
|
||||
recovery_token: string | null
|
||||
role: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
aud?: string | null
|
||||
banned_until?: string | null
|
||||
confirmation_sent_at?: string | null
|
||||
confirmation_token?: string | null
|
||||
confirmed_at?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
email?: string | null
|
||||
email_change?: string | null
|
||||
email_change_confirm_status?: number | null
|
||||
email_change_sent_at?: string | null
|
||||
email_change_token_current?: string | null
|
||||
email_change_token_new?: string | null
|
||||
email_confirmed_at?: string | null
|
||||
encrypted_password?: string | null
|
||||
id?: string | null
|
||||
instance_id?: string | null
|
||||
invited_at?: string | null
|
||||
is_sso_user?: boolean | null
|
||||
is_super_admin?: boolean | null
|
||||
last_sign_in_at?: string | null
|
||||
phone?: string | null
|
||||
phone_change?: string | null
|
||||
phone_change_sent_at?: string | null
|
||||
phone_change_token?: string | null
|
||||
phone_confirmed_at?: string | null
|
||||
raw_app_meta_data?: Json | null
|
||||
raw_user_meta_data?: Json | null
|
||||
reauthentication_sent_at?: string | null
|
||||
reauthentication_token?: string | null
|
||||
recovery_sent_at?: string | null
|
||||
recovery_token?: string | null
|
||||
role?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
aud?: string | null
|
||||
banned_until?: string | null
|
||||
confirmation_sent_at?: string | null
|
||||
confirmation_token?: string | null
|
||||
confirmed_at?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
email?: string | null
|
||||
email_change?: string | null
|
||||
email_change_confirm_status?: number | null
|
||||
email_change_sent_at?: string | null
|
||||
email_change_token_current?: string | null
|
||||
email_change_token_new?: string | null
|
||||
email_confirmed_at?: string | null
|
||||
encrypted_password?: string | null
|
||||
id?: string | null
|
||||
instance_id?: string | null
|
||||
invited_at?: string | null
|
||||
is_sso_user?: boolean | null
|
||||
is_super_admin?: boolean | null
|
||||
last_sign_in_at?: string | null
|
||||
phone?: string | null
|
||||
phone_change?: string | null
|
||||
phone_change_sent_at?: string | null
|
||||
phone_change_token?: string | null
|
||||
phone_confirmed_at?: string | null
|
||||
raw_app_meta_data?: Json | null
|
||||
raw_user_meta_data?: Json | null
|
||||
reauthentication_sent_at?: string | null
|
||||
reauthentication_token?: string | null
|
||||
recovery_sent_at?: string | null
|
||||
recovery_token?: string | null
|
||||
role?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Functions: {
|
||||
generate_random_string: {
|
||||
Args: {
|
||||
length: number
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
generate_slug: {
|
||||
Args: {
|
||||
title: string
|
||||
@@ -304,82 +489,3 @@ export type Database = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Tables<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof (Database["public"]["Tables"] & Database["public"]["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof (Database["public"]["Tables"] &
|
||||
Database["public"]["Views"])
|
||||
? (Database["public"]["Tables"] &
|
||||
Database["public"]["Views"])[PublicTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof Database["public"]["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof Database["public"]["Tables"]
|
||||
? Database["public"]["Tables"][PublicTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof Database["public"]["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof Database["public"]["Tables"]
|
||||
? Database["public"]["Tables"][PublicTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
PublicEnumNameOrOptions extends
|
||||
| keyof Database["public"]["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never
|
||||
> = PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: PublicEnumNameOrOptions extends keyof Database["public"]["Enums"]
|
||||
? Database["public"]["Enums"][PublicEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { UserResponse } from "@supabase/supabase-js";
|
||||
import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type User = UserResponse["data"]["user"];
|
||||
|
||||
const UserContext = createContext<User | null>(null);
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
|
||||
export function UserProvider({ children }: PropsWithChildren) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.onAuthStateChange((_, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const user = useContext(UserContext);
|
||||
|
||||
return user;
|
||||
}
|
||||
Generated
+434
-67
@@ -12,7 +12,8 @@
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"next": "^14.1.0"
|
||||
"next": "^14.1.0",
|
||||
"tsx": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbo/gen": "^1.9.7",
|
||||
@@ -179,9 +180,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@stripe/react-stripe-js": "^2.4.0",
|
||||
"@stripe/stripe-js": "^2.4.0",
|
||||
"@supabase/auth-helpers-nextjs": "^0.8.1",
|
||||
"@supabase/auth-helpers-react": "^0.3.1",
|
||||
"@supabase/supabase-js": "^2.21.0",
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@supabase/supabase-js": "^2.39.6",
|
||||
"@t3-oss/env-nextjs": "^0.2.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
@@ -204,6 +204,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"formidable": "^3.5.1",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^10.12.12",
|
||||
@@ -1396,6 +1397,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
|
||||
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||
@@ -4809,41 +4825,10 @@
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.4.0.tgz",
|
||||
"integrity": "sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA=="
|
||||
},
|
||||
"node_modules/@supabase/auth-helpers-nextjs": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.8.1.tgz",
|
||||
"integrity": "sha512-ZO/UGDE9oaVXwPL4gpzGu2NpPwfKOgxTUnuK6no51So8Ah9BUuUUsTj+BdZlx7jJoZEIAkHWdRIx65WuQgWAiQ==",
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-shared": "0.5.0",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-helpers-react": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-react/-/auth-helpers-react-0.3.1.tgz",
|
||||
"integrity": "sha512-g3SFv08Dz9FapNif/ZY1b7qKGlMJDyTLSayHBz3kb3FuYxg7aLWgQtydDhm5AGbc0XtvpIBuhGTIOVevwpdosA==",
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-helpers-shared": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.5.0.tgz",
|
||||
"integrity": "sha512-kioUeYDBZ89cfqOTZFOLEFIO7kGczL0XJYZdVv/bKt5efXqhiY14NbzgGWiMvvD9o9Iluo0+YEV2tG3ZZ5W06A==",
|
||||
"dependencies": {
|
||||
"jose": "^4.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.2.0.tgz",
|
||||
"integrity": "sha512-lAmxD/mZ8vk2mg1CmXQWzK5mOHk7kDxAnxoyqUj2BVPvacEZ52P8nFkInEuSMqx6P6FKy64selW1Vyhui9racA==",
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.1.5.tgz",
|
||||
"integrity": "sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -4868,9 +4853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.11.0.tgz",
|
||||
"integrity": "sha512-Q2WwEid0I2rZfSZ0hDjmI20Fhu31Hzl4bwYTGJjLNMGbUOLoAQSaw0Hv/yHLZnl6OmOrH/trZqDiE5WocHN0oQ==",
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.9.2.tgz",
|
||||
"integrity": "sha512-I6yHo8CC9cxhOo6DouDMy9uOfW7hjdsnCxZiaJuIVZm1dBGTFiQPgfMa9zXCamEWzNyWRjZvupAUuX+tqcl5Sw==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -4907,16 +4892,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.39.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.39.3.tgz",
|
||||
"integrity": "sha512-NoltJSaJNKDJNutO5sJPAAi5RIWrn1z2XH+ig1+cHDojT6BTN7TvZPNa3Kq3gFQWfO5H1N9El/bCTZJ3iFW2kQ==",
|
||||
"version": "2.39.6",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.39.6.tgz",
|
||||
"integrity": "sha512-HlflDzem0+l3KYYTqHV0UsqkDooV9my5UcBCV2zvvTrl77UtW97uKTZWn9lSWMuiy+ZvRLsiuG+WTiBuKMQl0Q==",
|
||||
"dependencies": {
|
||||
"@supabase/functions-js": "^2.1.5",
|
||||
"@supabase/gotrue-js": "^2.60.0",
|
||||
"@supabase/node-fetch": "^2.6.14",
|
||||
"@supabase/postgrest-js": "^1.9.0",
|
||||
"@supabase/realtime-js": "^2.9.3",
|
||||
"@supabase/storage-js": "^2.5.4"
|
||||
"@supabase/functions-js": "2.1.5",
|
||||
"@supabase/gotrue-js": "2.62.2",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "1.9.2",
|
||||
"@supabase/realtime-js": "2.9.3",
|
||||
"@supabase/storage-js": "2.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
@@ -7940,6 +7925,17 @@
|
||||
"no-case": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.4",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
|
||||
"integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -9404,9 +9400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.6.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
|
||||
"integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
@@ -10498,14 +10494,6 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.14.4",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
|
||||
"integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-beautify": {
|
||||
"version": "1.14.11",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz",
|
||||
@@ -12896,7 +12884,6 @@
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
@@ -13219,11 +13206,6 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
||||
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
|
||||
},
|
||||
"node_modules/set-function-name": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
|
||||
@@ -14175,6 +14157,391 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz",
|
||||
"integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.19.10",
|
||||
"get-tsconfig": "^4.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
|
||||
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
|
||||
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
|
||||
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
|
||||
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
|
||||
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
|
||||
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
|
||||
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
|
||||
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
|
||||
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
|
||||
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.19.12",
|
||||
"@esbuild/android-arm": "0.19.12",
|
||||
"@esbuild/android-arm64": "0.19.12",
|
||||
"@esbuild/android-x64": "0.19.12",
|
||||
"@esbuild/darwin-arm64": "0.19.12",
|
||||
"@esbuild/darwin-x64": "0.19.12",
|
||||
"@esbuild/freebsd-arm64": "0.19.12",
|
||||
"@esbuild/freebsd-x64": "0.19.12",
|
||||
"@esbuild/linux-arm": "0.19.12",
|
||||
"@esbuild/linux-arm64": "0.19.12",
|
||||
"@esbuild/linux-ia32": "0.19.12",
|
||||
"@esbuild/linux-loong64": "0.19.12",
|
||||
"@esbuild/linux-mips64el": "0.19.12",
|
||||
"@esbuild/linux-ppc64": "0.19.12",
|
||||
"@esbuild/linux-riscv64": "0.19.12",
|
||||
"@esbuild/linux-s390x": "0.19.12",
|
||||
"@esbuild/linux-x64": "0.19.12",
|
||||
"@esbuild/netbsd-x64": "0.19.12",
|
||||
"@esbuild/openbsd-x64": "0.19.12",
|
||||
"@esbuild/sunos-x64": "0.19.12",
|
||||
"@esbuild/win32-arm64": "0.19.12",
|
||||
"@esbuild/win32-ia32": "0.19.12",
|
||||
"@esbuild/win32-x64": "0.19.12"
|
||||
}
|
||||
},
|
||||
"node_modules/turbo": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.5.tgz",
|
||||
|
||||
+10
-5
@@ -6,12 +6,16 @@
|
||||
"build:web": "turbo run build --filter=website",
|
||||
"dev": "turbo run dev",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"supa:start": "npx supabase start",
|
||||
"supa:stop": "npx supabase stop",
|
||||
"supa:open": "open http://localhost:54323",
|
||||
"db:typegen": "npx supabase gen types typescript --local --schema public > apps/zendo/src/types/supabase.ts",
|
||||
"db:start": "npx supabase start",
|
||||
"db:stop": "npx supabase stop",
|
||||
"db:open": "open http://localhost:54323",
|
||||
"db:pull": "npx supabase db pull",
|
||||
"db:local:diff": "npx supabase db diff",
|
||||
"qs": "git add . && git commit -m \"quick save\" && git push",
|
||||
"stripe:webhook": "stripe listen --forward-to localhost:3000/api/webhooks/stripe",
|
||||
"stripe:event": "stripe trigger payment_intent.succeeded"
|
||||
"stripe:event": "stripe trigger payment_intent.succeeded",
|
||||
"stripe:sync": "turbo run stripe:sync --filter=website"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbo/gen": "^1.9.7",
|
||||
@@ -26,7 +30,8 @@
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"next": "^14.1.0"
|
||||
"next": "^14.1.0",
|
||||
"tsx": "^4.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
|
||||
@@ -87,7 +87,7 @@ enable_signup = true
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
enable_confirmations = true
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
|
||||
+279
-34
@@ -12,8 +12,14 @@ SET row_security = off;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "timescaledb" WITH SCHEMA "extensions";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS "stripe";
|
||||
|
||||
ALTER SCHEMA "stripe" OWNER TO "postgres";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
|
||||
@@ -26,6 +32,23 @@ CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."generate_random_string"("length" integer) RETURNS "text"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
DECLARE
|
||||
chars text := 'abcdefghijklmnopqrstuvwxyz';
|
||||
result text := '';
|
||||
i int := 1;
|
||||
BEGIN
|
||||
FOR i IN 1..length LOOP
|
||||
result := result || substr(chars, floor(random() * length(chars) + 1)::int, 1);
|
||||
END LOOP;
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION "public"."generate_random_string"("length" integer) OWNER TO "postgres";
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."generate_slug"("title" "text") RETURNS "text"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
@@ -62,6 +85,23 @@ SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = "heap";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."admin_users" (
|
||||
"id" bigint NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"user_id" "uuid"
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."admin_users" OWNER TO "postgres";
|
||||
|
||||
ALTER TABLE "public"."admin_users" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME "public"."admin_users_id_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."blogs" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
@@ -74,6 +114,35 @@ CREATE TABLE IF NOT EXISTS "public"."blogs" (
|
||||
|
||||
ALTER TABLE "public"."blogs" OWNER TO "postgres";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."categories" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"blog_id" "uuid" NOT NULL,
|
||||
"name" "text" NOT NULL,
|
||||
"slug" "text" NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."categories" OWNER TO "postgres";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."feedback" (
|
||||
"id" bigint NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"feedback" "text",
|
||||
"user_email" "text"
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."feedback" OWNER TO "postgres";
|
||||
|
||||
ALTER TABLE "public"."feedback" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME "public"."feedback_id_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."homepage_signup" (
|
||||
"id" bigint NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"(),
|
||||
@@ -102,24 +171,6 @@ CREATE TABLE IF NOT EXISTS "public"."invitations" (
|
||||
|
||||
ALTER TABLE "public"."invitations" OWNER TO "postgres";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."members" (
|
||||
"id" bigint NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"user_id" "text" NOT NULL,
|
||||
"blog_id" "uuid" NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."members" OWNER TO "postgres";
|
||||
|
||||
ALTER TABLE "public"."members" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME "public"."members_id_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."posts" (
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"user_id" "text" DEFAULT "public"."requesting_user_id"() NOT NULL,
|
||||
@@ -136,21 +187,132 @@ CREATE TABLE IF NOT EXISTS "public"."posts" (
|
||||
|
||||
ALTER TABLE "public"."posts" OWNER TO "postgres";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."products" (
|
||||
"id" bigint NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"product" "jsonb" NOT NULL,
|
||||
"stripe_product_id" "text"
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."products" OWNER TO "postgres";
|
||||
|
||||
ALTER TABLE "public"."products" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME "public"."products_id_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."subscriptions" (
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"status" "text" NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."subscriptions" OWNER TO "postgres";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."teams" (
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"owner_id" "uuid",
|
||||
"name" "text" NOT NULL,
|
||||
"ref" "text" DEFAULT "public"."generate_random_string"(20),
|
||||
"id" bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."teams" OWNER TO "postgres";
|
||||
|
||||
ALTER TABLE "public"."teams" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME "public"."teams_id_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE OR REPLACE VIEW "public"."users" AS
|
||||
SELECT "users"."instance_id",
|
||||
"users"."id",
|
||||
"users"."aud",
|
||||
"users"."role",
|
||||
"users"."email",
|
||||
"users"."encrypted_password",
|
||||
"users"."email_confirmed_at",
|
||||
"users"."invited_at",
|
||||
"users"."confirmation_token",
|
||||
"users"."confirmation_sent_at",
|
||||
"users"."recovery_token",
|
||||
"users"."recovery_sent_at",
|
||||
"users"."email_change_token_new",
|
||||
"users"."email_change",
|
||||
"users"."email_change_sent_at",
|
||||
"users"."last_sign_in_at",
|
||||
"users"."raw_app_meta_data",
|
||||
"users"."raw_user_meta_data",
|
||||
"users"."is_super_admin",
|
||||
"users"."created_at",
|
||||
"users"."updated_at",
|
||||
"users"."phone",
|
||||
"users"."phone_confirmed_at",
|
||||
"users"."phone_change",
|
||||
"users"."phone_change_token",
|
||||
"users"."phone_change_sent_at",
|
||||
"users"."confirmed_at",
|
||||
"users"."email_change_token_current",
|
||||
"users"."email_change_confirm_status",
|
||||
"users"."banned_until",
|
||||
"users"."reauthentication_token",
|
||||
"users"."reauthentication_sent_at",
|
||||
"users"."is_sso_user",
|
||||
"users"."deleted_at"
|
||||
FROM "auth"."users";
|
||||
|
||||
ALTER TABLE "public"."users" OWNER TO "postgres";
|
||||
|
||||
ALTER TABLE ONLY "public"."admin_users"
|
||||
ADD CONSTRAINT "admin_users_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."blogs"
|
||||
ADD CONSTRAINT "blogs_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."categories"
|
||||
ADD CONSTRAINT "categories_blog_id_slug_key" UNIQUE ("blog_id", "slug");
|
||||
|
||||
ALTER TABLE ONLY "public"."categories"
|
||||
ADD CONSTRAINT "categories_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."feedback"
|
||||
ADD CONSTRAINT "feedback_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."homepage_signup"
|
||||
ADD CONSTRAINT "homepage_signup_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."invitations"
|
||||
ADD CONSTRAINT "invitations_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."members"
|
||||
ADD CONSTRAINT "members_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."posts"
|
||||
ADD CONSTRAINT "posts_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."products"
|
||||
ADD CONSTRAINT "products_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."subscriptions"
|
||||
ADD CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("user_id");
|
||||
|
||||
ALTER TABLE ONLY "public"."subscriptions"
|
||||
ADD CONSTRAINT "subscriptions_user_id_key" UNIQUE ("user_id");
|
||||
|
||||
ALTER TABLE ONLY "public"."teams"
|
||||
ADD CONSTRAINT "teams_id_key" UNIQUE ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."teams"
|
||||
ADD CONSTRAINT "teams_pkey" PRIMARY KEY ("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."teams"
|
||||
ADD CONSTRAINT "teams_ref_key" UNIQUE ("ref");
|
||||
|
||||
ALTER TABLE ONLY "public"."posts"
|
||||
ADD CONSTRAINT "unique_slug_per_user_post_constraint" UNIQUE ("slug", "user_id", "blog_id");
|
||||
|
||||
@@ -158,34 +320,61 @@ CREATE INDEX "posts_slug_blog_id_idx" ON "public"."posts" USING "btree" ("slug",
|
||||
|
||||
CREATE INDEX "posts_slug_idx" ON "public"."posts" USING "btree" ("slug");
|
||||
|
||||
ALTER TABLE ONLY "public"."admin_users"
|
||||
ADD CONSTRAINT "admin_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."categories"
|
||||
ADD CONSTRAINT "categories_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY "public"."invitations"
|
||||
ADD CONSTRAINT "invitations_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs"("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."members"
|
||||
ADD CONSTRAINT "members_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs"("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."posts"
|
||||
ADD CONSTRAINT "posts_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY "public"."teams"
|
||||
ADD CONSTRAINT "public_teams_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id");
|
||||
|
||||
ALTER TABLE ONLY "public"."subscriptions"
|
||||
ADD CONSTRAINT "subscriptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
|
||||
|
||||
CREATE POLICY "Allow AUTHED users to query ALL their POSTS" ON "public"."posts" FOR SELECT TO "authenticated" USING ((("auth"."uid"())::"text" = "user_id"));
|
||||
|
||||
CREATE POLICY "Enable INSERT access for all users" ON "public"."homepage_signup" FOR INSERT TO "anon" WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Enable delete for users based on user_id" ON "public"."posts" FOR DELETE USING ((("auth"."uid"())::"text" = "user_id"));
|
||||
|
||||
CREATE POLICY "Enable insert for authenticated users only" ON "public"."feedback" FOR INSERT TO "authenticated" WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Enable insert for authenticated users only" ON "public"."posts" FOR INSERT TO "authenticated" WITH CHECK ((("auth"."uid"())::"text" = "user_id"));
|
||||
|
||||
CREATE POLICY "Enable insert for owners" ON "public"."teams" FOR INSERT TO "authenticated" WITH CHECK (("auth"."uid"() = "owner_id"));
|
||||
|
||||
CREATE POLICY "Enable read access for all users" ON "public"."teams" FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "Enable read access for all users if published" ON "public"."posts" FOR SELECT TO "anon" USING (("published" = true));
|
||||
|
||||
CREATE POLICY "Enable update for users based on user_id" ON "public"."posts" FOR UPDATE USING ((("auth"."uid"())::"text" = "user_id"));
|
||||
|
||||
ALTER TABLE "public"."admin_users" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "public"."blogs" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "public"."categories" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "delete_categories_policy" ON "public"."categories" FOR DELETE USING ((EXISTS ( SELECT 1
|
||||
FROM "public"."blogs"
|
||||
WHERE (("blogs"."id" = "categories"."blog_id") AND ("blogs"."user_id" = CURRENT_USER)))));
|
||||
|
||||
ALTER TABLE "public"."feedback" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "public"."homepage_signup" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "public"."invitations" ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "insert_categories_policy" ON "public"."categories" FOR INSERT WITH CHECK ((EXISTS ( SELECT 1
|
||||
FROM "public"."blogs"
|
||||
WHERE (("blogs"."id" = "categories"."blog_id") AND ("blogs"."user_id" = CURRENT_USER)))));
|
||||
|
||||
ALTER TABLE "public"."members" ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "public"."invitations" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "owners of a blog can create invitations" ON "public"."invitations" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
|
||||
FROM "public"."blogs"
|
||||
@@ -207,6 +396,22 @@ CREATE POLICY "owners of the blog can see invitations" ON "public"."invitations"
|
||||
|
||||
ALTER TABLE "public"."posts" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "public"."products" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "select subscription" ON "public"."subscriptions" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id"));
|
||||
|
||||
CREATE POLICY "select_categories_policy" ON "public"."categories" FOR SELECT USING ((EXISTS ( SELECT 1
|
||||
FROM "public"."blogs"
|
||||
WHERE (("blogs"."id" = "categories"."blog_id") AND ("blogs"."user_id" = CURRENT_USER)))));
|
||||
|
||||
ALTER TABLE "public"."subscriptions" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "public"."teams" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "update_categories_policy" ON "public"."categories" FOR UPDATE USING ((EXISTS ( SELECT 1
|
||||
FROM "public"."blogs"
|
||||
WHERE (("blogs"."id" = "categories"."blog_id") AND ("blogs"."user_id" = CURRENT_USER)))));
|
||||
|
||||
CREATE POLICY "users can crud their blogs" ON "public"."blogs" TO "authenticated" USING ((("auth"."uid"())::"text" = "user_id"));
|
||||
|
||||
REVOKE USAGE ON SCHEMA "public" FROM PUBLIC;
|
||||
@@ -215,6 +420,10 @@ GRANT USAGE ON SCHEMA "public" TO "anon";
|
||||
GRANT USAGE ON SCHEMA "public" TO "authenticated";
|
||||
GRANT USAGE ON SCHEMA "public" TO "service_role";
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."generate_random_string"("length" integer) TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."generate_random_string"("length" integer) TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."generate_random_string"("length" integer) TO "service_role";
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."generate_slug"("title" "text") TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."generate_slug"("title" "text") TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."generate_slug"("title" "text") TO "service_role";
|
||||
@@ -223,10 +432,30 @@ GRANT ALL ON FUNCTION "public"."requesting_user_id"() TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."requesting_user_id"() TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."requesting_user_id"() TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."admin_users" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."admin_users" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."admin_users" TO "service_role";
|
||||
|
||||
GRANT ALL ON SEQUENCE "public"."admin_users_id_seq" TO "anon";
|
||||
GRANT ALL ON SEQUENCE "public"."admin_users_id_seq" TO "authenticated";
|
||||
GRANT ALL ON SEQUENCE "public"."admin_users_id_seq" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."blogs" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."blogs" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."blogs" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."categories" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."categories" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."categories" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."feedback" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."feedback" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."feedback" TO "service_role";
|
||||
|
||||
GRANT ALL ON SEQUENCE "public"."feedback_id_seq" TO "anon";
|
||||
GRANT ALL ON SEQUENCE "public"."feedback_id_seq" TO "authenticated";
|
||||
GRANT ALL ON SEQUENCE "public"."feedback_id_seq" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."homepage_signup" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."homepage_signup" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."homepage_signup" TO "service_role";
|
||||
@@ -239,18 +468,34 @@ GRANT ALL ON TABLE "public"."invitations" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."invitations" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."invitations" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."members" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."members" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."members" TO "service_role";
|
||||
|
||||
GRANT ALL ON SEQUENCE "public"."members_id_seq" TO "anon";
|
||||
GRANT ALL ON SEQUENCE "public"."members_id_seq" TO "authenticated";
|
||||
GRANT ALL ON SEQUENCE "public"."members_id_seq" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."posts" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."posts" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."posts" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."products" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."products" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."products" TO "service_role";
|
||||
|
||||
GRANT ALL ON SEQUENCE "public"."products_id_seq" TO "anon";
|
||||
GRANT ALL ON SEQUENCE "public"."products_id_seq" TO "authenticated";
|
||||
GRANT ALL ON SEQUENCE "public"."products_id_seq" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."subscriptions" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."subscriptions" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."subscriptions" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."teams" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."teams" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."teams" TO "service_role";
|
||||
|
||||
GRANT ALL ON SEQUENCE "public"."teams_id_seq" TO "anon";
|
||||
GRANT ALL ON SEQUENCE "public"."teams_id_seq" TO "authenticated";
|
||||
GRANT ALL ON SEQUENCE "public"."teams_id_seq" TO "service_role";
|
||||
|
||||
GRANT ALL ON TABLE "public"."users" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."users" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."users" TO "service_role";
|
||||
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
|
||||
+6
-16
@@ -6,31 +6,21 @@
|
||||
- [] FAQ on homepage
|
||||
- [] Update README
|
||||
- [] Homepage use cases examples: personal blog, docs, careers page, product listing pages, help center, changelogs, etc.
|
||||
- [] email provider
|
||||
- [] supabase spend cap
|
||||
|
||||
## Bugs & Improvements
|
||||
|
||||
- [] First time a user logs in, they should be prompted to add a password and reload the session.
|
||||
- [x] Fix email redirect when navigating to protected routes
|
||||
- [] Force email confirmation
|
||||
- [] Make layout work on mobile
|
||||
- [] Make inputs not zoom in on mobile
|
||||
|
||||
## Teams
|
||||
|
||||
- [] When a user logs in, a team is created for them.
|
||||
- [] Billing is per team.
|
||||
- [] Blogs are owned by a team.
|
||||
|
||||
## Pricing
|
||||
|
||||
- [] Add pricing page
|
||||
- [] Make pricing work
|
||||
|
||||
### Pro plan
|
||||
|
||||
- $50/year (2 months free) or $5/month
|
||||
- 1 editor
|
||||
- Unlimited blogs
|
||||
- Unlimited posts
|
||||
- Unlimited categories
|
||||
- [x] Sync products between Stripe and Supabase
|
||||
- [x] Sync subscriptions between Stripe and Supabase
|
||||
|
||||
## Blogs
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## Ideas
|
||||
|
||||
- [] Allow to fetch list of posts with content.
|
||||
|
||||
```typescript
|
||||
const posts = await client.posts.list({ withContent: true, limit: 10 });
|
||||
```
|
||||
|
||||
- [] Allow users to add default metadata for posts in a blog.
|
||||
- [] Add PolyScale cache to Supabase for faster queries?
|
||||
- [] Featured Images
|
||||
@@ -13,6 +19,12 @@
|
||||
- [] Use a generic on createClient to pass the custom metadata
|
||||
- [] Add files to custom metadata
|
||||
|
||||
## Teams
|
||||
|
||||
- [] When a user logs in, a team is created for them.
|
||||
- [] Billing is per team.
|
||||
- [] Blogs are owned by a team.
|
||||
|
||||
## Hosted blogs
|
||||
|
||||
- [] Let zenblog host your blog for you
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Template ideas for Zenblog
|
||||
|
||||
- [] Simple blog template
|
||||
- [] News website
|
||||
- [] Personal blog
|
||||
- [] Portfolio
|
||||
- [] Company blog
|
||||
- [] Documentation
|
||||
- [] Careers page
|
||||
- [] Multilanguage blog
|
||||
Reference in New Issue
Block a user