1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 17:55:32 -04:00

refactor: replace react-countup with @number-flow/react

This commit is contained in:
2026-04-30 10:36:33 -04:00
parent b2416ff0db
commit 62d632f909
26 changed files with 419 additions and 450 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { ArrowUpRight } from "lucide-react"; import { IconArrowUpRight } from "@tabler/icons-react";
import Image, { type StaticImageData } from "next/image"; import Image, { type StaticImageData } from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -97,7 +97,7 @@ const Page = () => (
</div> </div>
<span className="text-muted-foreground ml-9 text-xs text-pretty sm:ml-auto"> <span className="text-muted-foreground ml-9 text-xs text-pretty sm:ml-auto">
{project.tagline} {project.tagline}
<ArrowUpRight <IconArrowUpRight
className="group-hover:text-primary ml-1 inline size-3.5 shrink-0 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" className="group-hover:text-primary ml-1 inline size-3.5 shrink-0 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
aria-hidden="true" aria-hidden="true"
/> />
+10 -16
View File
@@ -1,10 +1,4 @@
import { import { IconCalendarEvent, IconEdit, IconEye, IconMessages, IconTag } from "@tabler/icons-react";
CalendarDaysIcon,
EyeIcon,
MessagesSquareIcon,
SquarePenIcon,
TagIcon,
} from "lucide-react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
@@ -110,14 +104,14 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
}} }}
/> />
<div className="text-foreground/70 flex flex-wrap justify-items-start space-y-2.5 space-x-4 text-[13px] tracking-wide"> <div className="text-foreground/70 flex flex-wrap items-center gap-x-4 gap-y-2.5 text-[13px] tracking-wide">
<Link <Link
href={`/${POSTS_DIR}/${frontmatter?.slug}`} href={`/${POSTS_DIR}/${frontmatter?.slug}`}
className={ className={
"text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline" "flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-inherit hover:no-underline"
} }
> >
<CalendarDaysIcon className="inline size-3 shrink-0" aria-hidden="true" /> <IconCalendarEvent className="inline size-3.5 shrink-0" aria-hidden="true" />
<time <time
dateTime={formattedDates.dateISO} dateTime={formattedDates.dateISO}
title={formattedDates.dateTitle} title={formattedDates.dateTitle}
@@ -129,7 +123,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
{frontmatter?.tags && ( {frontmatter?.tags && (
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
<TagIcon className="inline size-3 shrink-0" aria-hidden="true" /> <IconTag className="inline size-3.5 shrink-0" aria-hidden="true" />
{frontmatter?.tags.map((tag) => ( {frontmatter?.tags.map((tag) => (
<span <span
key={tag} key={tag}
@@ -146,23 +140,23 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter?.slug}/index.mdx`} href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter?.slug}/index.mdx`}
title={`Edit "${frontmatter?.title}" on GitHub`} title={`Edit "${frontmatter?.title}" on GitHub`}
className={ className={
"text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline" "flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-inherit hover:no-underline"
} }
> >
<SquarePenIcon className="inline size-3 shrink-0" aria-hidden="true" /> <IconEdit className="inline size-3.5 shrink-0" aria-hidden="true" />
<span>Improve This Post</span> <span>Improve This Post</span>
</Link> </Link>
<Link <Link
href={`/${POSTS_DIR}/${frontmatter?.slug}#comments`} href={`/${POSTS_DIR}/${frontmatter?.slug}#comments`}
className="text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline" className="flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-inherit hover:no-underline"
> >
<MessagesSquareIcon className="inline size-3 shrink-0" aria-hidden="true" /> <IconMessages className="inline size-3.5 shrink-0" aria-hidden="true" />
<CommentCount slug={`${POSTS_DIR}/${frontmatter?.slug}`} /> <CommentCount slug={`${POSTS_DIR}/${frontmatter?.slug}`} />
</Link> </Link>
<div className="flex min-w-14 flex-nowrap items-center gap-1.5 whitespace-nowrap"> <div className="flex min-w-14 flex-nowrap items-center gap-1.5 whitespace-nowrap">
<EyeIcon className="inline size-3 shrink-0" aria-hidden="true" /> <IconEye className="inline size-3.5 shrink-0" aria-hidden="true" />
<ViewCounter slug={`${POSTS_DIR}/${frontmatter?.slug}`} /> <ViewCounter slug={`${POSTS_DIR}/${frontmatter?.slug}`} />
</div> </div>
</div> </div>
+4 -4
View File
@@ -1,4 +1,4 @@
import { ExternalLinkIcon, GitForkIcon, StarIcon } from "lucide-react"; import { IconExternalLink, IconGitFork, IconStar } from "@tabler/icons-react";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { Suspense } from "react"; import { Suspense } from "react";
@@ -113,7 +113,7 @@ const Page = async () => {
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-1.5 hover:no-underline" className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-1.5 hover:no-underline"
> >
<StarIcon className="inline-block size-3.5 shrink-0" aria-hidden="true" /> <IconStar className="inline-block size-3.5 shrink-0" aria-hidden="true" />
<span> <span>
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format( {Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(
repo?.stargazerCount, repo?.stargazerCount,
@@ -130,7 +130,7 @@ const Page = async () => {
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(repo?.forkCount)} ${repo?.forkCount === 1 ? "fork" : "forks"}`} title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(repo?.forkCount)} ${repo?.forkCount === 1 ? "fork" : "forks"}`}
className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-1.5 hover:no-underline" className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-1.5 hover:no-underline"
> >
<GitForkIcon className="inline-block size-3.5 shrink-0" aria-hidden="true" /> <IconGitFork className="inline-block size-3.5 shrink-0" aria-hidden="true" />
<span> <span>
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format( {Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(
repo?.forkCount, repo?.forkCount,
@@ -169,7 +169,7 @@ const Page = async () => {
} }
> >
View all View all
<ExternalLinkIcon className="inline-block size-3.5 shrink-0" aria-hidden="true" /> <IconExternalLink className="inline-block size-3.5 shrink-0" aria-hidden="true" />
</Button> </Button>
</p> </p>
</FadeTransition> </FadeTransition>
+1 -1
View File
@@ -10,7 +10,7 @@
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"iconLibrary": "lucide", "iconLibrary": "tabler",
"rtl": false, "rtl": false,
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
+2 -2
View File
@@ -48,8 +48,8 @@ const ActivityCalendar = ({
renderBlock={(block, activity) => ( renderBlock={(block, activity) => (
<Tooltip> <Tooltip>
<TooltipTrigger render={block} /> <TooltipTrigger render={block} />
<TooltipContent> <TooltipContent sideOffset={8}>
<span className="text-[0.825rem] font-medium">{`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${new Date(activity.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`}</span> {`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${new Date(activity.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
+6 -5
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import NumberFlow from "@number-flow/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getCommentCount } from "@/lib/server/comments"; import { getCommentCount } from "@/lib/server/comments";
@@ -28,11 +29,11 @@ const CommentCount = ({ slug }: { slug: string }) => {
} }
return ( return (
<span <NumberFlow
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(count)} ${count === 1 ? "comment" : "comments"}`} className={count === null ? "motion-safe:animate-pulse" : undefined}
> locales={process.env.NEXT_PUBLIC_SITE_LOCALE}
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(count)} value={count}
</span> />
); );
}; };
+6 -5
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { EditIcon, EllipsisIcon, Loader2Icon, ReplyIcon, Trash2Icon } from "lucide-react"; import { IconDots, IconEdit, IconMessageReply, IconTrash } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -21,6 +21,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Spinner } from "@/components/ui/spinner";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import { type CommentWithUser, deleteComment } from "@/lib/server/comments"; import { type CommentWithUser, deleteComment } from "@/lib/server/comments";
@@ -75,19 +76,19 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
setMode(mode.type === "replying" ? { type: "idle" } : { type: "replying" }) setMode(mode.type === "replying" ? { type: "idle" } : { type: "replying" })
} }
> >
<ReplyIcon /> <IconMessageReply />
Reply Reply
</Button> </Button>
{session.user.id === comment.user.id && ( {session.user.id === comment.user.id && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}> <DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
<EllipsisIcon /> <IconDots />
<span className="sr-only">Actions Menu</span> <span className="sr-only">Actions Menu</span>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={() => setMode({ type: "editing" })}> <DropdownMenuItem onClick={() => setMode({ type: "editing" })}>
<EditIcon /> <IconEdit />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
@@ -95,7 +96,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
disabled={isDeleting} disabled={isDeleting}
variant="destructive" variant="destructive"
> >
{isDeleting ? <Loader2Icon className="animate-spin" /> : <Trash2Icon />} {isDeleting ? <Spinner /> : <IconTrash />}
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
+5 -5
View File
@@ -1,12 +1,12 @@
"use client"; "use client";
import { InfoIcon, Loader2Icon } from "lucide-react"; import { IconInfoCircle, IconMarkdown } from "@tabler/icons-react";
import { createContext, useContext, useMemo, useState, useTransition } from "react"; import { createContext, useContext, useMemo, useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { MarkdownIcon } from "@/components/icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import { createComment, updateComment } from "@/lib/server/comments"; import { createComment, updateComment } from "@/lib/server/comments";
@@ -122,7 +122,7 @@ const SubmitButton = ({
<Button type="submit" disabled={isPending || disabled}> <Button type="submit" disabled={isPending || disabled}>
{isPending ? ( {isPending ? (
<> <>
<Loader2Icon className="animate-spin" /> <Spinner />
{pendingLabel} {pendingLabel}
</> </>
) : ( ) : (
@@ -134,7 +134,7 @@ const SubmitButton = ({
// Markdown help popover (only shown for new comments) // Markdown help popover (only shown for new comments)
const MarkdownHelp = () => ( const MarkdownHelp = () => (
<p className="text-muted-foreground text-[0.8rem] leading-relaxed"> <p className="text-muted-foreground text-[0.8rem] leading-relaxed">
<MarkdownIcon className="mr-1.5 inline-block size-4 align-text-top" /> <IconMarkdown className="mr-1.5 inline-block size-4 align-text-top" />
<span className="max-md:hidden">Basic&nbsp;</span> <span className="max-md:hidden">Basic&nbsp;</span>
<Popover> <Popover>
<PopoverTrigger <PopoverTrigger
@@ -150,7 +150,7 @@ const MarkdownHelp = () => (
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="start"> <PopoverContent align="start">
<p className="text-sm leading-loose"> <p className="text-sm leading-loose">
<InfoIcon className="mr-1.5 inline size-4.5 align-text-top" /> <IconInfoCircle className="mr-1.5 inline size-4.5 align-text-top" />
Examples: Examples:
</p> </p>
+2 -2
View File
@@ -1,11 +1,11 @@
"use client"; "use client";
import { Loader2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { GitHubIcon } from "@/components/icons"; import { GitHubIcon } from "@/components/icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { signIn } from "@/lib/auth-client"; import { signIn } from "@/lib/auth-client";
const SignIn = ({ callbackPath }: { callbackPath?: string }) => { const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
@@ -28,7 +28,7 @@ const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
return ( return (
<Button onClick={handleSignIn} disabled={isLoading} size="lg" variant="outline"> <Button onClick={handleSignIn} disabled={isLoading} size="lg" variant="outline">
{isLoading ? <Loader2Icon className="animate-spin" /> : <GitHubIcon />} {isLoading ? <Spinner /> : <GitHubIcon />}
Sign in with GitHub Sign in with GitHub
</Button> </Button>
); );
+5 -5
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { IconCheck, IconClipboardCheck, IconCopy } from "@tabler/icons-react";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { CheckIcon, ClipboardCheckIcon, CopyIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -34,7 +34,7 @@ function CopyButton({
copy(value); copy(value);
setHasCopied(true); setHasCopied(true);
toast.success("Copied!", { toast.success("Copied!", {
icon: <ClipboardCheckIcon className="text-foreground/85 size-4" aria-hidden="true" />, icon: <IconClipboardCheck className="text-foreground/85 size-4" aria-hidden="true" />,
duration: 2000, duration: 2000,
id: "copy-button-toast-success", id: "copy-button-toast-success",
}); });
@@ -52,7 +52,7 @@ function CopyButton({
size="icon" size="icon"
variant={variant} variant={variant}
className={cn( className={cn(
"bg-code hover:bg-accent dark:hover:bg-accent absolute top-3 right-2 z-10 size-7.5 hover:opacity-100 focus-visible:opacity-100", "text-muted-foreground bg-code hover:bg-accent dark:hover:bg-accent absolute top-3 right-2 z-10 size-7.5 hover:opacity-100 focus-visible:opacity-100",
hasCopied ? "cursor-default" : "cursor-pointer", hasCopied ? "cursor-default" : "cursor-pointer",
className, className,
)} )}
@@ -61,9 +61,9 @@ function CopyButton({
{...props} {...props}
> >
{hasCopied ? ( {hasCopied ? (
<CheckIcon className="text-green-600 dark:text-green-400" aria-hidden="true" /> <IconCheck className="text-green-600 dark:text-green-400" aria-hidden="true" />
) : ( ) : (
<CopyIcon aria-hidden="true" /> <IconCopy aria-hidden="true" />
)} )}
</Button> </Button>
); );
-6
View File
@@ -1,6 +0,0 @@
"use client";
// marking the library as a proper client component so that react doesn't complain about hydration whenever we use it in
// a server component.
// see: https://react.dev/reference/rsc/use-client#using-third-party-libraries
export { default as CountUp } from "react-countup";
-16
View File
@@ -1,5 +1,3 @@
// miscellaneous icons that are not part of lucide-react
export const Win95Icon = ({ className }: { className?: string }) => ( export const Win95Icon = ({ className }: { className?: string }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -14,20 +12,6 @@ export const Win95Icon = ({ className }: { className?: string }) => (
</svg> </svg>
); );
export const MarkdownIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
stroke="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
className={className}
aria-hidden="true"
>
<path d="M22.27 19.385H1.73A1.73 1.73 0 010 17.655V6.345a1.73 1.73 0 011.73-1.73h20.54A1.73 1.73 0 0124 6.345v11.308a1.73 1.73 0 01-1.73 1.731zM5.769 15.923v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.232 12h-2.309V8.077h-2.307V12h-2.308l3.461 4.039z" />
</svg>
);
export const GitHubIcon = ({ className }: { className?: string }) => ( export const GitHubIcon = ({ className }: { className?: string }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
+1
View File
@@ -40,6 +40,7 @@ const ImageDiff = ({
style={{ aspectRatio }} style={{ aspectRatio }}
itemOne={<ReactCompareSliderImage {...beforeImageProps} className="size-full object-cover" />} itemOne={<ReactCompareSliderImage {...beforeImageProps} className="size-full object-cover" />}
itemTwo={<ReactCompareSliderImage {...afterImageProps} className="size-full object-cover" />} itemTwo={<ReactCompareSliderImage {...afterImageProps} className="size-full object-cover" />}
suppressHydrationWarning
/> />
); );
}; };
+7 -7
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { AtSignIcon, ExternalLinkIcon, MoonIcon, SunIcon } from "lucide-react"; import { IconAt, IconExternalLink, IconMoon, IconSun } from "@tabler/icons-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -228,15 +228,15 @@ const ContactPopover = () => (
delay={0} delay={0}
render={<Button variant="ghost" size="icon" aria-label="Open contact links" />} render={<Button variant="ghost" size="icon" aria-label="Open contact links" />}
> >
<AtSignIcon aria-hidden="true" /> <IconAt aria-hidden="true" />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="end" className="max-h-(--available-height) overflow-y-auto"> <PopoverContent align="end" className="max-h-(--available-height) gap-2 overflow-y-auto p-2">
<PopoverHeader className="mt-1 px-1"> <PopoverHeader className="mt-1 px-1">
<PopoverTitle>Get in touch:</PopoverTitle> <PopoverTitle>Get in touch:</PopoverTitle>
<PopoverDescription className="sr-only">Email and social links.</PopoverDescription> <PopoverDescription className="sr-only">Email and social links.</PopoverDescription>
</PopoverHeader> </PopoverHeader>
<nav aria-label="Contact links" className="flex flex-col gap-1"> <nav aria-label="Contact links" className="flex flex-col gap-0.5">
{contactLinks.map((link) => ( {contactLinks.map((link) => (
<a <a
key={link.href} key={link.href}
@@ -255,7 +255,7 @@ const ContactPopover = () => (
{link.value} {link.value}
</span> </span>
{link.external ? ( {link.external ? (
<ExternalLinkIcon className="text-muted-foreground/70 size-3.5" aria-hidden="true" /> <IconExternalLink className="text-muted-foreground/70 size-3.5" aria-hidden="true" />
) : null} ) : null}
</a> </a>
))} ))}
@@ -339,8 +339,8 @@ const Header = ({ className }: { className?: string }) => {
aria-label="Toggle theme" aria-label="Toggle theme"
className="group" className="group"
> >
<SunIcon className="group-hover:stroke-orange-600 dark:hidden" aria-hidden="true" /> <IconSun className="group-hover:stroke-orange-600 dark:hidden" aria-hidden="true" />
<MoonIcon <IconMoon
className="not-dark:hidden group-hover:stroke-yellow-400" className="not-dark:hidden group-hover:stroke-yellow-400"
aria-hidden="true" aria-hidden="true"
/> />
+1 -1
View File
@@ -39,7 +39,7 @@ const Menu = () => {
nativeButton={false} nativeButton={false}
aria-label={item.text} aria-label={item.text}
data-current={isCurrent || undefined} data-current={isCurrent || undefined}
className="data-current:bg-accent/60 data-current:text-accent-foreground text-sm leading-none" className="data-current:bg-accent/60 data-current:text-accent-foreground px-2.5 py-3.5 text-sm leading-none"
render={<Link href={item.href} transitionTypes={transitionTypes} />} render={<Link href={item.href} transitionTypes={transitionTypes} />}
> >
{item.text} {item.text}
+3 -3
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { EyeIcon, MessagesSquareIcon } from "lucide-react"; import { IconEye, IconMessages } from "@tabler/icons-react";
import Link from "next/link"; import Link from "next/link";
import { createContext, type ReactNode, useContext, useEffect, useState } from "react"; import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
@@ -74,7 +74,7 @@ const PostStats = ({ slug }: { slug: string }) => {
variant="secondary" variant="secondary"
className="text-foreground/80 gap-[5px] text-[11px] tabular-nums" className="text-foreground/80 gap-[5px] text-[11px] tabular-nums"
> >
<EyeIcon className="text-foreground/65" aria-hidden="true" /> <IconEye className="text-foreground/65" aria-hidden="true" />
{numberFormatter.format(viewCount)} {numberFormatter.format(viewCount)}
</Badge> </Badge>
)} )}
@@ -90,7 +90,7 @@ const PostStats = ({ slug }: { slug: string }) => {
/> />
} }
> >
<MessagesSquareIcon className="text-foreground/65" aria-hidden="true" /> <IconMessages className="text-foreground/65" aria-hidden="true" />
{numberFormatter.format(commentCount)} {numberFormatter.format(commentCount)}
</Badge> </Badge>
)} )}
+2 -2
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react"; import { IconX } from "@tabler/icons-react";
import * as React from "react"; import * as React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -61,7 +61,7 @@ function DialogContent({
data-slot="dialog-close" data-slot="dialog-close"
render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />} render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />}
> >
<XIcon /> <IconX />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
+4 -4
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu"; import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { ChevronRightIcon, CheckIcon } from "lucide-react"; import { IconCheck, IconChevronRight } from "@tabler/icons-react";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -119,7 +119,7 @@ function DropdownMenuSubTrigger({
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <IconChevronRight className="ml-auto" />
</MenuPrimitive.SubmenuTrigger> </MenuPrimitive.SubmenuTrigger>
); );
} }
@@ -173,7 +173,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item-indicator" data-slot="dropdown-menu-checkbox-item-indicator"
> >
<MenuPrimitive.CheckboxItemIndicator> <MenuPrimitive.CheckboxItemIndicator>
<CheckIcon /> <IconCheck />
</MenuPrimitive.CheckboxItemIndicator> </MenuPrimitive.CheckboxItemIndicator>
</span> </span>
{children} {children}
@@ -208,7 +208,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item-indicator" data-slot="dropdown-menu-radio-item-indicator"
> >
<MenuPrimitive.RadioItemIndicator> <MenuPrimitive.RadioItemIndicator>
<CheckIcon /> <IconCheck />
</MenuPrimitive.RadioItemIndicator> </MenuPrimitive.RadioItemIndicator>
</span> </span>
{children} {children}
+5 -5
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { Select as SelectPrimitive } from "@base-ui/react/select"; import { Select as SelectPrimitive } from "@base-ui/react/select";
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"; import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -48,7 +48,7 @@ function SelectTrigger({
> >
{children} {children}
<SelectPrimitive.Icon <SelectPrimitive.Icon
render={<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />} render={<IconChevronDown className="text-muted-foreground pointer-events-none size-4" />}
/> />
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
); );
@@ -124,7 +124,7 @@ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Prop
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
} }
> >
<CheckIcon className="pointer-events-none" /> <IconCheck className="pointer-events-none" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item> </SelectPrimitive.Item>
); );
@@ -153,7 +153,7 @@ function SelectScrollUpButton({
)} )}
{...props} {...props}
> >
<ChevronUpIcon /> <IconChevronUp />
</SelectPrimitive.ScrollUpArrow> </SelectPrimitive.ScrollUpArrow>
); );
} }
@@ -171,7 +171,7 @@ function SelectScrollDownButton({
)} )}
{...props} {...props}
> >
<ChevronDownIcon /> <IconChevronDown />
</SelectPrimitive.ScrollDownArrow> </SelectPrimitive.ScrollDownArrow>
); );
} }
+12 -11
View File
@@ -1,15 +1,16 @@
"use client"; "use client";
import { import {
CircleCheckIcon, IconAlertTriangle,
InfoIcon, IconCircleCheck,
TriangleAlertIcon, IconCircleX,
OctagonXIcon, IconInfoCircle,
Loader2Icon, } from "@tabler/icons-react";
} from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner"; import { Toaster as Sonner, type ToasterProps } from "sonner";
import { Spinner } from "@/components/ui/spinner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme(); const { theme = "system" } = useTheme();
@@ -18,11 +19,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
icons={{ icons={{
success: <CircleCheckIcon className="size-4" />, success: <IconCircleCheck className="size-4" />,
info: <InfoIcon className="size-4" />, info: <IconInfoCircle className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />, warning: <IconAlertTriangle className="size-4" />,
error: <OctagonXIcon className="size-4" />, error: <IconCircleX className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />, loading: <Spinner className="size-4" />,
}} }}
style={ style={
{ {
+2 -2
View File
@@ -1,10 +1,10 @@
import { Loader2Icon } from "lucide-react"; import { IconLoader2 } from "@tabler/icons-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) { function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return ( return (
<Loader2Icon <IconLoader2
role="status" role="status"
aria-label="Loading" aria-label="Loading"
className={cn("size-4 animate-spin", className)} className={cn("size-4 animate-spin", className)}
+6 -10
View File
@@ -1,8 +1,8 @@
"use client"; "use client";
import NumberFlow from "@number-flow/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
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 }) => {
@@ -25,16 +25,12 @@ const ViewCounter = ({ slug }: { slug: string }) => {
return <span title="Error getting views! :(">?</span>; return <span title="Error getting views! :(">?</span>;
} }
if (views === null) {
return <span className="motion-safe:animate-pulse">0</span>;
}
return ( return (
<span <NumberFlow
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(views)} ${views === 1 ? "view" : "views"}`} className={views === null ? "motion-safe:animate-pulse" : undefined}
> locales={process.env.NEXT_PUBLIC_SITE_LOCALE}
<CountUp start={0} end={views} delay={0} duration={1.5} /> value={views ?? 0}
</span> />
); );
}; };
-2
View File
@@ -1,2 +0,0 @@
[tools]
node = "24"
+8 -8
View File
@@ -27,10 +27,12 @@
"@mdx-js/loader": "^3.1.1", "@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1", "@mdx-js/react": "^3.1.1",
"@next/mdx": "16.2.4", "@next/mdx": "16.2.4",
"@number-flow/react": "^0.6.0",
"@octokit/graphql": "^9.0.3", "@octokit/graphql": "^9.0.3",
"@octokit/graphql-schema": "^15.26.1", "@octokit/graphql-schema": "^15.26.1",
"@tabler/icons-react": "^3.41.1",
"@vercel/analytics": "^2.0.1", "@vercel/analytics": "^2.0.1",
"@vercel/functions": "^3.4.4", "@vercel/functions": "^3.4.6",
"@vercel/speed-insights": "^2.0.0", "@vercel/speed-insights": "^2.0.0",
"better-auth": "^1.6.9", "better-auth": "^1.6.9",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
@@ -41,14 +43,12 @@
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"feed": "^5.2.1", "feed": "^5.2.1",
"html-entities": "^2.6.0", "html-entities": "^2.6.0",
"lucide-react": "1.11.0",
"next": "16.2.4", "next": "16.2.4",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.20.0", "pg": "^8.20.0",
"react": "19.2.5", "react": "19.2.5",
"react-activity-calendar": "^3.2.0", "react-activity-calendar": "^3.2.0",
"react-compare-slider": "^4.0.0", "react-compare-slider": "^4.0.0",
"react-countup": "^6.5.3",
"react-dom": "19.2.5", "react-dom": "19.2.5",
"react-lite-youtube-embed": "~3.5.1", "react-lite-youtube-embed": "~3.5.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -72,12 +72,12 @@
"remark-smartypants": "^3.0.2", "remark-smartypants": "^3.0.2",
"remark-strip-mdx-imports-exports": "^1.0.1", "remark-strip-mdx-imports-exports": "^1.0.1",
"server-only": "0.0.1", "server-only": "0.0.1",
"shadcn": "^4.5.0", "shadcn": "^4.6.0",
"shiki": "^4.0.2", "shiki": "^4.0.2",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"zod": "^4.3.6" "zod": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.2.4", "@tailwindcss/postcss": "^4.2.4",
@@ -90,9 +90,9 @@
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"oxfmt": "^0.46.0", "oxfmt": "^0.47.0",
"oxlint": "^1.61.0", "oxlint": "^1.62.0",
"postcss": "^8.5.10", "postcss": "^8.5.12",
"schema-dts": "^2.0.0", "schema-dts": "^2.0.0",
"tailwindcss": "^4.2.4", "tailwindcss": "^4.2.4",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
+324 -325
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,5 +6,5 @@
"prHourlyLimit": 0, "prHourlyLimit": 0,
"rangeStrategy": "bump", "rangeStrategy": "bump",
"postUpdateOptions": ["pnpmDedupe"], "postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["@types/node", "lucide-react"] "ignoreDeps": ["@types/node"]
} }