diff --git a/.env.example b/.env.example
index 93a9058f..8c033865 100644
--- a/.env.example
+++ b/.env.example
@@ -3,6 +3,10 @@
# See lib/env.ts for the list of required environment variables and how to get them.
###############
+AUTH_GITHUB_CLIENT_ID=
+AUTH_GITHUB_CLIENT_SECRET=
+AUTH_SECRET=
+DATABASE_URL=
GITHUB_TOKEN=
KV_REST_API_TOKEN=
KV_REST_API_URL=
@@ -11,8 +15,6 @@ RESEND_FROM_EMAIL=
RESEND_TO_EMAIL=
TURNSTILE_SECRET_KEY=
NEXT_PUBLIC_BASE_URL=
-NEXT_PUBLIC_GISCUS_CATEGORY_ID=
-NEXT_PUBLIC_GISCUS_REPO_ID=
NEXT_PUBLIC_GITHUB_REPO=
NEXT_PUBLIC_ONION_DOMAIN=
NEXT_PUBLIC_SITE_LOCALE=
diff --git a/.prettierignore b/.prettierignore
index b87eae66..5053ae69 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -8,6 +8,7 @@ pnpm-lock.yaml
.vercel/
# other
+lib/db/migrations/
public/
.devcontainer/devcontainer.json
.vscode/
diff --git a/README.md b/README.md
index 96ce4057..398ca90b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://github.com/jakejarvis/jarv.is)
[](https://jarv.is/api/hits)
-My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Tailwind CSS](https://github.com/user-attachments/assets/dfe99976-c73d-46f1-8a50-f26338463ad8), [Upstash](https://upstash.com/), [Giscus](https://giscus.app/), [and more](https://jarv.is/humans.txt).
+My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Tailwind CSS](https://github.com/user-attachments/assets/dfe99976-c73d-46f1-8a50-f26338463ad8), [Neon Postgres](https://neon.tech/), [Drizzle](https://orm.drizzle.team/), [Better Auth](https://www.better-auth.com/), [and more](https://jarv.is/humans.txt).
## 🕹️ Getting Started
@@ -14,7 +14,7 @@ My humble abode on the World Wide Web, created and deployed using [Next.js](http
I highly recommend spinning up a [Codespace](https://github.com/features/codespaces) with the button above to start inside of a preconfigured and tested environment. But you can also clone this repository locally, run `pnpm install` to pull down the necessary dependencies and `pnpm dev` to start the local server, and then open [localhost:3000](http://localhost:3000/) in a browser. Pages will live-refresh when source files are changed.
-**Be sure to populate the required environment variables!** Refer to [`lib/env.ts`](lib/env.ts), which documents (and strictly [type-checks](https://env.t3.gg/docs/introduction)) these variables. The included [`.env.example`](.env.example) file should be copied and used as a template for a new `.env.local` file, which the local `next dev` server will then ingest.
+**Be sure to populate the required environment variables!** Refer to [`lib/env.ts`](lib/env.ts), which documents (and strictly [type-checks](https://env.t3.gg/docs/introduction)) these variables. The included [`.env.example`](.env.example) file should be copied and used as a template for a new local `.env` file, which the local `next dev` server will then ingest.
> ⚠️ **Currently, there are a few assumptions sprinkled throughout the code that this repo will be deployed to [Vercel](https://nextjs.org/docs/app/building-your-application/deploying#managed-nextjs-with-vercel) and _only_ Vercel.** I'll correct this soon™ now that some escape hatches (namely [OpenNext](https://opennext.js.org/)) actually exist...
diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts
new file mode 100644
index 00000000..7cbe91bb
--- /dev/null
+++ b/app/api/auth/[...all]/route.ts
@@ -0,0 +1,4 @@
+import { auth } from "@/lib/auth";
+import { toNextJsHandler } from "better-auth/next-js";
+
+export const { POST, GET } = toNextJsHandler(auth);
diff --git a/app/api/hits/route.ts b/app/api/hits/route.ts
index f67f77f3..dd4fdbff 100644
--- a/app/api/hits/route.ts
+++ b/app/api/hits/route.ts
@@ -1,7 +1,6 @@
import { NextResponse } from "next/server";
import { unstable_cache as cache } from "next/cache";
-import { getViews as _getViews } from "@/lib/posts";
-import { POSTS_DIR } from "@/lib/config/constants";
+import { getViews as _getViews } from "@/lib/server/views";
const getViews = cache(_getViews, undefined, {
revalidate: 300, // 5 minutes
@@ -25,9 +24,9 @@ export const GET = async (): Promise<
const total = {
hits: Object.values(views).reduce((acc, curr) => acc + curr, 0),
};
- const pages = Object.entries(views).map(([slug, hits]) => ({
- slug: `${POSTS_DIR}/${slug}`,
- hits,
+ const pages = Object.entries(views).map(([slug, views]) => ({
+ slug,
+ hits: views,
}));
pages.sort((a, b) => b.hits - a.hits);
diff --git a/app/cli/page.mdx b/app/cli/page.mdx
index 085158ba..ce32e7ea 100644
--- a/app/cli/page.mdx
+++ b/app/cli/page.mdx
@@ -1,5 +1,4 @@
import PageTitle from "@/components/layout/page-title";
-import Comments from "@/components/comments";
import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({
@@ -35,7 +34,3 @@ npx @jakejarvis/cli
## License
MIT © [Jake Jarvis](https://jarv.is/), [Sindre Sorhus](https://sindresorhus.com/)
-
----
-
-
{posts.map(({ slug, date, title, htmlTitle, views }) => (-
-
+
+
+
+
{views > 0 && (
diff --git a/app/previously/page.mdx b/app/previously/page.mdx
index 71d80263..6be2b0cf 100644
--- a/app/previously/page.mdx
+++ b/app/previously/page.mdx
@@ -1,6 +1,6 @@
import PageTitle from "@/components/layout/page-title";
import Marquee from "@/components/marquee";
-import Comments from "@/components/comments";
+import { Win95Icon } from "@/components/icons";
import { createMetadata } from "@/lib/metadata";
import { ComicNeue } from "@/lib/fonts";
@@ -49,19 +49,6 @@ export const PageStyles = () => (
);
-export const WindowsLogo = () => (
-
-);
-
@@ -75,7 +62,7 @@ _Previously on the [Cringey Chronicles™](https://web.archive.org/web/20010
-[ Click here for the _full_ experience (at your own risk).](https://y2k.pages.dev)
+[ Click here for the _full_ experience (at your own risk).](https://y2k.pages.dev)
diff --git a/app/privacy/page.mdx b/app/privacy/page.mdx
index 0806b1fa..2a86814b 100644
--- a/app/privacy/page.mdx
+++ b/app/privacy/page.mdx
@@ -21,12 +21,16 @@ For a likely excessive level of privacy and security, this website is also mirro
## Analytics
-A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e. `hits = hits + 1`) in a [Upstash Redis](https://upstash.com/) database. Individual views and identifying (or non-identifying) details are **never stored or logged**.
+A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e. `hits = hits + 1`) in a [Neon Postgres](https://neon.tech/) database. Individual views and identifying (or non-identifying) details are **never stored or logged**.
-The [server component](https://github.com/jakejarvis/jarv.is/blob/main/app/notes/%5Bslug%5D/counter.tsx) and [API endpoint](https://github.com/jakejarvis/jarv.is/blob/main/app/api/hits/route.ts) are open source, and [snapshots of the database](https://github.com/jakejarvis/website-stats) are public.
+The [server component](https://github.com/jakejarvis/jarv.is/blob/main/components/view-counter.tsx) and [API endpoint](https://github.com/jakejarvis/jarv.is/blob/main/app/api/hits/route.ts) are open source, and [snapshots of the database](https://github.com/jakejarvis/website-stats) are public.
[**Vercel Analytics**](https://vercel.com/docs/analytics) is also used to gain insights into referrers, search terms, etc. [without collecting anything identifiable](https://vercel.com/docs/analytics/privacy-policy) about you.
+## Comments
+
+Post comments are similarly stored in a Postgres database. Authentication is provided via GitHub, from which the account's username, email, and avatar URL are stored.
+
## Third-Party Content
Occasionally, embedded content from third-party services is included in posts, and some may contain tracking code that is outside of my control. Please refer to their privacy policies for more information:
diff --git a/app/projects/page.tsx b/app/projects/page.tsx
index 5917dcc6..d2d8a1af 100644
--- a/app/projects/page.tsx
+++ b/app/projects/page.tsx
@@ -6,9 +6,10 @@ import PageTitle from "@/components/layout/page-title";
import Link from "@/components/link";
import RelativeTime from "@/components/relative-time";
import ActivityCalendar from "@/components/activity-calendar";
+import { GitHubIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
import { createMetadata } from "@/lib/metadata";
-import { getContributions, getRepos } from "./api";
+import { getContributions, getRepos } from "@/lib/server/github";
export const metadata = createMetadata({
title: "Projects",
@@ -111,15 +112,7 @@ const Page = async () => {
diff --git a/app/zip/page.mdx b/app/zip/page.mdx
index bc1bec3e..89dbe049 100644
--- a/app/zip/page.mdx
+++ b/app/zip/page.mdx
@@ -1,5 +1,4 @@
import PageTitle from "@/components/layout/page-title";
-import Comments from "@/components/comments";
import { createMetadata } from "@/lib/metadata";
import backgroundImg from "./sundar.jpg";
@@ -72,7 +71,3 @@ A little-known monopolistic internet conglomorate simply unleashed [multiple](ht
- **Kaspersky:** [Beware the .zip and .mov domains!](https://usa.kaspersky.com/blog/zip-mov-domain-extension-confusion/28351/)
- **Palo Alto Networks:** [New Top Level Domains .zip and .mov open the door for new attacks](https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA14u000000g1wOCAQ)
- **Google, twenty years ago:** ["Don't be evil"](https://web.archive.org/web/20050204181615/http://investor.google.com/conduct.html)
-
----
-
-
diff --git a/components/activity-calendar.tsx b/components/activity-calendar.tsx
index 86fd00a8..55974a4c 100644
--- a/components/activity-calendar.tsx
+++ b/components/activity-calendar.tsx
@@ -1,7 +1,7 @@
"use client";
import { ActivityCalendar } from "react-activity-calendar";
-import { format } from "date-fns";
+import { formatDate } from "@/lib/date";
import type { ComponentPropsWithoutRef } from "react";
import type { Activity } from "react-activity-calendar";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@@ -48,7 +48,7 @@ const Calendar = ({
{block}
- {`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${format(activity.date, "MMMM do")}`}
+ {`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${formatDate(activity.date, "MMMM do")}`}
)}
diff --git a/components/comments.tsx b/components/comments.tsx
deleted file mode 100644
index 5abd12ad..00000000
--- a/components/comments.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-"use client";
-
-import { env } from "@/lib/env";
-import Giscus from "@giscus/react";
-import type { GiscusProps } from "@giscus/react";
-
-const Comments = ({ title }: { title: string }) => {
- // fail silently if giscus isn't configured
- if (!env.NEXT_PUBLIC_GISCUS_REPO_ID || !env.NEXT_PUBLIC_GISCUS_CATEGORY_ID) {
- console.warn(
- "[giscus] not configured, ensure 'NEXT_PUBLIC_GISCUS_REPO_ID' and 'NEXT_PUBLIC_GISCUS_CATEGORY_ID' environment variables are set."
- );
-
- return null;
- }
-
- return (
-
- );
-};
-
-export default Comments;
diff --git a/components/comments/comment-actions.tsx b/components/comments/comment-actions.tsx
new file mode 100644
index 00000000..10e49605
--- /dev/null
+++ b/components/comments/comment-actions.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import { useState } from "react";
+import { ReplyIcon, EditIcon, Trash2Icon, EllipsisIcon, Loader2Icon } from "lucide-react";
+import { toast } from "sonner";
+import Button from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+ DropdownMenuItem,
+} from "@/components/ui/dropdown-menu";
+import Form from "./comment-form";
+import { useSession } from "@/lib/auth-client";
+import { deleteComment, type CommentWithUser } from "@/lib/server/comments";
+
+const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
+ const [isReplying, setIsReplying] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ const { data: session } = useSession();
+
+ if (!session) return null;
+
+ const handleDelete = async () => {
+ if (!confirm("Are you sure you want to delete this comment?")) return;
+
+ setIsDeleting(true);
+
+ try {
+ await deleteComment(comment.id);
+ toast.success("Your comment has been deleted successfully.");
+ } catch (error) {
+ console.error("Error deleting comment:", error);
+ toast.error("Failed to delete comment. Please try again.");
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ return (
+
+ {isEditing ? (
+
+ );
+};
+
+export default CommentActions;
diff --git a/components/comments/comment-form.tsx b/components/comments/comment-form.tsx
new file mode 100644
index 00000000..77063115
--- /dev/null
+++ b/components/comments/comment-form.tsx
@@ -0,0 +1,194 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { getImageProps } from "next/image";
+import { InfoIcon, Loader2Icon } from "lucide-react";
+import { toast } from "sonner";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import Button from "@/components/ui/button";
+import Textarea from "@/components/ui/textarea";
+import Link from "@/components/link";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { MarkdownIcon } from "@/components/icons";
+import { useSession } from "@/lib/auth-client";
+import { createComment, updateComment } from "@/lib/server/comments";
+import type { FormEvent } from "react";
+
+const CommentForm = ({
+ slug,
+ parentId,
+ commentId,
+ initialContent = "",
+ onCancel,
+ onSuccess,
+}: {
+ slug: string;
+ parentId?: string;
+ commentId?: string;
+ initialContent?: string;
+ onCancel?: () => void;
+ onSuccess?: () => void;
+}) => {
+ const [content, setContent] = useState(initialContent);
+ const [isPending, startTransition] = useTransition();
+ const isEditing = !!commentId;
+ const isReplying = !!parentId;
+
+ const { data: session } = useSession();
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+
+ if (!content.trim()) {
+ toast.error("Comment cannot be empty.");
+ return;
+ }
+
+ startTransition(async () => {
+ try {
+ if (isEditing) {
+ await updateComment(commentId, content);
+ toast.success("Comment updated!");
+ } else {
+ await createComment({
+ content,
+ parentId,
+ pageSlug: slug,
+ });
+ toast.success("Comment posted!");
+ }
+
+ // Reset form if not editing
+ if (!isEditing) {
+ setContent("");
+ }
+
+ // Call success callback if provided
+ onSuccess?.();
+ } catch (error) {
+ console.error("Error submitting comment:", error);
+ toast.error("Failed to submit comment. Please try again.");
+ }
+ });
+ };
+
+ return (
+
+
+ {!isEditing && (
+
+
+ {session?.user.image && (
+
+ )}
+ {session?.user.name.charAt(0).toUpperCase()}
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default CommentForm;
diff --git a/components/comments/comment-single.tsx b/components/comments/comment-single.tsx
new file mode 100644
index 00000000..007366d5
--- /dev/null
+++ b/components/comments/comment-single.tsx
@@ -0,0 +1,71 @@
+import { getImageProps } from "next/image";
+import Markdown from "react-markdown";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import Link from "@/components/link";
+import RelativeTime from "@/components/relative-time";
+import Actions from "./comment-actions";
+import { remarkGfm, remarkSmartypants } from "@/lib/remark";
+import { rehypeExternalLinks } from "@/lib/rehype";
+import { cn } from "@/lib/utils";
+import type { CommentWithUser } from "@/lib/server/comments";
+
+const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
+ const divId = `comment-${comment.id.substring(0, 8)}`;
+
+ return (
+
+
+
+
+ {comment.user.image && (
+
+ )}
+ {comment.user.name.charAt(0).toUpperCase()}
+
+
+
+
+
+
+ @{comment.user.name}
+
+
+
+
+
+
+
+
+ {comment.content}
+
+
+
+
+
+
+
+ );
+};
+
+export default CommentSingle;
diff --git a/components/comments/comment-thread.tsx b/components/comments/comment-thread.tsx
new file mode 100644
index 00000000..f234842e
--- /dev/null
+++ b/components/comments/comment-thread.tsx
@@ -0,0 +1,40 @@
+import Single from "./comment-single";
+import { cn } from "@/lib/utils";
+import type { CommentWithUser } from "@/lib/server/comments";
+
+const CommentThread = ({
+ comment,
+ replies,
+ allComments,
+ level = 0,
+}: {
+ comment: CommentWithUser;
+ replies: CommentWithUser[];
+ allComments: Record;
+ level?: number;
+}) => {
+ // Limit nesting to 3 levels
+ const maxLevel = 2;
+
+ return (
+ <>
+
+
+ {replies.length > 0 && (
+
+ {replies.map((reply) => (
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+export default CommentThread;
diff --git a/components/comments/comments-skeleton.tsx b/components/comments/comments-skeleton.tsx
new file mode 100644
index 00000000..e14ee3c9
--- /dev/null
+++ b/components/comments/comments-skeleton.tsx
@@ -0,0 +1,26 @@
+import Skeleton from "@/components/ui/skeleton";
+
+const CommentsSkeleton = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CommentsSkeleton;
diff --git a/components/comments/comments.tsx b/components/comments/comments.tsx
new file mode 100644
index 00000000..6413e8b7
--- /dev/null
+++ b/components/comments/comments.tsx
@@ -0,0 +1,64 @@
+import { headers } from "next/headers";
+import Form from "./comment-form";
+import Thread from "./comment-thread";
+import SignIn from "./sign-in";
+import { auth } from "@/lib/auth";
+import { getComments, type CommentWithUser } from "@/lib/server/comments";
+
+const Comments = async ({ slug, closed = false }: { slug: string; closed?: boolean }) => {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ const comments = await getComments(slug);
+
+ const commentsByParentId = comments.reduce(
+ (acc, comment) => {
+ const parentId = comment.parentId || "root";
+ if (!acc[parentId]) {
+ acc[parentId] = [];
+ }
+ acc[parentId].push(comment);
+ return acc;
+ },
+ {} as Record
+ );
+
+ const rootComments = commentsByParentId["root"] || [];
+
+ return (
+
+ {closed ? (
+
+
+ ) : !session ? (
+
+
+
+ ) : (
+
+ )}
+
+ {!closed && rootComments.length === 0 ? (
+
+ Be the first to comment!
+
+ ) : (
+
+ {rootComments.map((comment: CommentWithUser) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default Comments;
diff --git a/components/comments/sign-in.tsx b/components/comments/sign-in.tsx
new file mode 100644
index 00000000..b284c1f9
--- /dev/null
+++ b/components/comments/sign-in.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { env } from "@/lib/env";
+import { useState } from "react";
+import { Loader2Icon } from "lucide-react";
+import Button from "@/components/ui/button";
+import { GitHubIcon } from "@/components/icons";
+import { signIn } from "@/lib/auth-client";
+
+const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSignIn = async () => {
+ setIsLoading(true);
+
+ try {
+ await signIn.social({
+ provider: "github",
+ callbackURL: `${env.NEXT_PUBLIC_BASE_URL}${callbackPath ? callbackPath : "/"}`,
+ });
+ } catch (error) {
+ console.error("Error signing in:", error);
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default SignIn;
diff --git a/app/contact/form.tsx b/components/contact-form.tsx
similarity index 86%
rename from app/contact/form.tsx
rename to components/contact-form.tsx
index ec2d117c..fb247bde 100644
--- a/app/contact/form.tsx
+++ b/components/contact-form.tsx
@@ -8,9 +8,9 @@ import Link from "@/components/link";
import Input from "@/components/ui/input";
import Textarea from "@/components/ui/textarea";
import Button from "@/components/ui/button";
+import { MarkdownIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
-
-import { send, type ContactState, type ContactInput } from "./action";
+import { send, type ContactState, type ContactInput } from "@/lib/server/resend";
const ContactForm = () => {
const [formState, formAction, pending] = useActionState(send, {
@@ -79,17 +79,7 @@ const ContactForm = () => {
)}
- {" "}
- Basic{" "}
+ Basic{" "}
Markdown syntax
{" "}
diff --git a/components/icons.tsx b/components/icons.tsx
new file mode 100644
index 00000000..7c5312c1
--- /dev/null
+++ b/components/icons.tsx
@@ -0,0 +1,53 @@
+// miscellaneous icons that are not part of lucide-react
+
+export const Win95Icon = ({ className }: { className?: string }) => (
+
+);
+
+export const MarkdownIcon = ({ className }: { className?: string }) => (
+
+);
+
+export const GitHubIcon = ({ className }: { className?: string }) => (
+
+);
+
+export const NextjsIcon = ({ className }: { className?: string }) => (
+
+);
diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx
index 60370456..be4ca103 100644
--- a/components/layout/footer.tsx
+++ b/components/layout/footer.tsx
@@ -1,6 +1,7 @@
import { env } from "@/lib/env";
import { HeartIcon } from "lucide-react";
import Link from "@/components/link";
+import { NextjsIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
import siteConfig from "@/lib/config/site";
import type { ComponentPropsWithoutRef } from "react";
@@ -33,16 +34,7 @@ const Footer = ({ className, ...rest }: ComponentPropsWithoutRef<"footer">) => {
aria-label="Next.js"
className="text-foreground/85 hover:text-foreground/60 hover:no-underline"
>
-
+
.{" "}
) => {
href="/"
rel="author"
aria-label={siteConfig.name}
- className="hover:text-primary text-foreground/85 flex flex-shrink-0 items-center hover:no-underline"
+ className="hover:text-primary text-foreground/85 flex shrink-0 items-center hover:no-underline"
>
& {
- date: string;
-}) => {
- // play nice with SSR -- only use relative time on the client, since it'll quickly become outdated on the server and
- // cause a react hydration mismatch error.
- const isMounted = useMountedState();
-
- if (!isMounted) {
- return ;
- }
-
- return ;
+const RelativeTime = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+
+
+ );
};
export default RelativeTime;
diff --git a/components/time.tsx b/components/time.tsx
deleted file mode 100644
index 45dcf6a6..00000000
--- a/components/time.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { env } from "@/lib/env";
-import { format, formatISO } from "date-fns";
-import { enUS } from "date-fns/locale";
-import { tz } from "@date-fns/tz";
-import { utc } from "@date-fns/utc";
-import type { ComponentPropsWithoutRef } from "react";
-
-const Time = ({
- date,
- format: formatStr = "PPpp",
- ...rest
-}: ComponentPropsWithoutRef<"time"> & {
- date: string;
- format?: string;
-}) => {
- return (
-
- );
-};
-
-export default Time;
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 00000000..bcc93686
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import type { ComponentPropsWithoutRef } from "react";
+
+const AlertDialog = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const AlertDialogTrigger = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const AlertDialogPortal = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const AlertDialogOverlay = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+const AlertDialogContent = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+
+
+
+ );
+};
+
+const AlertDialogHeader = ({ className, ...rest }: ComponentPropsWithoutRef<"div">) => {
+ return (
+
+ );
+};
+
+const AlertDialogFooter = ({ className, ...rest }: ComponentPropsWithoutRef<"div">) => {
+ return (
+
+ );
+};
+
+const AlertDialogTitle = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+const AlertDialogDescription = ({
+ className,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+const AlertDialogAction = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const AlertDialogCancel = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 00000000..625c8c1b
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import { cn } from "@/lib/utils";
+import type { ComponentPropsWithoutRef } from "react";
+
+const Avatar = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+const AvatarImage = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+const AvatarFallback = ({ className, ...rest }: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index be91d74d..66e10012 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import type { ComponentPropsWithoutRef } from "react";
-const buttonVariants = cva(
+export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
new file mode 100644
index 00000000..d1509690
--- /dev/null
+++ b/components/ui/dropdown-menu.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
+import { cn } from "@/lib/utils";
+import type { ComponentPropsWithoutRef } from "react";
+
+const DropdownMenu = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const DropdownMenuPortal = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const DropdownMenuTrigger = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const DropdownMenuContent = ({
+ className,
+ sideOffset = 4,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+
+
+ );
+};
+
+const DropdownMenuGroup = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const DropdownMenuItem = ({
+ className,
+ inset,
+ variant = "default",
+ ...rest
+}: ComponentPropsWithoutRef & {
+ inset?: boolean;
+ variant?: "default" | "destructive";
+}) => {
+ return (
+
+ );
+};
+
+const DropdownMenuCheckboxItem = ({
+ className,
+ children,
+ checked,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+};
+
+const DropdownMenuRadioGroup = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const DropdownMenuRadioItem = ({
+ className,
+ children,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+};
+
+const DropdownMenuLabel = ({
+ className,
+ inset,
+ ...rest
+}: ComponentPropsWithoutRef & {
+ inset?: boolean;
+}) => {
+ return (
+
+ );
+};
+
+const DropdownMenuSeparator = ({
+ className,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+const DropdownMenuShortcut = ({ className, ...rest }: ComponentPropsWithoutRef<"span">) => {
+ return (
+
+ );
+};
+
+const DropdownMenuSub = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const DropdownMenuSubTrigger = ({
+ className,
+ inset,
+ children,
+ ...rest
+}: ComponentPropsWithoutRef & {
+ inset?: boolean;
+}) => {
+ return (
+
+ {children}
+
+
+ );
+};
+
+const DropdownMenuSubContent = ({
+ className,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+};
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
new file mode 100644
index 00000000..c1af30f6
--- /dev/null
+++ b/components/ui/popover.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+import { cn } from "@/lib/utils";
+import type { ComponentPropsWithoutRef } from "react";
+
+const Popover = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const PopoverTrigger = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+const PopoverContent = ({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+
+
+ );
+};
+
+const PopoverAnchor = ({ ...rest }: ComponentPropsWithoutRef) => {
+ return ;
+};
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
new file mode 100644
index 00000000..3cd2acaf
--- /dev/null
+++ b/components/ui/separator.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import { cn } from "@/lib/utils";
+import type { ComponentPropsWithoutRef } from "react";
+
+const Separator = ({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...rest
+}: ComponentPropsWithoutRef) => {
+ return (
+
+ );
+};
+
+export default Separator;
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
new file mode 100644
index 00000000..94a0a10d
--- /dev/null
+++ b/components/ui/skeleton.tsx
@@ -0,0 +1,8 @@
+import { cn } from "@/lib/utils";
+import type { ComponentPropsWithoutRef } from "react";
+
+const Skeleton = ({ className, ...rest }: ComponentPropsWithoutRef<"div">) => {
+ return ;
+};
+
+export default Skeleton;
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
new file mode 100644
index 00000000..5532f447
--- /dev/null
+++ b/components/ui/sonner.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { Toaster as Sonner, type ToasterProps } from "sonner";
+
+const Toaster = ({ ...rest }: ToasterProps) => {
+ return (
+
+ );
+};
+
+export default Toaster;
diff --git a/components/view-counter.tsx b/components/view-counter.tsx
index 37728eda..314061e1 100644
--- a/components/view-counter.tsx
+++ b/components/view-counter.tsx
@@ -1,19 +1,14 @@
import { env } from "@/lib/env";
import { connection } from "next/server";
-import { kv } from "@vercel/kv";
import CountUp from "@/components/count-up";
+import { incrementViews } from "@/lib/server/views";
const ViewCounter = async ({ slug }: { slug: string }) => {
// ensure this component isn't triggered by prerenders and/or preloads
await connection();
try {
- // if this is a new slug, redis will automatically create a new key and set its value to 0 (and then 1, obviously)
- // https://upstash.com/docs/redis/sdks/ts/commands/string/incr
- // TODO: maybe don't allow this? or maybe it's fine? kinda unclear how secure this is:
- // https://nextjs.org/blog/security-nextjs-server-components-actions
- // https://nextjs.org/docs/app/building-your-application/rendering/server-components
- const hits = await kv.incr(`hits:${slug}`);
+ const hits = await incrementViews(slug);
// we have data!
return (
diff --git a/drizzle.config.ts b/drizzle.config.ts
new file mode 100644
index 00000000..bcc2d4bf
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,11 @@
+import "dotenv/config";
+import { defineConfig } from "drizzle-kit";
+
+export default defineConfig({
+ schema: "./lib/db/schema.ts",
+ out: "./lib/db/migrations",
+ dialect: "postgresql",
+ dbCredentials: {
+ url: process.env.DATABASE_URL!,
+ },
+});
diff --git a/eslint.config.mjs b/eslint.config.mjs
index d2804ac4..2b17225e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -14,10 +14,16 @@ const compat = new FlatCompat({
/** @type {import("eslint").Linter.Config[]} */
export default [
- { ignores: ["README.md", ".next", ".vercel", "node_modules"] },
+ { ignores: ["README.md", ".next", ".vercel", "node_modules", "lib/db/migrations"] },
...compat.config({
plugins: ["react-compiler", "css-modules"],
- extends: ["eslint:recommended", "next/core-web-vitals", "next/typescript", "plugin:css-modules/recommended"],
+ extends: [
+ "eslint:recommended",
+ "next/core-web-vitals",
+ "next/typescript",
+ "plugin:css-modules/recommended",
+ "plugin:drizzle/recommended",
+ ],
}),
...eslintCustomConfig,
eslintPluginPrettierRecommended,
diff --git a/lib/auth-client.ts b/lib/auth-client.ts
new file mode 100644
index 00000000..e2bf89f2
--- /dev/null
+++ b/lib/auth-client.ts
@@ -0,0 +1,8 @@
+import { env } from "@/lib/env";
+import { createAuthClient } from "better-auth/react";
+
+export const authClient = createAuthClient({
+ baseURL: env.NEXT_PUBLIC_BASE_URL,
+});
+
+export const { signIn, signUp, useSession } = authClient;
diff --git a/lib/auth.ts b/lib/auth.ts
new file mode 100644
index 00000000..8e411e05
--- /dev/null
+++ b/lib/auth.ts
@@ -0,0 +1,31 @@
+import { env } from "@/lib/env";
+import { betterAuth, type BetterAuthOptions } from "better-auth";
+import { nextCookies } from "better-auth/next-js";
+import { drizzleAdapter } from "better-auth/adapters/drizzle";
+import { db } from "@/lib/db";
+import * as schema from "@/lib/db/schema";
+
+export const auth = betterAuth({
+ baseURL: env.NEXT_PUBLIC_BASE_URL,
+ database: drizzleAdapter(db, {
+ provider: "pg",
+ schema,
+ }),
+ plugins: [nextCookies()],
+ socialProviders: {
+ github: {
+ clientId: env.AUTH_GITHUB_CLIENT_ID,
+ clientSecret: env.AUTH_GITHUB_CLIENT_SECRET,
+ scope: ["read:user"],
+ disableDefaultScope: true,
+ mapProfileToUser(profile) {
+ return {
+ name: profile.login,
+ email: profile.email,
+ emailVerified: true,
+ image: profile.avatar_url,
+ };
+ },
+ },
+ },
+} satisfies BetterAuthOptions);
diff --git a/lib/date.ts b/lib/date.ts
new file mode 100644
index 00000000..3b3b5109
--- /dev/null
+++ b/lib/date.ts
@@ -0,0 +1,13 @@
+import { env } from "@/lib/env";
+import { format, formatISO } from "date-fns";
+import { enUS } from "date-fns/locale";
+import { tz } from "@date-fns/tz";
+import { utc } from "@date-fns/utc";
+
+export const formatDate = (date: string | number | Date, formatStr = "PPpp") => {
+ return format(date, formatStr, { in: tz(env.NEXT_PUBLIC_SITE_TZ), locale: enUS });
+};
+
+export const formatDateISO = (date: string | number | Date) => {
+ return formatISO(date, { in: utc });
+};
diff --git a/lib/db/index.ts b/lib/db/index.ts
new file mode 100644
index 00000000..ad52ae01
--- /dev/null
+++ b/lib/db/index.ts
@@ -0,0 +1,6 @@
+import { drizzle } from "drizzle-orm/neon-http";
+import { neon } from "@neondatabase/serverless";
+
+const sql = neon(process.env.DATABASE_URL!);
+
+export const db = drizzle({ client: sql });
diff --git a/lib/db/migrations/0000_puzzling_sphinx.sql b/lib/db/migrations/0000_puzzling_sphinx.sql
new file mode 100644
index 00000000..73903c63
--- /dev/null
+++ b/lib/db/migrations/0000_puzzling_sphinx.sql
@@ -0,0 +1,68 @@
+CREATE TABLE "account" (
+ "id" text PRIMARY KEY NOT NULL,
+ "account_id" text NOT NULL,
+ "provider_id" text NOT NULL,
+ "user_id" text NOT NULL,
+ "access_token" text,
+ "refresh_token" text,
+ "id_token" text,
+ "access_token_expires_at" timestamp,
+ "refresh_token_expires_at" timestamp,
+ "scope" text,
+ "password" text,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "comment" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "content" text NOT NULL,
+ "page_slug" text NOT NULL,
+ "parent_id" uuid,
+ "user_id" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "page" (
+ "slug" text PRIMARY KEY NOT NULL,
+ "views" integer DEFAULT 1 NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "session" (
+ "id" text PRIMARY KEY NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "token" text NOT NULL,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ "ip_address" text,
+ "user_agent" text,
+ "user_id" text NOT NULL,
+ CONSTRAINT "session_token_unique" UNIQUE("token")
+);
+--> statement-breakpoint
+CREATE TABLE "user" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "email" text NOT NULL,
+ "email_verified" boolean NOT NULL,
+ "image" text,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ CONSTRAINT "user_email_unique" UNIQUE("email")
+);
+--> statement-breakpoint
+CREATE TABLE "verification" (
+ "id" text PRIMARY KEY NOT NULL,
+ "identifier" text NOT NULL,
+ "value" text NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "created_at" timestamp,
+ "updated_at" timestamp
+);
+--> statement-breakpoint
+ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "comment" ADD CONSTRAINT "comment_page_slug_page_slug_fk" FOREIGN KEY ("page_slug") REFERENCES "public"."page"("slug") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "comment" ADD CONSTRAINT "comment_parent_id_comment_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."comment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
\ No newline at end of file
diff --git a/lib/db/migrations/meta/0000_snapshot.json b/lib/db/migrations/meta/0000_snapshot.json
new file mode 100644
index 00000000..c2a9551f
--- /dev/null
+++ b/lib/db/migrations/meta/0000_snapshot.json
@@ -0,0 +1,443 @@
+{
+ "id": "d126927d-35cf-4b6f-ab97-3872b8db26a7",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.comment": {
+ "name": "comment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "page_slug": {
+ "name": "page_slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "comment_page_slug_page_slug_fk": {
+ "name": "comment_page_slug_page_slug_fk",
+ "tableFrom": "comment",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_slug"
+ ],
+ "columnsTo": [
+ "slug"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "comment_parent_id_comment_id_fk": {
+ "name": "comment_parent_id_comment_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "comment",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comment_user_id_user_id_fk": {
+ "name": "comment_user_id_user_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.page": {
+ "name": "page",
+ "schema": "",
+ "columns": {
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json
new file mode 100644
index 00000000..3783072d
--- /dev/null
+++ b/lib/db/migrations/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1747229716675,
+ "tag": "0000_puzzling_sphinx",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
new file mode 100644
index 00000000..0b69682a
--- /dev/null
+++ b/lib/db/schema.ts
@@ -0,0 +1,70 @@
+import { pgTable, text, timestamp, boolean, integer, uuid, AnyPgColumn } from "drizzle-orm/pg-core";
+
+export const user = pgTable("user", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ email: text("email").notNull().unique(),
+ emailVerified: boolean("email_verified").notNull(),
+ image: text("image"),
+ createdAt: timestamp("created_at").notNull(),
+ updatedAt: timestamp("updated_at").notNull(),
+});
+
+export const session = pgTable("session", {
+ id: text("id").primaryKey(),
+ expiresAt: timestamp("expires_at").notNull(),
+ token: text("token").notNull().unique(),
+ createdAt: timestamp("created_at").notNull(),
+ updatedAt: timestamp("updated_at").notNull(),
+ ipAddress: text("ip_address"),
+ userAgent: text("user_agent"),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+});
+
+export const account = pgTable("account", {
+ id: text("id").primaryKey(),
+ accountId: text("account_id").notNull(),
+ providerId: text("provider_id").notNull(),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ accessToken: text("access_token"),
+ refreshToken: text("refresh_token"),
+ idToken: text("id_token"),
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
+ scope: text("scope"),
+ password: text("password"),
+ createdAt: timestamp("created_at").notNull(),
+ updatedAt: timestamp("updated_at").notNull(),
+});
+
+export const verification = pgTable("verification", {
+ id: text("id").primaryKey(),
+ identifier: text("identifier").notNull(),
+ value: text("value").notNull(),
+ expiresAt: timestamp("expires_at").notNull(),
+ createdAt: timestamp("created_at"),
+ updatedAt: timestamp("updated_at"),
+});
+
+export const page = pgTable("page", {
+ slug: text("slug").primaryKey(),
+ views: integer("views").notNull().default(1),
+});
+
+export const comment = pgTable("comment", {
+ id: uuid("id").defaultRandom().primaryKey(),
+ content: text("content").notNull(),
+ pageSlug: text("page_slug")
+ .notNull()
+ .references(() => page.slug),
+ parentId: uuid("parent_id").references((): AnyPgColumn => comment.id, { onDelete: "cascade" }),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ createdAt: timestamp("created_at").notNull().defaultNow(),
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
+});
diff --git a/lib/env.ts b/lib/env.ts
index 779e92d2..cc38f263 100644
--- a/lib/env.ts
+++ b/lib/env.ts
@@ -3,6 +3,35 @@ import * as v from "valibot";
export const env = createEnv({
server: {
+ /**
+ * Required. A random value used for authentication encryption.
+ *
+ * @see https://www.better-auth.com/docs/installation#set-environment-variables
+ */
+ AUTH_SECRET: v.string(),
+
+ /**
+ * Required. The client ID from the GitHub Developer Portal for this site's OAuth App.
+ *
+ * @see https://www.better-auth.com/docs/authentication/github
+ */
+ AUTH_GITHUB_CLIENT_ID: v.string(),
+
+ /**
+ * Required. A client secret from the GitHub Developer Portal for this site's OAuth App.
+ *
+ * @see https://www.better-auth.com/docs/authentication/github
+ */
+ AUTH_GITHUB_CLIENT_SECRET: v.string(),
+
+ /**
+ * Required. Database connection string for a Postgres database. May be set automatically by Vercel's Neon
+ * integration.
+ *
+ * @see https://vercel.com/integrations/neon
+ */
+ DATABASE_URL: v.pipe(v.string(), v.startsWith("postgres://")),
+
/**
* Required. GitHub API token used for [/projects](../app/projects/page.tsx) grid. Only needs the `public_repo`
* scope since we don't need/want to change anything, obviously.
@@ -11,24 +40,6 @@ export const env = createEnv({
*/
GITHUB_TOKEN: v.optional(v.pipe(v.string(), v.startsWith("ghp_"))),
- /**
- * Required. Redis storage credentials for hit counter's [server component](../app/notes/[slug]/counter.tsx) and API
- * endpoint. Currently set automatically by Vercel's Upstash integration.
- *
- * @see https://upstash.com/docs/redis/sdks/ts/getstarted
- * @see https://vercel.com/marketplace/upstash
- */
- KV_REST_API_TOKEN: v.string(),
-
- /**
- * Required. Redis storage credentials for hit counter's [server component](../app/notes/[slug]/counter.tsx) and API
- * endpoint. Currently set automatically by Vercel's Upstash integration.
- *
- * @see https://upstash.com/docs/redis/sdks/ts/getstarted
- * @see https://vercel.com/marketplace/upstash
- */
- KV_REST_API_URL: v.pipe(v.string(), v.url(), v.startsWith("https://"), v.endsWith(".upstash.io")),
-
/**
* Required. Uses Resend API to send contact form submissions via a [server action](../app/contact/action.ts). May
* be set automatically by Vercel's Resend integration.
@@ -102,20 +113,6 @@ export const env = createEnv({
: "development"
),
- /**
- * Optional. Enables comments on blog posts via GitHub discussions.
- *
- * @see https://giscus.app/
- */
- NEXT_PUBLIC_GISCUS_CATEGORY_ID: v.optional(v.string()),
-
- /**
- * Optional. Enables comments on blog posts via GitHub discussions.
- *
- * @see https://giscus.app/
- */
- NEXT_PUBLIC_GISCUS_REPO_ID: v.optional(v.string()),
-
/** Required. GitHub repository for the site in the format of `{username}/{repo}`. */
NEXT_PUBLIC_GITHUB_REPO: v.pipe(v.string(), v.includes("/")),
@@ -157,8 +154,6 @@ export const env = createEnv({
experimental__runtimeEnv: {
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_ENV: process.env.NEXT_PUBLIC_ENV,
- NEXT_PUBLIC_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
- NEXT_PUBLIC_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_GISCUS_REPO_ID,
NEXT_PUBLIC_GITHUB_REPO: process.env.NEXT_PUBLIC_GITHUB_REPO,
NEXT_PUBLIC_GITHUB_USERNAME: process.env.NEXT_PUBLIC_GITHUB_USERNAME,
NEXT_PUBLIC_ONION_DOMAIN: process.env.NEXT_PUBLIC_ONION_DOMAIN,
diff --git a/lib/posts.ts b/lib/posts.ts
index 43919143..d90fc02e 100644
--- a/lib/posts.ts
+++ b/lib/posts.ts
@@ -1,10 +1,10 @@
import { env } from "@/lib/env";
import { cache } from "react";
-import { kv } from "@vercel/kv";
import path from "path";
import fs from "fs/promises";
import glob from "fast-glob";
import { unified } from "unified";
+import { decode } from "html-entities";
import {
remarkParse,
remarkSmartypants,
@@ -13,10 +13,8 @@ import {
remarkMdx,
remarkStripMdxImportsExports,
} from "@/lib/remark";
-import { decode } from "html-entities";
+import { rehypeSanitize, rehypeStringify } from "@/lib/rehype";
import { POSTS_DIR } from "@/lib/config/constants";
-import rehypeSanitize from "rehype-sanitize";
-import rehypeStringify from "rehype-stringify";
export type FrontMatter = {
slug: string;
@@ -112,57 +110,6 @@ export const getFrontMatter: {
}
);
-export const getViews: {
- /**
- * Retrieves the number of views for ALL posts
- */
- (): Promise>;
- /**
- * Retrieves the number of views for a given slug, or undefined if the slug does not exist
- */
- (slug: string): Promise;
-} = cache(
- async (
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- slug?: any
- ): // eslint-disable-next-line @typescript-eslint/no-explicit-any
- Promise => {
- // ensure the prefix is consistent for all keys in the KV store
- const KEY_PREFIX = `hits:${POSTS_DIR}/`;
-
- if (typeof slug === "string") {
- try {
- const views = await kv.get(`${KEY_PREFIX}${slug}`);
-
- return views ? parseInt(views, 10) : undefined;
- } catch (error) {
- console.error(`Failed to retrieve view count for post with slug "${slug}":`, error);
- return undefined;
- }
- }
-
- if (!slug) {
- try {
- const allSlugs = await getSlugs();
- const pages: Record = {};
-
- // get the value (number of views) for each key (the slug of the page)
- const values = await kv.mget(...allSlugs.map((slug) => `${KEY_PREFIX}${slug}`));
-
- // pair the slugs with their view counts
- allSlugs.forEach((slug, index) => (pages[slug.replace(KEY_PREFIX, "")] = parseInt(values[index], 10)));
-
- return pages;
- } catch (error) {
- console.error("Failed to retrieve view counts:", error);
- return undefined;
- }
- }
-
- throw new Error("getViews() called with invalid argument.");
- }
-);
-
/** Returns the content of a post with very limited processing to include in RSS feeds */
export const getContent = cache(async (slug: string): Promise => {
try {
@@ -182,6 +129,7 @@ export const getContent = cache(async (slug: string): Promise;
+};
+
+export const getComments = async (pageSlug: string): Promise => {
+ try {
+ // Fetch all comments for the page with user details
+ const commentsWithUsers = await db
+ .select()
+ .from(schema.comment)
+ .innerJoin(schema.user, eq(schema.comment.userId, schema.user.id))
+ .where(eq(schema.comment.pageSlug, pageSlug))
+ .orderBy(desc(schema.comment.createdAt));
+
+ return commentsWithUsers.map(({ comment, user }) => ({
+ ...comment,
+ user: {
+ // we're namely worried about keeping the user's email private here, but nothing sensitive is stored in the db
+ id: user.id,
+ name: user.name,
+ image: user.image,
+ },
+ }));
+ } catch (error) {
+ console.error("[server/comments] error fetching comments:", error);
+ throw new Error("Failed to fetch comments");
+ }
+};
+
+export const createComment = async (data: { content: string; pageSlug: string; parentId?: string }) => {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session || !session.user) {
+ throw new Error("You must be logged in to comment");
+ }
+
+ try {
+ // Insert the comment
+ await db.insert(schema.comment).values({
+ content: data.content,
+ pageSlug: data.pageSlug,
+ parentId: data.parentId || null,
+ userId: session.user.id,
+ });
+
+ // Revalidate the page to show the new comment
+ revalidatePath(`/${data.pageSlug}`);
+ } catch (error) {
+ console.error("[server/comments] error creating comment:", error);
+ throw new Error("Failed to create comment");
+ }
+};
+
+export const updateComment = async (commentId: string, content: string) => {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session || !session.user) {
+ throw new Error("You must be logged in to update a comment");
+ }
+
+ try {
+ // Get the comment to verify ownership
+ const comment = await db
+ .select({ userId: schema.comment.userId, pageSlug: schema.comment.pageSlug })
+ .from(schema.comment)
+ .where(eq(schema.comment.id, commentId))
+ .then((results) => results[0]);
+
+ if (!comment) {
+ throw new Error("Comment not found");
+ }
+
+ // Verify ownership
+ if (comment.userId !== session.user.id) {
+ throw new Error("You can only edit your own comments");
+ }
+
+ // Update the comment
+ await db
+ .update(schema.comment)
+ .set({
+ content,
+ updatedAt: new Date(),
+ })
+ .where(eq(schema.comment.id, commentId));
+
+ // Revalidate the page to show the updated comment
+ revalidatePath(`/${comment.pageSlug}`);
+ } catch (error) {
+ console.error("[server/comments] error updating comment:", error);
+ throw new Error("Failed to update comment");
+ }
+};
+
+export const deleteComment = async (commentId: string) => {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session || !session.user) {
+ throw new Error("You must be logged in to delete a comment");
+ }
+
+ try {
+ // Get the comment to verify ownership and get the page_slug for revalidation
+ const comment = await db
+ .select({ userId: schema.comment.userId, pageSlug: schema.comment.pageSlug })
+ .from(schema.comment)
+ .where(eq(schema.comment.id, commentId))
+ .then((results) => results[0]);
+
+ if (!comment) {
+ throw new Error("Comment not found");
+ }
+
+ // Verify ownership
+ if (comment.userId !== session.user.id) {
+ throw new Error("You can only delete your own comments");
+ }
+
+ // Delete the comment
+ await db.delete(schema.comment).where(eq(schema.comment.id, commentId));
+
+ // Revalidate the page to update the comments list
+ revalidatePath(`/${comment.pageSlug}`);
+ } catch (error) {
+ console.error("[server/comments] error deleting comment:", error);
+ throw new Error("Failed to delete comment");
+ }
+};
diff --git a/app/projects/api.ts b/lib/server/github.ts
similarity index 93%
rename from app/projects/api.ts
rename to lib/server/github.ts
index dfd371e8..c820c2e4 100644
--- a/app/projects/api.ts
+++ b/lib/server/github.ts
@@ -1,4 +1,3 @@
-// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment
import "server-only";
import { env } from "@/lib/env";
@@ -73,7 +72,7 @@ export const getContributions = async (): Promise<
};
});
} catch (error) {
- console.error("[/projects] Failed to fetch contributions:", error);
+ console.error("[server/github] Failed to fetch contributions:", error);
return [];
}
};
@@ -138,7 +137,7 @@ export const getRepos = async (): Promise => {
return user.repositories.edges?.map((edge) => edge!.node as Repository);
} catch (error) {
- console.error("[/projects] Failed to fetch repositories:", error);
+ console.error("[server/github] Failed to fetch repositories:", error);
return [];
}
};
diff --git a/app/contact/action.ts b/lib/server/resend.ts
similarity index 91%
rename from app/contact/action.ts
rename to lib/server/resend.ts
index f82726fd..bcfe71ec 100644
--- a/app/contact/action.ts
+++ b/lib/server/resend.ts
@@ -36,7 +36,7 @@ export type ContactState = {
export const send = async (state: ContactState, payload: FormData): Promise => {
// TODO: remove after debugging why automated spam entries are causing 500 errors
- console.debug("[/contact] received payload:", payload);
+ console.debug("[server/resend] received payload:", payload);
try {
const data = v.safeParse(ContactSchema, Object.fromEntries(payload));
@@ -68,7 +68,7 @@ export const send = async (state: ContactState, payload: FormData): Promise => {
+ try {
+ // First, try to find the existing record
+ const existingHit = await db.select().from(page).where(eq(page.slug, slug)).limit(1);
+
+ if (existingHit.length === 0) {
+ // Create new record if it doesn't exist
+ await db.insert(page).values({ slug, views: 1 }).execute();
+
+ return 1; // New record starts with 1 hit
+ } else {
+ // Calculate new hit count
+ const newViewCount = existingHit[0].views + 1;
+
+ // Update existing record by incrementing hits
+ await db.update(page).set({ views: newViewCount }).where(eq(page.slug, slug)).execute();
+
+ return newViewCount;
+ }
+ } catch (error) {
+ console.error("[view-counter] fatal error:", error);
+ throw new Error("Failed to increment views");
+ }
+};
+
+export const getViews: {
+ /**
+ * Retrieves the number of views for a given slug, or null if the slug does not exist
+ */
+ (slug: string): Promise;
+ /**
+ * Retrieves the numbers of views for an array of slugs
+ */
+ (slug: string[]): Promise>;
+ /**
+ * Retrieves the numbers of views for ALL slugs
+ */
+ (): Promise>;
+} = cache(
+ async (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ slug?: any
+ ): // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ Promise => {
+ try {
+ // return one page
+ if (typeof slug === "string") {
+ const pages = await db.select().from(page).where(eq(page.slug, slug)).limit(1);
+ return pages[0].views;
+ }
+
+ // return multiple pages
+ if (Array.isArray(slug)) {
+ const pages = await db.select().from(page).where(inArray(page.slug, slug));
+ return pages.reduce(
+ (acc, page, index) => {
+ acc[slug[index]] = page.views;
+ return acc;
+ },
+ {} as Record
+ );
+ }
+
+ // return ALL pages
+ const pages = await db.select().from(page);
+ return pages.reduce(
+ (acc, page) => {
+ acc[page.slug] = page.views;
+ return acc;
+ },
+ {} as Record
+ );
+ } catch (error) {
+ console.error("[server/views] fatal error:", error);
+ throw new Error("Failed to get views");
+ }
+ }
+);
diff --git a/next.config.ts b/next.config.ts
index 4cc4fe0a..2907b047 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -20,9 +20,10 @@ const nextConfig = {
{
protocol: "https",
hostname: "ijyxfbpcm3itvdly.public.blob.vercel-storage.com",
- port: "",
- pathname: "/**",
- search: "",
+ },
+ {
+ protocol: "https",
+ hostname: "avatars.githubusercontent.com",
},
],
},
diff --git a/notes/coronavirus-open-source/index.mdx b/notes/coronavirus-open-source/index.mdx
index 14d616d7..4c7db8fa 100644
--- a/notes/coronavirus-open-source/index.mdx
+++ b/notes/coronavirus-open-source/index.mdx
@@ -13,6 +13,8 @@ tags:
image: ./covid19dashboards.png
---
+import { GitHubIcon } from "@/components/icons";
+
export const OctocatLink = ({ repo }) => {
return (
{
rel="noopener noreferrer"
className="mx-1.5"
>
-
+
);
};
diff --git a/package.json b/package.json
index 017a5c6d..2174b51c 100644
--- a/package.json
+++ b/package.json
@@ -20,25 +20,32 @@
"dependencies": {
"@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
- "@giscus/react": "^3.1.0",
"@marsidev/react-turnstile": "^1.1.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
+ "@neondatabase/serverless": "^1.0.0",
"@next/bundle-analyzer": "15.4.0-canary.26",
"@next/mdx": "15.4.0-canary.26",
"@octokit/graphql": "^8.2.2",
"@octokit/graphql-schema": "^15.26.0",
+ "@radix-ui/react-alert-dialog": "^1.1.13",
+ "@radix-ui/react-avatar": "^1.1.9",
+ "@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-popover": "^1.1.13",
+ "@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2",
+ "@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-tooltip": "^1.2.6",
"@t3-oss/env-nextjs": "^0.13.4",
"@vercel/analytics": "^1.5.0",
- "@vercel/kv": "^3.0.0",
+ "better-auth": "^1.2.7",
"cheerio": "^1.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"date-fns": "^4.1.0",
+ "drizzle-orm": "^0.43.1",
"fast-glob": "^3.3.3",
"feed": "^4.2.2",
"geist": "^1.4.2",
@@ -50,11 +57,13 @@
"react-countup": "^6.5.3",
"react-dom": "19.1.0",
"react-lite-youtube-embed": "^2.5.1",
+ "react-markdown": "^10.1.0",
"react-schemaorg": "^2.0.0",
"react-timeago": "^8.2.0",
"react-to-text": "^2.0.1",
"react-tweet": "^3.2.2",
"react-use": "^17.6.0",
+ "rehype-external-links": "^3.0.0",
"rehype-mdx-code-props": "^3.0.1",
"rehype-mdx-import-media": "^1.2.0",
"rehype-sanitize": "^6.0.0",
@@ -73,6 +82,7 @@
"resend": "^4.5.1",
"server-only": "0.0.1",
"shiki": "^3.4.0",
+ "sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
"unified": "^11.0.5",
@@ -89,10 +99,13 @@
"@types/react-dom": "19.1.3",
"babel-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417",
"cross-env": "^7.0.3",
+ "dotenv": "^16.5.0",
+ "drizzle-kit": "^0.31.1",
"eslint": "^9.26.0",
"eslint-config-next": "15.4.0-canary.26",
"eslint-config-prettier": "^10.1.3",
"eslint-plugin-css-modules": "^2.12.0",
+ "eslint-plugin-drizzle": "^0.2.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-mdx": "^3.4.1",
@@ -130,6 +143,18 @@
"react": "^19",
"react-dom": "^19"
}
- }
+ },
+ "ignoredOptionalDependencies": [
+ "prisma",
+ "@prisma/client"
+ ],
+ "ignoredBuiltDependencies": [
+ "unrs-resolver"
+ ],
+ "onlyBuiltDependencies": [
+ "@tailwindcss/oxide",
+ "esbuild",
+ "sharp"
+ ]
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dec331e7..61d13094 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,9 +14,6 @@ importers:
'@date-fns/utc':
specifier: ^2.1.0
version: 2.1.0
- '@giscus/react':
- specifier: ^3.1.0
- version: 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@marsidev/react-turnstile':
specifier: ^1.1.0
version: 1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -26,6 +23,9 @@ importers:
'@mdx-js/react':
specifier: ^3.1.0
version: 3.1.0(@types/react@19.1.3)(react@19.1.0)
+ '@neondatabase/serverless':
+ specifier: ^1.0.0
+ version: 1.0.0
'@next/bundle-analyzer':
specifier: 15.4.0-canary.26
version: 15.4.0-canary.26
@@ -38,12 +38,30 @@ importers:
'@octokit/graphql-schema':
specifier: ^15.26.0
version: 15.26.0
+ '@radix-ui/react-alert-dialog':
+ specifier: ^1.1.13
+ version: 1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-avatar':
+ specifier: ^1.1.9
+ version: 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.14
+ version: 2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-label':
specifier: ^2.1.6
version: 2.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-popover':
+ specifier: ^1.1.13
+ version: 1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.6
+ version: 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.2.2
version: 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-toast':
+ specifier: ^1.2.13
+ version: 1.2.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-tooltip':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -53,9 +71,9 @@ importers:
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@15.4.0-canary.26(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
- '@vercel/kv':
- specifier: ^3.0.0
- version: 3.0.0
+ better-auth:
+ specifier: ^1.2.7
+ version: 1.2.7
cheerio:
specifier: ^1.0.0
version: 1.0.0
@@ -71,6 +89,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
+ drizzle-orm:
+ specifier: ^0.43.1
+ version: 0.43.1(@neondatabase/serverless@1.0.0)(@types/pg@8.15.1)(kysely@0.27.6)
fast-glob:
specifier: ^3.3.3
version: 3.3.3
@@ -104,6 +125,9 @@ importers:
react-lite-youtube-embed:
specifier: ^2.5.1
version: 2.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react-markdown:
+ specifier: ^10.1.0
+ version: 10.1.0(@types/react@19.1.3)(react@19.1.0)
react-schemaorg:
specifier: ^2.0.0
version: 2.0.0(react@19.1.0)(schema-dts@1.1.5)(typescript@5.8.3)
@@ -119,6 +143,9 @@ importers:
react-use:
specifier: ^17.6.0
version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ rehype-external-links:
+ specifier: ^3.0.0
+ version: 3.0.0
rehype-mdx-code-props:
specifier: ^3.0.1
version: 3.0.1
@@ -173,6 +200,9 @@ importers:
shiki:
specifier: ^3.4.0
version: 3.4.0
+ sonner:
+ specifier: ^2.0.3
+ version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tailwind-merge:
specifier: ^3.2.0
version: 3.2.0
@@ -216,6 +246,12 @@ importers:
cross-env:
specifier: ^7.0.3
version: 7.0.3
+ dotenv:
+ specifier: ^16.5.0
+ version: 16.5.0
+ drizzle-kit:
+ specifier: ^0.31.1
+ version: 0.31.1
eslint:
specifier: ^9.26.0
version: 9.26.0(jiti@2.4.2)
@@ -228,6 +264,9 @@ importers:
eslint-plugin-css-modules:
specifier: ^2.12.0
version: 2.12.0(eslint@9.26.0(jiti@2.4.2))
+ eslint-plugin-drizzle:
+ specifier: ^0.2.3
+ version: 0.2.3(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-import:
specifier: ^2.31.0
version: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.26.0(jiti@2.4.2))
@@ -397,6 +436,12 @@ packages:
resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
engines: {node: '>=6.9.0'}
+ '@better-auth/utils@0.2.4':
+ resolution: {integrity: sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==}
+
+ '@better-fetch/fetch@1.1.18':
+ resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
+
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
@@ -407,6 +452,9 @@ packages:
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
+ '@drizzle-team/brocli@0.10.2':
+ resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
+
'@emnapi/core@1.4.3':
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
@@ -416,6 +464,296 @@ packages:
'@emnapi/wasi-threads@1.0.2':
resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==}
+ '@esbuild-kit/core-utils@3.3.2':
+ resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
+ deprecated: 'Merged into tsx: https://tsx.is'
+
+ '@esbuild-kit/esm-loader@2.6.5':
+ resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
+ deprecated: 'Merged into tsx: https://tsx.is'
+
+ '@esbuild/aix-ppc64@0.25.4':
+ resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.18.20':
+ resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm64@0.25.4':
+ resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.18.20':
+ resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.4':
+ resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.18.20':
+ resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.4':
+ resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.18.20':
+ resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-arm64@0.25.4':
+ resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.18.20':
+ resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.4':
+ resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.18.20':
+ resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-arm64@0.25.4':
+ resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.18.20':
+ resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.4':
+ resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.18.20':
+ resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm64@0.25.4':
+ resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.18.20':
+ resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.4':
+ resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.18.20':
+ resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.4':
+ resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.18.20':
+ resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.4':
+ resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.18.20':
+ resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.4':
+ resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.18.20':
+ resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.4':
+ resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.18.20':
+ resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.4':
+ resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.18.20':
+ resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.4':
+ resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.18.20':
+ resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.4':
+ resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.4':
+ resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.18.20':
+ resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.4':
+ resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.4':
+ resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.18.20':
+ resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.4':
+ resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.18.20':
+ resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/sunos-x64@0.25.4':
+ resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.18.20':
+ resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-arm64@0.25.4':
+ resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.18.20':
+ resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.4':
+ resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.18.20':
+ resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.4':
+ resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@eslint-community/eslint-utils@4.6.1':
resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -469,11 +807,8 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
- '@giscus/react@3.1.0':
- resolution: {integrity: sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg==}
- peerDependencies:
- react: ^16 || ^17 || ^18 || ^19
- react-dom: ^16 || ^17 || ^18 || ^19
+ '@hexagon/base64@1.1.28':
+ resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
@@ -632,11 +967,8 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
- '@lit-labs/ssr-dom-shim@1.3.0':
- resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==}
-
- '@lit/reactive-element@2.1.0':
- resolution: {integrity: sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==}
+ '@levischuck/tiny-cbor@0.2.11':
+ resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
'@marsidev/react-turnstile@1.1.0':
resolution: {integrity: sha512-X7bP9ZYutDd+E+klPYF+/BJHqEyyVkN4KKmZcNRr84zs3DcMoftlMAuoKqNSnqg0HE7NQ1844+TLFSJoztCdSA==}
@@ -668,6 +1000,10 @@ packages:
'@napi-rs/wasm-runtime@0.2.9':
resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==}
+ '@neondatabase/serverless@1.0.0':
+ resolution: {integrity: sha512-XWmEeWpBXIoksZSDN74kftfTnXFEGZ3iX8jbANWBc+ag6dsiQuvuR4LgB0WdCOKMb5AQgjqgufc0TgAsZubUYw==}
+ engines: {node: '>=19.0.0'}
+
'@next/bundle-analyzer@15.4.0-canary.26':
resolution: {integrity: sha512-3HiFZxOX3v6nZFtwsq9I4mZHkOBJUneZXpD5ZTd3RO7qtPmRdRCSYUPriCdfidwDp6TIQouz1IU77Tx1fQBXUA==}
@@ -736,6 +1072,13 @@ packages:
cpu: [x64]
os: [win32]
+ '@noble/ciphers@0.6.0':
+ resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==}
+
+ '@noble/hashes@1.8.0':
+ resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+ engines: {node: ^14.21.3 || >=16}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -801,6 +1144,21 @@ packages:
'@octokit/types@14.0.0':
resolution: {integrity: sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==}
+ '@peculiar/asn1-android@2.3.16':
+ resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==}
+
+ '@peculiar/asn1-ecc@2.3.15':
+ resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==}
+
+ '@peculiar/asn1-rsa@2.3.15':
+ resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==}
+
+ '@peculiar/asn1-schema@2.3.15':
+ resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==}
+
+ '@peculiar/asn1-x509@2.3.15':
+ resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==}
+
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -815,6 +1173,19 @@ packages:
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+ '@radix-ui/react-alert-dialog@1.1.13':
+ resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-arrow@1.1.6':
resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==}
peerDependencies:
@@ -828,6 +1199,32 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-avatar@1.1.9':
+ resolution: {integrity: sha512-10tQokfvZdFvnvDkcOJPjm2pWiP8A0R4T83MoD7tb15bC/k2GU7B1YBuzJi8lNQ8V1QqhP8ocNqp27ByZaNagQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collection@1.1.6':
+ resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
@@ -846,6 +1243,28 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-dialog@1.1.13':
+ resolution: {integrity: sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-direction@1.1.1':
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-dismissable-layer@1.1.9':
resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==}
peerDependencies:
@@ -859,6 +1278,41 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-dropdown-menu@2.1.14':
+ resolution: {integrity: sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.2':
+ resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.6':
+ resolution: {integrity: sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
@@ -881,6 +1335,32 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-menu@2.1.14':
+ resolution: {integrity: sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popover@1.1.13':
+ resolution: {integrity: sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-popper@1.2.6':
resolution: {integrity: sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==}
peerDependencies:
@@ -933,6 +1413,32 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-roving-focus@1.1.9':
+ resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-separator@1.1.6':
+ resolution: {integrity: sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-slot@1.2.2':
resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
peerDependencies:
@@ -942,6 +1448,19 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-toast@1.2.13':
+ resolution: {integrity: sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-tooltip@1.2.6':
resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==}
peerDependencies:
@@ -991,6 +1510,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-use-is-hydrated@0.1.0':
+ resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
@@ -1071,6 +1599,13 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+ '@simplewebauthn/browser@13.1.0':
+ resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==}
+
+ '@simplewebauthn/server@13.1.1':
+ resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==}
+ engines: {node: '>=20.0.0'}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -1239,6 +1774,9 @@ packages:
'@types/node@22.15.17':
resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==}
+ '@types/pg@8.15.1':
+ resolution: {integrity: sha512-YKHrkGWBX5+ivzvOQ66I0fdqsQTsvxqM0AGP2i0XrVZ9DP5VA/deEbTf7VuLPGpY7fJB9uGbkZ6KjVhuHcrTkQ==}
+
'@types/react-dom@19.1.3':
resolution: {integrity: sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==}
peerDependencies:
@@ -1250,9 +1788,6 @@ packages:
'@types/supports-color@8.1.3':
resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==}
- '@types/trusted-types@2.0.7':
- resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
-
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -1389,9 +1924,6 @@ packages:
cpu: [x64]
os: [win32]
- '@upstash/redis@1.34.8':
- resolution: {integrity: sha512-eGJgOKc+2Uq4AdSM0lNx+WvFFhQeyhJ32SGNuSniLPg4lNb6m5h2AQ77qL+TgWiMZO8HCQ82Zsc/RlVBevCWTg==}
-
'@vercel/analytics@1.5.0':
resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
peerDependencies:
@@ -1418,10 +1950,6 @@ packages:
vue-router:
optional: true
- '@vercel/kv@3.0.0':
- resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==}
- engines: {node: '>=14.6'}
-
'@xobotyi/scrollbar-width@1.9.5':
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
@@ -1473,6 +2001,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-hidden@1.2.4:
+ resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
+ engines: {node: '>=10'}
+
aria-query@5.3.2:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
@@ -1515,6 +2047,10 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
+ asn1js@3.0.6:
+ resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==}
+ engines: {node: '>=12.0.0'}
+
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
@@ -1547,6 +2083,12 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ better-auth@1.2.7:
+ resolution: {integrity: sha512-2hCB263GSrgetsMUZw8vv9O1e4S4AlYJW3P4e8bX9u3Q3idv4u9BzDFCblpTLuL4YjYovghMCN0vurAsctXOAQ==}
+
+ better-call@1.0.9:
+ resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==}
+
body-parser@2.2.0:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'}
@@ -1723,9 +2265,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
- crypto-js@4.2.0:
- resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
-
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
@@ -1799,6 +2338,9 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ defu@6.1.4:
+ resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -1811,6 +2353,9 @@ packages:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -1835,6 +2380,103 @@ packages:
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+ dotenv@16.5.0:
+ resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
+ engines: {node: '>=12'}
+
+ drizzle-kit@0.31.1:
+ resolution: {integrity: sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q==}
+ hasBin: true
+
+ drizzle-orm@0.43.1:
+ resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==}
+ peerDependencies:
+ '@aws-sdk/client-rds-data': '>=3'
+ '@cloudflare/workers-types': '>=4'
+ '@electric-sql/pglite': '>=0.2.0'
+ '@libsql/client': '>=0.10.0'
+ '@libsql/client-wasm': '>=0.10.0'
+ '@neondatabase/serverless': '>=0.10.0'
+ '@op-engineering/op-sqlite': '>=2'
+ '@opentelemetry/api': ^1.4.1
+ '@planetscale/database': '>=1.13'
+ '@prisma/client': '*'
+ '@tidbcloud/serverless': '*'
+ '@types/better-sqlite3': '*'
+ '@types/pg': '*'
+ '@types/sql.js': '*'
+ '@vercel/postgres': '>=0.8.0'
+ '@xata.io/client': '*'
+ better-sqlite3: '>=7'
+ bun-types: '*'
+ expo-sqlite: '>=14.0.0'
+ gel: '>=2'
+ knex: '*'
+ kysely: '*'
+ mysql2: '>=2'
+ pg: '>=8'
+ postgres: '>=3'
+ prisma: '*'
+ sql.js: '>=1'
+ sqlite3: '>=5'
+ peerDependenciesMeta:
+ '@aws-sdk/client-rds-data':
+ optional: true
+ '@cloudflare/workers-types':
+ optional: true
+ '@electric-sql/pglite':
+ optional: true
+ '@libsql/client':
+ optional: true
+ '@libsql/client-wasm':
+ optional: true
+ '@neondatabase/serverless':
+ optional: true
+ '@op-engineering/op-sqlite':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@prisma/client':
+ optional: true
+ '@tidbcloud/serverless':
+ optional: true
+ '@types/better-sqlite3':
+ optional: true
+ '@types/pg':
+ optional: true
+ '@types/sql.js':
+ optional: true
+ '@vercel/postgres':
+ optional: true
+ '@xata.io/client':
+ optional: true
+ better-sqlite3:
+ optional: true
+ bun-types:
+ optional: true
+ expo-sqlite:
+ optional: true
+ gel:
+ optional: true
+ knex:
+ optional: true
+ kysely:
+ optional: true
+ mysql2:
+ optional: true
+ pg:
+ optional: true
+ postgres:
+ optional: true
+ prisma:
+ optional: true
+ sql.js:
+ optional: true
+ sqlite3:
+ optional: true
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1926,6 +2568,21 @@ packages:
esast-util-from-js@2.0.1:
resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+ esbuild-register@3.6.0:
+ resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
+ peerDependencies:
+ esbuild: '>=0.12 <1'
+
+ esbuild@0.18.20:
+ resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ esbuild@0.25.4:
+ resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
+ engines: {node: '>=18'}
+ hasBin: true
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -2009,6 +2666,11 @@ packages:
peerDependencies:
eslint: '>=2.0.0'
+ eslint-plugin-drizzle@0.2.3:
+ resolution: {integrity: sha512-BO+ymHo33IUNoJlC0rbd7HP9EwwpW4VIp49R/tWQF/d2E1K2kgTf0tCXT0v9MSiBr6gGR1LtPwMLapTKEWSg9A==}
+ peerDependencies:
+ eslint: '>=8.0.0'
+
eslint-plugin-import@2.31.0:
resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
engines: {node: '>=4'}
@@ -2281,6 +2943,10 @@ packages:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
@@ -2296,9 +2962,6 @@ packages:
get-tsconfig@4.10.0:
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
- giscus@1.6.0:
- resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==}
-
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
@@ -2391,6 +3054,9 @@ packages:
hast-util-interactive@3.0.0:
resolution: {integrity: sha512-9VFa3kP6AT40BNYcPmn3jpsG+1KPDF0rUFCrFVQDUsuUXZ3YLODm8UGV0tmYzFpcOIQXTAOi2ccS3ywlj2dQTA==}
+ hast-util-is-element@3.0.0:
+ resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+
hast-util-properties-to-mdx-jsx-attributes@1.0.1:
resolution: {integrity: sha512-ZzxhjHZ+gyxaPIFp/nuRpVL4GIFoqzfH6vNgjaA3CuUAV6XCxYwAQfRczrZRkgL6msi6DdOl+/QEduOdzszvbg==}
@@ -2432,6 +3098,9 @@ packages:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
+ html-url-attributes@3.0.1:
+ resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
+
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -2501,6 +3170,10 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
+ is-absolute-url@4.0.1:
+ resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
@@ -2669,6 +3342,9 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
+ jose@5.10.0:
+ resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
+
js-cookie@2.2.1:
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
@@ -2717,6 +3393,10 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
+ kysely@0.27.6:
+ resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
+ engines: {node: '>=14.0.0'}
+
language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@@ -2812,15 +3492,6 @@ packages:
resolution: {integrity: sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==}
engines: {node: '>=18.0.0'}
- lit-element@4.2.0:
- resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==}
-
- lit-html@3.3.0:
- resolution: {integrity: sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==}
-
- lit@3.3.0:
- resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==}
-
load-plugin@6.0.3:
resolution: {integrity: sha512-kc0X2FEUZr145odl68frm+lMJuQ23+rTXYmR6TImqPtbpmXC4vVXbWKDQ9IzndA0HfyQamWfKLhzsqGSTxE63w==}
@@ -3100,6 +3771,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanostores@0.11.4:
+ resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -3198,6 +3873,9 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ obuf@1.1.2:
+ resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
+
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -3301,6 +3979,21 @@ packages:
peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+ pg-int8@1.0.1:
+ resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
+ engines: {node: '>=4.0.0'}
+
+ pg-numeric@1.0.2:
+ resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
+ engines: {node: '>=4'}
+
+ pg-protocol@1.10.0:
+ resolution: {integrity: sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==}
+
+ pg-types@4.0.2:
+ resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
+ engines: {node: '>=10'}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3333,6 +4026,25 @@ packages:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
+ postgres-array@3.0.4:
+ resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==}
+ engines: {node: '>=12'}
+
+ postgres-bytea@3.0.0:
+ resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
+ engines: {node: '>= 6'}
+
+ postgres-date@2.1.0:
+ resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
+ engines: {node: '>=12'}
+
+ postgres-interval@3.0.0:
+ resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
+ engines: {node: '>=12'}
+
+ postgres-range@1.1.4:
+ resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -3431,6 +4143,13 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ pvtsutils@1.3.6:
+ resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
+
+ pvutils@1.1.3:
+ resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
+ engines: {node: '>=6.0.0'}
+
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
@@ -3470,9 +4189,35 @@ packages:
react: '>=18.2.0'
react-dom: '>=18.2.0'
+ react-markdown@10.1.0:
+ resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
+ peerDependencies:
+ '@types/react': '>=18'
+ react: '>=18'
+
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.6.3:
+ resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react-schemaorg@2.0.0:
resolution: {integrity: sha512-UqciFKA203ewNjn0zC09uYKuJSvMD8L75L1s/cW4rc7cS64w8l7vaI3SYkuoI/nwCBkJRmOkSJedWDUZBlYZwg==}
engines: {node: '>=12.0.0'}
@@ -3481,6 +4226,16 @@ packages:
schema-dts: '>=0.7.4'
typescript: '>=3.1.6'
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react-timeago@8.2.0:
resolution: {integrity: sha512-RWDlG3Jj+iwv+yNEDweA/Qk1mxE8i/Oc4oW8Irp29ZfBp+eNpqqYPMLPYQJyfRMJcGB8CmWkEGMYhB4fW8eZlQ==}
peerDependencies:
@@ -3551,6 +4306,9 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ rehype-external-links@3.0.0:
+ resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
+
rehype-mdx-code-props@3.0.1:
resolution: {integrity: sha512-BWWKn0N6r7/qd7lbLgv5J8of7imz1l1PyCNoY7BH0AOR9JdJlQIfA9cKqTZVEb2h2GPKh473qrBajF0i01fq3A==}
@@ -3653,6 +4411,9 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+ rou3@0.5.1:
+ resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
+
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
@@ -3721,6 +4482,9 @@ packages:
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -3790,10 +4554,19 @@ packages:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
+ sonner@2.0.3:
+ resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
source-map@0.5.6:
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
engines: {node: '>=0.10.0'}
@@ -4049,6 +4822,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
+ uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -4111,6 +4887,26 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
use-sync-external-store@1.5.0:
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
peerDependencies:
@@ -4264,6 +5060,10 @@ packages:
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+ignoredOptionalDependencies:
+ - '@prisma/client'
+ - prisma
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -4433,12 +5233,21 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
+ '@better-auth/utils@0.2.4':
+ dependencies:
+ typescript: 5.8.3
+ uncrypto: 0.1.3
+
+ '@better-fetch/fetch@1.1.18': {}
+
'@date-fns/tz@1.2.0': {}
'@date-fns/utc@2.1.0': {}
'@discoveryjs/json-ext@0.5.7': {}
+ '@drizzle-team/brocli@0.10.2': {}
+
'@emnapi/core@1.4.3':
dependencies:
'@emnapi/wasi-threads': 1.0.2
@@ -4455,6 +5264,157 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@esbuild-kit/core-utils@3.3.2':
+ dependencies:
+ esbuild: 0.18.20
+ source-map-support: 0.5.21
+
+ '@esbuild-kit/esm-loader@2.6.5':
+ dependencies:
+ '@esbuild-kit/core-utils': 3.3.2
+ get-tsconfig: 4.10.0
+
+ '@esbuild/aix-ppc64@0.25.4':
+ optional: true
+
+ '@esbuild/android-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/android-arm@0.18.20':
+ optional: true
+
+ '@esbuild/android-arm@0.25.4':
+ optional: true
+
+ '@esbuild/android-x64@0.18.20':
+ optional: true
+
+ '@esbuild/android-x64@0.25.4':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/darwin-x64@0.18.20':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.4':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.18.20':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-arm@0.18.20':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.4':
+ optional: true
+
+ '@esbuild/linux-ia32@0.18.20':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.4':
+ optional: true
+
+ '@esbuild/linux-loong64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.18.20':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.4':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-s390x@0.18.20':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.4':
+ optional: true
+
+ '@esbuild/linux-x64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.4':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.18.20':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.4':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.18.20':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.4':
+ optional: true
+
+ '@esbuild/sunos-x64@0.18.20':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.4':
+ optional: true
+
+ '@esbuild/win32-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/win32-ia32@0.18.20':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.4':
+ optional: true
+
+ '@esbuild/win32-x64@0.18.20':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.4':
+ optional: true
+
'@eslint-community/eslint-utils@4.6.1(eslint@9.26.0(jiti@2.4.2))':
dependencies:
eslint: 9.26.0(jiti@2.4.2)
@@ -4516,11 +5476,7 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
- '@giscus/react@3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
- dependencies:
- giscus: 1.6.0
- react: 19.1.0
- react-dom: 19.1.0(react@19.1.0)
+ '@hexagon/base64@1.1.28': {}
'@humanfs/core@0.19.1': {}
@@ -4643,11 +5599,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
- '@lit-labs/ssr-dom-shim@1.3.0': {}
-
- '@lit/reactive-element@2.1.0':
- dependencies:
- '@lit-labs/ssr-dom-shim': 1.3.0
+ '@levischuck/tiny-cbor@0.2.11': {}
'@marsidev/react-turnstile@1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
@@ -4720,6 +5672,11 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
+ '@neondatabase/serverless@1.0.0':
+ dependencies:
+ '@types/node': 22.15.17
+ '@types/pg': 8.15.1
+
'@next/bundle-analyzer@15.4.0-canary.26':
dependencies:
webpack-bundle-analyzer: 4.10.1
@@ -4764,6 +5721,10 @@ snapshots:
'@next/swc-win32-x64-msvc@15.4.0-canary.26':
optional: true
+ '@noble/ciphers@0.6.0': {}
+
+ '@noble/hashes@1.8.0': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -4864,6 +5825,39 @@ snapshots:
dependencies:
'@octokit/openapi-types': 25.0.0
+ '@peculiar/asn1-android@2.3.16':
+ dependencies:
+ '@peculiar/asn1-schema': 2.3.15
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-ecc@2.3.15':
+ dependencies:
+ '@peculiar/asn1-schema': 2.3.15
+ '@peculiar/asn1-x509': 2.3.15
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-rsa@2.3.15':
+ dependencies:
+ '@peculiar/asn1-schema': 2.3.15
+ '@peculiar/asn1-x509': 2.3.15
+ asn1js: 3.0.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-schema@2.3.15':
+ dependencies:
+ asn1js: 3.0.6
+ pvtsutils: 1.3.6
+ tslib: 2.8.1
+
+ '@peculiar/asn1-x509@2.3.15':
+ dependencies:
+ '@peculiar/asn1-schema': 2.3.15
+ asn1js: 3.0.6
+ pvtsutils: 1.3.6
+ tslib: 2.8.1
+
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -4873,6 +5867,20 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
+ '@radix-ui/react-alert-dialog@1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-dialog': 1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
'@radix-ui/react-arrow@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -4882,6 +5890,31 @@ snapshots:
'@types/react': 19.1.3
'@types/react-dom': 19.1.3(@types/react@19.1.3)
+ '@radix-ui/react-avatar@1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
+ '@radix-ui/react-collection@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.3)(react@19.1.0)':
dependencies:
react: 19.1.0
@@ -4894,6 +5927,34 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
+ '@radix-ui/react-dialog@1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ aria-hidden: 1.2.4
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
+ '@radix-ui/react-direction@1.1.1(@types/react@19.1.3)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.3
+
'@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -4907,6 +5968,38 @@ snapshots:
'@types/react': 19.1.3
'@types/react-dom': 19.1.3(@types/react@19.1.3)
+ '@radix-ui/react-dropdown-menu@2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-menu': 2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
+ '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.3)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.3
+
+ '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
'@radix-ui/react-id@1.1.1(@types/react@19.1.3)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
@@ -4923,6 +6016,55 @@ snapshots:
'@types/react': 19.1.3
'@types/react-dom': 19.1.3(@types/react@19.1.3)
+ '@radix-ui/react-menu@2.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ aria-hidden: 1.2.4
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
+ '@radix-ui/react-popover@1.1.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-popper': 1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ aria-hidden: 1.2.4
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
'@radix-ui/react-popper@1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -4970,6 +6112,32 @@ snapshots:
'@types/react': 19.1.3
'@types/react-dom': 19.1.3(@types/react@19.1.3)
+ '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
+ '@radix-ui/react-separator@1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
'@radix-ui/react-slot@1.2.2(@types/react@19.1.3)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
@@ -4977,6 +6145,26 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
+ '@radix-ui/react-toast@1.2.13(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-collection': 1.1.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
+ '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
+
'@radix-ui/react-tooltip@1.2.6(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -5025,6 +6213,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
+ '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.3)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ use-sync-external-store: 1.5.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.3)(react@19.1.0)':
dependencies:
react: 19.1.0
@@ -5106,6 +6301,18 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
+ '@simplewebauthn/browser@13.1.0': {}
+
+ '@simplewebauthn/server@13.1.1':
+ dependencies:
+ '@hexagon/base64': 1.1.28
+ '@levischuck/tiny-cbor': 0.2.11
+ '@peculiar/asn1-android': 2.3.16
+ '@peculiar/asn1-ecc': 2.3.15
+ '@peculiar/asn1-rsa': 2.3.15
+ '@peculiar/asn1-schema': 2.3.15
+ '@peculiar/asn1-x509': 2.3.15
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -5245,6 +6452,12 @@ snapshots:
dependencies:
undici-types: 6.21.0
+ '@types/pg@8.15.1':
+ dependencies:
+ '@types/node': 22.15.17
+ pg-protocol: 1.10.0
+ pg-types: 4.0.2
+
'@types/react-dom@19.1.3(@types/react@19.1.3)':
dependencies:
'@types/react': 19.1.3
@@ -5255,8 +6468,6 @@ snapshots:
'@types/supports-color@8.1.3': {}
- '@types/trusted-types@2.0.7': {}
-
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
@@ -5390,19 +6601,11 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.5.0':
optional: true
- '@upstash/redis@1.34.8':
- dependencies:
- crypto-js: 4.2.0
-
'@vercel/analytics@1.5.0(next@15.4.0-canary.26(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)':
optionalDependencies:
next: 15.4.0-canary.26(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
- '@vercel/kv@3.0.0':
- dependencies:
- '@upstash/redis': 1.34.8
-
'@xobotyi/scrollbar-width@1.9.5': {}
abbrev@2.0.0: {}
@@ -5445,6 +6648,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-hidden@1.2.4:
+ dependencies:
+ tslib: 2.8.1
+
aria-query@5.3.2: {}
arktype@2.1.20:
@@ -5519,6 +6726,12 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
+ asn1js@3.0.6:
+ dependencies:
+ pvtsutils: 1.3.6
+ pvutils: 1.1.3
+ tslib: 2.8.1
+
ast-types-flow@0.0.8: {}
astring@1.9.0: {}
@@ -5541,6 +6754,28 @@ snapshots:
balanced-match@1.0.2: {}
+ better-auth@1.2.7:
+ dependencies:
+ '@better-auth/utils': 0.2.4
+ '@better-fetch/fetch': 1.1.18
+ '@noble/ciphers': 0.6.0
+ '@noble/hashes': 1.8.0
+ '@simplewebauthn/browser': 13.1.0
+ '@simplewebauthn/server': 13.1.1
+ better-call: 1.0.9
+ defu: 6.1.4
+ jose: 5.10.0
+ kysely: 0.27.6
+ nanostores: 0.11.4
+ zod: 3.24.3
+
+ better-call@1.0.9:
+ dependencies:
+ '@better-fetch/fetch': 1.1.18
+ rou3: 0.5.1
+ set-cookie-parser: 2.7.1
+ uncrypto: 0.1.3
+
body-parser@2.2.0:
dependencies:
bytes: 3.1.2
@@ -5731,8 +6966,6 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
- crypto-js@4.2.0: {}
-
css-in-js-utils@3.1.0:
dependencies:
hyphenate-style-name: 1.1.0
@@ -5806,12 +7039,16 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ defu@6.1.4: {}
+
depd@2.0.0: {}
dequal@2.0.3: {}
detect-libc@2.0.3: {}
+ detect-node-es@1.1.0: {}
+
devlop@1.1.0:
dependencies:
dequal: 2.0.3
@@ -5840,6 +7077,23 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
+ dotenv@16.5.0: {}
+
+ drizzle-kit@0.31.1:
+ dependencies:
+ '@drizzle-team/brocli': 0.10.2
+ '@esbuild-kit/esm-loader': 2.6.5
+ esbuild: 0.25.4
+ esbuild-register: 3.6.0(esbuild@0.25.4)
+ transitivePeerDependencies:
+ - supports-color
+
+ drizzle-orm@0.43.1(@neondatabase/serverless@1.0.0)(@types/pg@8.15.1)(kysely@0.27.6):
+ optionalDependencies:
+ '@neondatabase/serverless': 1.0.0
+ '@types/pg': 8.15.1
+ kysely: 0.27.6
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -5998,6 +7252,66 @@ snapshots:
esast-util-from-estree: 2.0.0
vfile-message: 4.0.2
+ esbuild-register@3.6.0(esbuild@0.25.4):
+ dependencies:
+ debug: 4.4.0
+ esbuild: 0.25.4
+ transitivePeerDependencies:
+ - supports-color
+
+ esbuild@0.18.20:
+ optionalDependencies:
+ '@esbuild/android-arm': 0.18.20
+ '@esbuild/android-arm64': 0.18.20
+ '@esbuild/android-x64': 0.18.20
+ '@esbuild/darwin-arm64': 0.18.20
+ '@esbuild/darwin-x64': 0.18.20
+ '@esbuild/freebsd-arm64': 0.18.20
+ '@esbuild/freebsd-x64': 0.18.20
+ '@esbuild/linux-arm': 0.18.20
+ '@esbuild/linux-arm64': 0.18.20
+ '@esbuild/linux-ia32': 0.18.20
+ '@esbuild/linux-loong64': 0.18.20
+ '@esbuild/linux-mips64el': 0.18.20
+ '@esbuild/linux-ppc64': 0.18.20
+ '@esbuild/linux-riscv64': 0.18.20
+ '@esbuild/linux-s390x': 0.18.20
+ '@esbuild/linux-x64': 0.18.20
+ '@esbuild/netbsd-x64': 0.18.20
+ '@esbuild/openbsd-x64': 0.18.20
+ '@esbuild/sunos-x64': 0.18.20
+ '@esbuild/win32-arm64': 0.18.20
+ '@esbuild/win32-ia32': 0.18.20
+ '@esbuild/win32-x64': 0.18.20
+
+ esbuild@0.25.4:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.4
+ '@esbuild/android-arm': 0.25.4
+ '@esbuild/android-arm64': 0.25.4
+ '@esbuild/android-x64': 0.25.4
+ '@esbuild/darwin-arm64': 0.25.4
+ '@esbuild/darwin-x64': 0.25.4
+ '@esbuild/freebsd-arm64': 0.25.4
+ '@esbuild/freebsd-x64': 0.25.4
+ '@esbuild/linux-arm': 0.25.4
+ '@esbuild/linux-arm64': 0.25.4
+ '@esbuild/linux-ia32': 0.25.4
+ '@esbuild/linux-loong64': 0.25.4
+ '@esbuild/linux-mips64el': 0.25.4
+ '@esbuild/linux-ppc64': 0.25.4
+ '@esbuild/linux-riscv64': 0.25.4
+ '@esbuild/linux-s390x': 0.25.4
+ '@esbuild/linux-x64': 0.25.4
+ '@esbuild/netbsd-arm64': 0.25.4
+ '@esbuild/netbsd-x64': 0.25.4
+ '@esbuild/openbsd-arm64': 0.25.4
+ '@esbuild/openbsd-x64': 0.25.4
+ '@esbuild/sunos-x64': 0.25.4
+ '@esbuild/win32-arm64': 0.25.4
+ '@esbuild/win32-ia32': 0.25.4
+ '@esbuild/win32-x64': 0.25.4
+
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -6091,6 +7405,10 @@ snapshots:
gonzales-pe: 4.3.0
lodash: 4.17.21
+ eslint-plugin-drizzle@0.2.3(eslint@9.26.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.26.0(jiti@2.4.2)
+
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.26.0(jiti@2.4.2)):
dependencies:
'@rtsao/scc': 1.1.0
@@ -6501,6 +7819,8 @@ snapshots:
hasown: 2.0.2
math-intrinsics: 1.1.0
+ get-nonce@1.0.1: {}
+
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
@@ -6518,10 +7838,6 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
- giscus@1.6.0:
- dependencies:
- lit: 3.3.0
-
github-slugger@2.0.0: {}
glob-parent@5.1.2:
@@ -6606,6 +7922,10 @@ snapshots:
'@types/hast': 3.0.4
hast-util-has-property: 3.0.0
+ hast-util-is-element@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
hast-util-properties-to-mdx-jsx-attributes@1.0.1:
dependencies:
'@types/estree': 1.0.7
@@ -6710,6 +8030,8 @@ snapshots:
htmlparser2: 8.0.2
selderee: 0.11.0
+ html-url-attributes@3.0.1: {}
+
html-void-elements@3.0.0: {}
htmlparser2@8.0.2:
@@ -6775,6 +8097,8 @@ snapshots:
ipaddr.js@1.9.1: {}
+ is-absolute-url@4.0.1: {}
+
is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1:
@@ -6941,6 +8265,8 @@ snapshots:
jiti@2.4.2: {}
+ jose@5.10.0: {}
+
js-cookie@2.2.1: {}
js-tokens@4.0.0: {}
@@ -6978,6 +8304,8 @@ snapshots:
kleur@4.1.5: {}
+ kysely@0.27.6: {}
+
language-subtag-registry@0.3.23: {}
language-tags@1.0.9:
@@ -7064,22 +8392,6 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.0
- lit-element@4.2.0:
- dependencies:
- '@lit-labs/ssr-dom-shim': 1.3.0
- '@lit/reactive-element': 2.1.0
- lit-html: 3.3.0
-
- lit-html@3.3.0:
- dependencies:
- '@types/trusted-types': 2.0.7
-
- lit@3.3.0:
- dependencies:
- '@lit/reactive-element': 2.1.0
- lit-element: 4.2.0
- lit-html: 3.3.0
-
load-plugin@6.0.3:
dependencies:
'@npmcli/config': 8.3.4
@@ -7628,6 +8940,8 @@ snapshots:
nanoid@3.3.11: {}
+ nanostores@0.11.4: {}
+
natural-compare@1.4.0: {}
negotiator@1.0.0: {}
@@ -7742,6 +9056,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ obuf@1.1.2: {}
+
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
@@ -7863,6 +9179,22 @@ snapshots:
peberminta@0.9.0: {}
+ pg-int8@1.0.1: {}
+
+ pg-numeric@1.0.2: {}
+
+ pg-protocol@1.10.0: {}
+
+ pg-types@4.0.2:
+ dependencies:
+ pg-int8: 1.0.1
+ pg-numeric: 1.0.2
+ postgres-array: 3.0.4
+ postgres-bytea: 3.0.0
+ postgres-date: 2.1.0
+ postgres-interval: 3.0.0
+ postgres-range: 1.1.4
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -7887,6 +9219,18 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ postgres-array@3.0.4: {}
+
+ postgres-bytea@3.0.0:
+ dependencies:
+ obuf: 1.1.2
+
+ postgres-date@2.1.0: {}
+
+ postgres-interval@3.0.0: {}
+
+ postgres-range@1.1.4: {}
+
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@@ -7923,6 +9267,12 @@ snapshots:
punycode@2.3.1: {}
+ pvtsutils@1.3.6:
+ dependencies:
+ tslib: 2.8.1
+
+ pvutils@1.1.3: {}
+
qs@6.14.0:
dependencies:
side-channel: 1.1.0
@@ -7960,16 +9310,61 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
+ react-markdown@10.1.0(@types/react@19.1.3)(react@19.1.0):
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/react': 19.1.3
+ devlop: 1.1.0
+ hast-util-to-jsx-runtime: 2.3.6
+ html-url-attributes: 3.0.1
+ mdast-util-to-hast: 13.2.0
+ react: 19.1.0
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ unified: 11.0.5
+ unist-util-visit: 5.0.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
react-promise-suspense@0.3.4:
dependencies:
fast-deep-equal: 2.0.1
+ react-remove-scroll-bar@2.3.8(@types/react@19.1.3)(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-style-singleton: 2.2.3(@types/react@19.1.3)(react@19.1.0)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.3
+
+ react-remove-scroll@2.6.3(@types/react@19.1.3)(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-remove-scroll-bar: 2.3.8(@types/react@19.1.3)(react@19.1.0)
+ react-style-singleton: 2.2.3(@types/react@19.1.3)(react@19.1.0)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.1.3)(react@19.1.0)
+ use-sidecar: 1.1.3(@types/react@19.1.3)(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+
react-schemaorg@2.0.0(react@19.1.0)(schema-dts@1.1.5)(typescript@5.8.3):
dependencies:
react: 19.1.0
schema-dts: 1.1.5
typescript: 5.8.3
+ react-style-singleton@2.2.3(@types/react@19.1.3)(react@19.1.0):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.1.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.3
+
react-timeago@8.2.0(react@19.1.0):
dependencies:
react: 19.1.0
@@ -8084,6 +9479,15 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ rehype-external-links@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@ungap/structured-clone': 1.3.0
+ hast-util-is-element: 3.0.0
+ is-absolute-url: 4.0.1
+ space-separated-tokens: 2.0.2
+ unist-util-visit: 5.0.0
+
rehype-mdx-code-props@3.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -8271,6 +9675,8 @@ snapshots:
rfdc@1.4.1: {}
+ rou3@0.5.1: {}
+
router@2.2.0:
dependencies:
debug: 4.4.0
@@ -8359,6 +9765,8 @@ snapshots:
server-only@0.0.1: {}
+ set-cookie-parser@2.7.1: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -8481,8 +9889,18 @@ snapshots:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
+ sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
source-map-js@1.2.1: {}
+ source-map-support@0.5.21:
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+
source-map@0.5.6: {}
source-map@0.6.1: {}
@@ -8761,6 +10179,8 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
+ uncrypto@0.1.3: {}
+
undici-types@6.21.0: {}
undici@6.21.2: {}
@@ -8885,6 +10305,21 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-callback-ref@1.3.3(@types/react@19.1.3)(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.3
+
+ use-sidecar@1.1.3(@types/react@19.1.3)(react@19.1.0):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.1.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.3
+
use-sync-external-store@1.5.0(react@19.1.0):
dependencies:
react: 19.1.0
diff --git a/public/humans.txt b/public/humans.txt
index cf81664b..759e747a 100644
--- a/public/humans.txt
+++ b/public/humans.txt
@@ -31,8 +31,9 @@
- Next.js
- Vercel
- Tailwind CSS
- - Upstash Redis
- - Giscus
+ - Neon Postgres
+ - Drizzle ORM
+ - Better Auth
- Resend
- ...and more: https://jarv.is/uses
- View more on{" "} - {" "} - GitHub. + View more on GitHub.
> diff --git a/app/uses/page.mdx b/app/uses/page.mdx index b43fe843..e662a1b9 100644 --- a/app/uses/page.mdx +++ b/app/uses/page.mdx @@ -1,5 +1,4 @@ import PageTitle from "@/components/layout/page-title"; -import Comments from "@/components/comments"; import { createMetadata } from "@/lib/metadata"; export const metadata = createMetadata({ @@ -165,7 +164,3 @@ Other geeky stuff: - 2x [**ecobee3 lite**](https://www.ecobee.com/en-us/smart-thermostats/smart-wifi-thermostat/) - 2x [**Sonos One**](https://www.sonos.com/en-us/shop/one.html) (with Alexa turned off...hopefully? 🤫) - 2x [**Apple TV 4K** (2021)](https://www.apple.com/apple-tv-4k/) - ---- - -Comments are closed for this post.
+Join the discussion by signing in:
+