mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-06-05 18:15:33 -04:00
fix: batch server requests from posts list
This commit is contained in:
@@ -35,3 +35,6 @@ yarn.lock
|
|||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
|
# next-agents-md
|
||||||
|
.next-docs/
|
||||||
|
|||||||
+1
-1
@@ -10,4 +10,4 @@ const Analytics = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Analytics;
|
export { Analytics };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import { JsonLd } from "react-schemaorg";
|
import { JsonLd } from "react-schemaorg";
|
||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import Video from "@/components/video";
|
import { Video } from "@/components/video";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
import type { VideoObject } from "schema-dts";
|
import type { VideoObject } from "schema-dts";
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
export const metadata = createMetadata({
|
export const metadata = createMetadata({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import ContactForm from "@/components/contact-form";
|
import { ContactForm } from "@/components/contact-form";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
export const metadata = createMetadata({
|
export const metadata = createMetadata({
|
||||||
|
|||||||
+2
-1
@@ -193,9 +193,10 @@
|
|||||||
::selection {
|
::selection {
|
||||||
@apply bg-selection text-selection-foreground;
|
@apply bg-selection text-selection-foreground;
|
||||||
}
|
}
|
||||||
|
/* https://ui.shadcn.com/docs/components/button#cursor */
|
||||||
button:not(:disabled),
|
button:not(:disabled),
|
||||||
[role="button"]:not(:disabled) {
|
[role="button"]:not(:disabled) {
|
||||||
@apply cursor-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import { JsonLd } from "react-schemaorg";
|
import { JsonLd } from "react-schemaorg";
|
||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import Video from "@/components/video";
|
import { Video } from "@/components/video";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
import type { VideoObject } from "schema-dts";
|
import type { VideoObject } from "schema-dts";
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
import { ViewTransition } from "react";
|
import { ViewTransition } from "react";
|
||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import { JsonLd } from "react-schemaorg";
|
import { JsonLd } from "react-schemaorg";
|
||||||
import Providers from "@/components/providers";
|
import { Providers } from "@/components/providers";
|
||||||
import Header from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import Footer from "@/components/layout/footer";
|
import { Footer } from "@/components/layout/footer";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import Analytics from "@/app/analytics";
|
import { Analytics } from "@/app/analytics";
|
||||||
import { defaultMetadata } from "@/lib/metadata";
|
import { defaultMetadata } from "@/lib/metadata";
|
||||||
import { GeistSans, GeistMono } from "@/lib/fonts";
|
import { GeistSans, GeistMono } from "@/lib/fonts";
|
||||||
import siteConfig from "@/lib/config/site";
|
import siteConfig from "@/lib/config/site";
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import { JsonLd } from "react-schemaorg";
|
import { JsonLd } from "react-schemaorg";
|
||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import Video from "@/components/video";
|
import { Video } from "@/components/video";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
import type { VideoObject } from "schema-dts";
|
import type { VideoObject } from "schema-dts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
export const metadata = createMetadata({
|
export const metadata = createMetadata({
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Video from "@/components/video";
|
import { Video } from "@/components/video";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Suspense } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { JsonLd } from "react-schemaorg";
|
import { JsonLd } from "react-schemaorg";
|
||||||
import { CalendarDaysIcon, TagIcon, SquarePenIcon, EyeIcon, MessagesSquareIcon } from "lucide-react";
|
import { CalendarDaysIcon, TagIcon, SquarePenIcon, EyeIcon, MessagesSquareIcon } from "lucide-react";
|
||||||
import ViewCounter from "@/components/view-counter";
|
import { ViewCounter } from "@/components/view-counter";
|
||||||
import CommentCount from "@/components/comment-count";
|
import { CommentCount } from "@/components/comment-count";
|
||||||
import Comments from "@/components/comments/comments";
|
import { Comments } from "@/components/comments/comments";
|
||||||
import CommentsSkeleton from "@/components/comments/comments-skeleton";
|
import { CommentsSkeleton } from "@/components/comments/comments-skeleton";
|
||||||
import { getSlugs, getFrontMatter, POSTS_DIR } from "@/lib/posts";
|
import { getSlugs, getFrontMatter, POSTS_DIR } from "@/lib/posts";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
import siteConfig from "@/lib/config/site";
|
import siteConfig from "@/lib/config/site";
|
||||||
|
|||||||
+7
-5
@@ -1,6 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import PostStats from "@/components/post-stats";
|
import { PostStats, PostStatsProvider } from "@/components/post-stats";
|
||||||
import { getFrontMatter, POSTS_DIR, type FrontMatter } from "@/lib/posts";
|
import { getFrontMatter, POSTS_DIR, type FrontMatter } from "@/lib/posts";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
import authorConfig from "@/lib/config/author";
|
import authorConfig from "@/lib/config/author";
|
||||||
@@ -61,12 +61,12 @@ const PostsList = async () => {
|
|||||||
{dateDisplay}
|
{dateDisplay}
|
||||||
</time>
|
</time>
|
||||||
</span>
|
</span>
|
||||||
<div className="space-x-2.5">
|
<div className="space-x-2">
|
||||||
{/* htmlTitle is sanitized by rehypeSanitize in lib/posts.ts with strict allowlist: only code, em, strong tags */}
|
{/* htmlTitle is sanitized by rehypeSanitize in lib/posts.ts with strict allowlist: only code, em, strong tags */}
|
||||||
<Link
|
<Link
|
||||||
href={`/${POSTS_DIR}/${slug}`}
|
href={`/${POSTS_DIR}/${slug}`}
|
||||||
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
|
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
|
||||||
className="underline-offset-4 hover:underline"
|
className="mr-2.5 underline-offset-4 hover:underline"
|
||||||
style={{ viewTransitionName: `note-title-${slug}` }}
|
style={{ viewTransitionName: `note-title-${slug}` }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -87,7 +87,9 @@ const Page = async () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle canonical="/notes">Notes</PageTitle>
|
<PageTitle canonical="/notes">Notes</PageTitle>
|
||||||
<PostsList />
|
<PostStatsProvider>
|
||||||
|
<PostsList />
|
||||||
|
</PostStatsProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import Marquee from "@/components/marquee";
|
import { Marquee } from "@/components/marquee";
|
||||||
import { Win95Icon } from "@/components/icons";
|
import { Win95Icon } from "@/components/icons";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
import { PageStyles } from "./page-styles";
|
import { PageStyles } from "./page-styles";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
export const metadata = createMetadata({
|
export const metadata = createMetadata({
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Suspense } from "react";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { GitForkIcon, StarIcon } from "lucide-react";
|
import { GitForkIcon, StarIcon } from "lucide-react";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import RelativeTime from "@/components/relative-time";
|
import { RelativeTime } from "@/components/relative-time";
|
||||||
import ActivityCalendar from "@/components/activity-calendar";
|
import { ActivityCalendar } from "@/components/activity-calendar";
|
||||||
import { GitHubIcon } from "@/components/icons";
|
import { GitHubIcon } from "@/components/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
export const metadata = createMetadata({
|
export const metadata = createMetadata({
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import PageTitle from "@/components/layout/page-title";
|
import { PageTitle } from "@/components/layout/page-title";
|
||||||
import { createMetadata } from "@/lib/metadata";
|
import { createMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
import backgroundImg from "./sundar.jpg";
|
import backgroundImg from "./sundar.jpg";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ActivityCalendar, type Activity } from "react-activity-calendar";
|
import { ActivityCalendar as ActivityCalendarPrimitive, type Activity } from "react-activity-calendar";
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Calendar = ({
|
const ActivityCalendar = ({
|
||||||
data,
|
data,
|
||||||
noun = "thing",
|
noun = "thing",
|
||||||
className,
|
className,
|
||||||
@@ -24,7 +24,7 @@ const Calendar = ({
|
|||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<ActivityCalendar
|
<ActivityCalendarPrimitive
|
||||||
data={data}
|
data={data}
|
||||||
colorScheme="dark"
|
colorScheme="dark"
|
||||||
theme={{
|
theme={{
|
||||||
@@ -55,4 +55,4 @@ const Calendar = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Calendar;
|
export { ActivityCalendar };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { codeToHtml } from "shiki";
|
import { codeToHtml } from "shiki";
|
||||||
import { cacheLife } from "next/cache";
|
import { cacheLife } from "next/cache";
|
||||||
import CopyButton from "@/components/copy-button";
|
import { CopyButton } from "@/components/copy-button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,4 +72,4 @@ const CodeBlock = async ({ children, className, showLineNumbers = true, ...props
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CodeBlock;
|
export { CodeBlock };
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ const CommentCount = ({ slug }: { slug: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentCount;
|
export { CommentCount };
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import Form from "./comment-form";
|
import { CommentForm } from "./comment-form";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
import { deleteComment, type CommentWithUser } from "@/lib/server/comments";
|
import { deleteComment, type CommentWithUser } from "@/lib/server/comments";
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form
|
<CommentForm
|
||||||
slug={comment.pageSlug}
|
slug={comment.pageSlug}
|
||||||
initialContent={comment.content}
|
initialContent={comment.content}
|
||||||
commentId={comment.id}
|
commentId={comment.id}
|
||||||
@@ -81,7 +81,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
|
|
||||||
{isReplying && (
|
{isReplying && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Form
|
<CommentForm
|
||||||
slug={comment.pageSlug}
|
slug={comment.pageSlug}
|
||||||
parentId={comment.id}
|
parentId={comment.id}
|
||||||
onCancel={() => setIsReplying(false)}
|
onCancel={() => setIsReplying(false)}
|
||||||
@@ -93,4 +93,4 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentActions;
|
export { CommentActions };
|
||||||
|
|||||||
@@ -193,4 +193,4 @@ const CommentForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentForm;
|
export { CommentForm };
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { getImageProps } from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import RelativeTime from "@/components/relative-time";
|
import { RelativeTime } from "@/components/relative-time";
|
||||||
import Actions from "./comment-actions";
|
import { CommentActions } from "./comment-actions";
|
||||||
import { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
import { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
||||||
import { rehypeExternalLinks } from "@/lib/rehype";
|
import { rehypeExternalLinks } from "@/lib/rehype";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -66,11 +66,11 @@ const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Actions comment={comment} />
|
<CommentActions comment={comment} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentSingle;
|
export { CommentSingle };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Single from "./comment-single";
|
import { CommentSingle } from "./comment-single";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { CommentWithUser } from "@/lib/server/comments";
|
import type { CommentWithUser } from "@/lib/server/comments";
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ const CommentThread = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Single comment={comment} />
|
<CommentSingle comment={comment} />
|
||||||
|
|
||||||
{replies.length > 0 && (
|
{replies.length > 0 && (
|
||||||
<div className={cn("mt-6 space-y-6", level < maxLevel && "ml-6 border-l-2 pl-6")}>
|
<div className={cn("mt-6 space-y-6", level < maxLevel && "ml-6 border-l-2 pl-6")}>
|
||||||
@@ -37,4 +37,4 @@ const CommentThread = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentThread;
|
export { CommentThread };
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ const CommentsSkeleton = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentsSkeleton;
|
export { CommentsSkeleton };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Form from "./comment-form";
|
import { CommentForm } from "./comment-form";
|
||||||
import Thread from "./comment-thread";
|
import { CommentThread } from "./comment-thread";
|
||||||
import SignIn from "./sign-in";
|
import { SignIn } from "./sign-in";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { getComments, type CommentWithUser } from "@/lib/server/comments";
|
import { getComments, type CommentWithUser } from "@/lib/server/comments";
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const Comments = async ({ slug }: { slug: string }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{session ? (
|
{session ? (
|
||||||
<Form slug={slug} />
|
<CommentForm slug={slug} />
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-muted/40 flex flex-col items-center justify-center gap-y-4 rounded-lg p-6">
|
<div className="bg-muted/40 flex flex-col items-center justify-center gap-y-4 rounded-lg p-6">
|
||||||
<p className="text-center font-medium">Join the discussion by signing in:</p>
|
<p className="text-center font-medium">Join the discussion by signing in:</p>
|
||||||
@@ -40,7 +40,7 @@ const Comments = async ({ slug }: { slug: string }) => {
|
|||||||
{rootComments.length > 0 ? (
|
{rootComments.length > 0 ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{rootComments.map((comment: CommentWithUser) => (
|
{rootComments.map((comment: CommentWithUser) => (
|
||||||
<Thread
|
<CommentThread
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
replies={commentsByParentId[comment.id] || []}
|
replies={commentsByParentId[comment.id] || []}
|
||||||
@@ -57,4 +57,4 @@ const Comments = async ({ slug }: { slug: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Comments;
|
export { Comments };
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SignIn;
|
export { SignIn };
|
||||||
|
|||||||
@@ -163,4 +163,4 @@ const ContactForm = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactForm;
|
export { ContactForm };
|
||||||
|
|||||||
@@ -67,4 +67,4 @@ function CopyButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CopyButton;
|
export { CopyButton };
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
// marking the library as a proper client component so that react doesn't complain about hydration whenever we use it in
|
// marking the library as a proper client component so that react doesn't complain about hydration whenever we use it in
|
||||||
// a server component.
|
// a server component.
|
||||||
// see: https://react.dev/reference/rsc/use-client#using-third-party-libraries
|
// see: https://react.dev/reference/rsc/use-client#using-third-party-libraries
|
||||||
export { default } from "react-countup";
|
export { default as CountUp } from "react-countup";
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ const HeadingAnchor = ({ id, title, className }: { id: string; title: string; cl
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HeadingAnchor;
|
export { HeadingAnchor };
|
||||||
|
|||||||
@@ -127,4 +127,4 @@ const ImageDiff = ({ children, className }: { children: React.ReactElement[]; cl
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImageDiff;
|
export { ImageDiff };
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ const Footer = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export { Footer };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import Menu from "@/components/layout/menu";
|
import { Menu } from "@/components/layout/menu";
|
||||||
import { GitHubIcon } from "@/components/icons";
|
import { GitHubIcon } from "@/components/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import authorConfig from "@/lib/config/author";
|
import authorConfig from "@/lib/config/author";
|
||||||
@@ -90,4 +90,4 @@ const Header = ({ className }: { className?: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export { Header };
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ const Menu = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Menu;
|
export { Menu };
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ const PageTitle = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PageTitle;
|
export { PageTitle };
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ const Marquee = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Marquee;
|
export { Marquee };
|
||||||
|
|||||||
+60
-29
@@ -1,55 +1,86 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { EyeIcon, MessagesSquareIcon } from "lucide-react";
|
import { EyeIcon, MessagesSquareIcon } from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import { getViewCount } from "@/lib/server/views";
|
import { getAllViewCounts } from "@/lib/server/views";
|
||||||
import { getCommentCount } from "@/lib/server/comments";
|
import { getAllCommentCounts } from "@/lib/server/comments";
|
||||||
|
|
||||||
const numberFormatter = new Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE);
|
const numberFormatter = new Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE);
|
||||||
|
|
||||||
const PostStats = ({ slug }: { slug: string }) => {
|
type Stats = {
|
||||||
const [stats, setStats] = useState<{ views: number; comments: number } | null>(null);
|
views: Record<string, number>;
|
||||||
|
comments: Record<string, number>;
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatsContext = createContext<Stats>({ views: {}, comments: {}, loaded: false });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider that fetches ALL post stats in a single batch (2 requests total).
|
||||||
|
* Wrap this around any component tree that contains PostStats components.
|
||||||
|
*/
|
||||||
|
export const PostStatsProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [stats, setStats] = useState<Stats>({ views: {}, comments: {}, loaded: false });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([getViewCount(slug), getCommentCount(slug)])
|
Promise.all([getAllViewCounts(), getAllCommentCounts()])
|
||||||
.then(([views, comments]) => {
|
.then(([views, comments]) => {
|
||||||
setStats({ views, comments });
|
setStats({ views, comments, loaded: true });
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("[post-stats] error:", err);
|
console.error("[post-stats] error fetching stats:", err);
|
||||||
// Silently fail - just don't show stats
|
setStats({ views: {}, comments: {}, loaded: true });
|
||||||
});
|
});
|
||||||
}, [slug]);
|
}, []);
|
||||||
|
|
||||||
if (!stats) {
|
return <StatsContext.Provider value={stats}>{children}</StatsContext.Provider>;
|
||||||
return null; // No loading state - badges just appear when ready
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays view/comment badges for a single post.
|
||||||
|
* Must be used within a PostStatsProvider.
|
||||||
|
*/
|
||||||
|
const PostStats = ({ slug }: { slug: string }) => {
|
||||||
|
const { views, comments, loaded } = useContext(StatsContext);
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton className="inline-block h-5 w-12 rounded-full align-text-top" />
|
||||||
|
<Skeleton className="inline-block h-5 w-8 rounded-full align-text-top" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewCount = views[slug] ?? 0;
|
||||||
|
const commentCount = comments[slug] ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{stats.views > 0 && (
|
{viewCount > 0 && (
|
||||||
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none">
|
<Badge variant="secondary" className="tabular-nums">
|
||||||
<EyeIcon className="inline-block size-4 shrink-0" aria-hidden="true" />
|
<EyeIcon className="text-foreground/85" aria-hidden="true" />
|
||||||
<span className="inline-block leading-none tabular-nums">{numberFormatter.format(stats.views)}</span>
|
{numberFormatter.format(viewCount)}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{stats.comments > 0 && (
|
{commentCount > 0 && (
|
||||||
<Link
|
<Badge variant="secondary" className="tabular-nums" asChild>
|
||||||
href={`/${slug}#comments`}
|
<Link
|
||||||
title={`${numberFormatter.format(stats.comments)} ${stats.comments === 1 ? "comment" : "comments"}`}
|
href={`/${slug}#comments`}
|
||||||
className="inline-flex hover:no-underline"
|
title={`${numberFormatter.format(commentCount)} ${commentCount === 1 ? "comment" : "comments"}`}
|
||||||
>
|
>
|
||||||
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none">
|
<MessagesSquareIcon className="text-foreground/85" aria-hidden="true" />
|
||||||
<MessagesSquareIcon className="inline-block size-3 shrink-0" aria-hidden="true" />
|
{numberFormatter.format(commentCount)}
|
||||||
<span className="inline-block leading-none tabular-nums">{numberFormatter.format(stats.comments)}</span>
|
</Link>
|
||||||
</span>
|
</Badge>
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PostStats;
|
export { PostStats };
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Providers;
|
export { Providers };
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ const RelativeTime = ({ ...rest }: React.ComponentProps<typeof TimeAgo>) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RelativeTime;
|
export { RelativeTime };
|
||||||
|
|||||||
Vendored
+1
-1
@@ -28,4 +28,4 @@ const CodePen = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CodePen;
|
export { CodePen };
|
||||||
|
|||||||
Vendored
+1
-1
@@ -52,4 +52,4 @@ const Gist = async ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Gist;
|
export { Gist };
|
||||||
|
|||||||
Vendored
+1
-1
@@ -57,4 +57,4 @@ const Tweet = async ({ id, className }: { id: string; className?: string }) => {
|
|||||||
return <TweetContent data={data} className={className} />;
|
return <TweetContent data={data} className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tweet;
|
export { Tweet };
|
||||||
|
|||||||
Vendored
+1
-1
@@ -8,4 +8,4 @@ const YouTube = ({ ...rest }: Omit<React.ComponentProps<typeof YouTubeEmbed>, "t
|
|||||||
return <YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />;
|
return <YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default YouTube;
|
export { YouTube };
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ const Video = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Video;
|
export { Video };
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import CountUp from "@/components/count-up";
|
import { CountUp } from "@/components/count-up";
|
||||||
import { incrementViews } from "@/lib/server/views";
|
import { incrementViews } from "@/lib/server/views";
|
||||||
|
|
||||||
const ViewCounter = ({ slug }: { slug: string }) => {
|
const ViewCounter = ({ slug }: { slug: string }) => {
|
||||||
@@ -36,4 +36,4 @@ const ViewCounter = ({ slug }: { slug: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewCounter;
|
export { ViewCounter };
|
||||||
|
|||||||
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import CodeBlock from "@/components/code-block";
|
import { CodeBlock } from "@/components/code-block";
|
||||||
import Video from "@/components/video";
|
import { Video } from "@/components/video";
|
||||||
import ImageDiff from "@/components/image-diff";
|
import { ImageDiff } from "@/components/image-diff";
|
||||||
import Tweet from "@/components/third-party/tweet";
|
import { Tweet } from "@/components/third-party/tweet";
|
||||||
import YouTube from "@/components/third-party/youtube";
|
import { YouTube } from "@/components/third-party/youtube";
|
||||||
import Gist from "@/components/third-party/gist";
|
import { Gist } from "@/components/third-party/gist";
|
||||||
import CodePen from "@/components/third-party/codepen";
|
import { CodePen } from "@/components/third-party/codepen";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { MDXComponents } from "mdx/types";
|
import type { MDXComponents } from "mdx/types";
|
||||||
|
|
||||||
|
|||||||
+4
-26
@@ -6,7 +6,6 @@ import "./lib/env";
|
|||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
cacheComponents: true,
|
cacheComponents: true,
|
||||||
reactStrictMode: true,
|
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
|
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
|
||||||
images: {
|
images: {
|
||||||
@@ -45,30 +44,6 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: async () => [
|
headers: async () => [
|
||||||
{
|
|
||||||
// matches any path
|
|
||||||
source: "/(.*)",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: "strict-transport-security",
|
|
||||||
value: "max-age=63072000",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 🥛 debugging
|
|
||||||
key: "x-got-milk",
|
|
||||||
value: "2%",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: "/api/auth/(.*)",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: "cache-control",
|
|
||||||
value: "private, max-age=0",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// https://community.torproject.org/onion-services/advanced/onion-location/
|
// https://community.torproject.org/onion-services/advanced/onion-location/
|
||||||
...(process.env.NEXT_PUBLIC_ONION_DOMAIN
|
...(process.env.NEXT_PUBLIC_ONION_DOMAIN
|
||||||
? [
|
? [
|
||||||
@@ -92,9 +67,12 @@ const nextConfig = {
|
|||||||
source: "/tweets/:path*",
|
source: "/tweets/:path*",
|
||||||
destination: "https://tweets-khaki.vercel.app/:path*",
|
destination: "https://tweets-khaki.vercel.app/:path*",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/y2k/:path*",
|
||||||
|
destination: "https://y2k.pages.dev/:path*",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
redirects: async () => [
|
redirects: async () => [
|
||||||
{ source: "/y2k", destination: "https://y2k.pages.dev", permanent: false },
|
|
||||||
{
|
{
|
||||||
source: "/pubkey.asc",
|
source: "/pubkey.asc",
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
Reference in New Issue
Block a user