1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 16:55:29 -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 Link from "next/link";
@@ -97,7 +97,7 @@ const Page = () => (
</div>
<span className="text-muted-foreground ml-9 text-xs text-pretty sm:ml-auto">
{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"
aria-hidden="true"
/>
+10 -16
View File
@@ -1,10 +1,4 @@
import {
CalendarDaysIcon,
EyeIcon,
MessagesSquareIcon,
SquarePenIcon,
TagIcon,
} from "lucide-react";
import { IconCalendarEvent, IconEdit, IconEye, IconMessages, IconTag } from "@tabler/icons-react";
import type { Metadata } from "next";
import Link from "next/link";
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
href={`/${POSTS_DIR}/${frontmatter?.slug}`}
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
dateTime={formattedDates.dateISO}
title={formattedDates.dateTitle}
@@ -129,7 +123,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
{frontmatter?.tags && (
<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) => (
<span
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`}
title={`Edit "${frontmatter?.title}" on GitHub`}
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>
</Link>
<Link
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}`} />
</Link>
<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}`} />
</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 { Suspense } from "react";
@@ -113,7 +113,7 @@ const Page = async () => {
rel="noopener noreferrer"
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>
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(
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"}`}
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>
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(
repo?.forkCount,
@@ -169,7 +169,7 @@ const Page = async () => {
}
>
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>
</p>
</FadeTransition>
+1 -1
View File
@@ -10,7 +10,7 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"iconLibrary": "tabler",
"rtl": false,
"aliases": {
"components": "@/components",
+2 -2
View File
@@ -48,8 +48,8 @@ const ActivityCalendar = ({
renderBlock={(block, activity) => (
<Tooltip>
<TooltipTrigger render={block} />
<TooltipContent>
<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>
<TooltipContent sideOffset={8}>
{`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${new Date(activity.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`}
</TooltipContent>
</Tooltip>
)}
+6 -5
View File
@@ -1,5 +1,6 @@
"use client";
import NumberFlow from "@number-flow/react";
import { useEffect, useState } from "react";
import { getCommentCount } from "@/lib/server/comments";
@@ -28,11 +29,11 @@ const CommentCount = ({ slug }: { slug: string }) => {
}
return (
<span
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(count)} ${count === 1 ? "comment" : "comments"}`}
>
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(count)}
</span>
<NumberFlow
className={count === null ? "motion-safe:animate-pulse" : undefined}
locales={process.env.NEXT_PUBLIC_SITE_LOCALE}
value={count}
/>
);
};
+6 -5
View File
@@ -1,6 +1,6 @@
"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 { toast } from "sonner";
@@ -21,6 +21,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Spinner } from "@/components/ui/spinner";
import { useSession } from "@/lib/auth-client";
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" })
}
>
<ReplyIcon />
<IconMessageReply />
Reply
</Button>
{session.user.id === comment.user.id && (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
<EllipsisIcon />
<IconDots />
<span className="sr-only">Actions Menu</span>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setMode({ type: "editing" })}>
<EditIcon />
<IconEdit />
Edit
</DropdownMenuItem>
<DropdownMenuItem
@@ -95,7 +96,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
disabled={isDeleting}
variant="destructive"
>
{isDeleting ? <Loader2Icon className="animate-spin" /> : <Trash2Icon />}
{isDeleting ? <Spinner /> : <IconTrash />}
Delete
</DropdownMenuItem>
</DropdownMenuContent>
+5 -5
View File
@@ -1,12 +1,12 @@
"use client";
import { InfoIcon, Loader2Icon } from "lucide-react";
import { IconInfoCircle, IconMarkdown } from "@tabler/icons-react";
import { createContext, useContext, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { MarkdownIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { useSession } from "@/lib/auth-client";
import { createComment, updateComment } from "@/lib/server/comments";
@@ -122,7 +122,7 @@ const SubmitButton = ({
<Button type="submit" disabled={isPending || disabled}>
{isPending ? (
<>
<Loader2Icon className="animate-spin" />
<Spinner />
{pendingLabel}
</>
) : (
@@ -134,7 +134,7 @@ const SubmitButton = ({
// Markdown help popover (only shown for new comments)
const MarkdownHelp = () => (
<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>
<Popover>
<PopoverTrigger
@@ -150,7 +150,7 @@ const MarkdownHelp = () => (
</PopoverTrigger>
<PopoverContent align="start">
<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:
</p>
+2 -2
View File
@@ -1,11 +1,11 @@
"use client";
import { Loader2Icon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { GitHubIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { signIn } from "@/lib/auth-client";
const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
@@ -28,7 +28,7 @@ const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
return (
<Button onClick={handleSignIn} disabled={isLoading} size="lg" variant="outline">
{isLoading ? <Loader2Icon className="animate-spin" /> : <GitHubIcon />}
{isLoading ? <Spinner /> : <GitHubIcon />}
Sign in with GitHub
</Button>
);
+5 -5
View File
@@ -1,7 +1,7 @@
"use client";
import { IconCheck, IconClipboardCheck, IconCopy } from "@tabler/icons-react";
import copy from "copy-to-clipboard";
import { CheckIcon, ClipboardCheckIcon, CopyIcon } from "lucide-react";
import * as React from "react";
import { toast } from "sonner";
@@ -34,7 +34,7 @@ function CopyButton({
copy(value);
setHasCopied(true);
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,
id: "copy-button-toast-success",
});
@@ -52,7 +52,7 @@ function CopyButton({
size="icon"
variant={variant}
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",
className,
)}
@@ -61,9 +61,9 @@ function CopyButton({
{...props}
>
{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>
);
-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 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -14,20 +12,6 @@ export const Win95Icon = ({ className }: { className?: string }) => (
</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 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
+1
View File
@@ -40,6 +40,7 @@ const ImageDiff = ({
style={{ aspectRatio }}
itemOne={<ReactCompareSliderImage {...beforeImageProps} className="size-full object-cover" />}
itemTwo={<ReactCompareSliderImage {...afterImageProps} className="size-full object-cover" />}
suppressHydrationWarning
/>
);
};
+7 -7
View File
@@ -1,6 +1,6 @@
"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 Image from "next/image";
import Link from "next/link";
@@ -228,15 +228,15 @@ const ContactPopover = () => (
delay={0}
render={<Button variant="ghost" size="icon" aria-label="Open contact links" />}
>
<AtSignIcon aria-hidden="true" />
<IconAt aria-hidden="true" />
</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">
<PopoverTitle>Get in touch:</PopoverTitle>
<PopoverDescription className="sr-only">Email and social links.</PopoverDescription>
</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) => (
<a
key={link.href}
@@ -255,7 +255,7 @@ const ContactPopover = () => (
{link.value}
</span>
{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}
</a>
))}
@@ -339,8 +339,8 @@ const Header = ({ className }: { className?: string }) => {
aria-label="Toggle theme"
className="group"
>
<SunIcon className="group-hover:stroke-orange-600 dark:hidden" aria-hidden="true" />
<MoonIcon
<IconSun className="group-hover:stroke-orange-600 dark:hidden" aria-hidden="true" />
<IconMoon
className="not-dark:hidden group-hover:stroke-yellow-400"
aria-hidden="true"
/>
+1 -1
View File
@@ -39,7 +39,7 @@ const Menu = () => {
nativeButton={false}
aria-label={item.text}
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} />}
>
{item.text}
+3 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { EyeIcon, MessagesSquareIcon } from "lucide-react";
import { IconEye, IconMessages } from "@tabler/icons-react";
import Link from "next/link";
import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
@@ -74,7 +74,7 @@ const PostStats = ({ slug }: { slug: string }) => {
variant="secondary"
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)}
</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)}
</Badge>
)}
+2 -2
View File
@@ -1,7 +1,7 @@
"use client";
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 { Button } from "@/components/ui/button";
@@ -61,7 +61,7 @@ function DialogContent({
data-slot="dialog-close"
render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />}
>
<XIcon />
<IconX />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
+4 -4
View File
@@ -1,7 +1,7 @@
"use client";
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 { cn } from "@/lib/utils";
@@ -119,7 +119,7 @@ function DropdownMenuSubTrigger({
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
<IconChevronRight className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
}
@@ -173,7 +173,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
<IconCheck />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
@@ -208,7 +208,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon />
<IconCheck />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
+5 -5
View File
@@ -1,7 +1,7 @@
"use client";
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 { cn } from "@/lib/utils";
@@ -48,7 +48,7 @@ function SelectTrigger({
>
{children}
<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>
);
@@ -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" />
}
>
<CheckIcon className="pointer-events-none" />
<IconCheck className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
);
@@ -153,7 +153,7 @@ function SelectScrollUpButton({
)}
{...props}
>
<ChevronUpIcon />
<IconChevronUp />
</SelectPrimitive.ScrollUpArrow>
);
}
@@ -171,7 +171,7 @@ function SelectScrollDownButton({
)}
{...props}
>
<ChevronDownIcon />
<IconChevronDown />
</SelectPrimitive.ScrollDownArrow>
);
}
+12 -11
View File
@@ -1,15 +1,16 @@
"use client";
import {
CircleCheckIcon,
InfoIcon,
TriangleAlertIcon,
OctagonXIcon,
Loader2Icon,
} from "lucide-react";
IconAlertTriangle,
IconCircleCheck,
IconCircleX,
IconInfoCircle,
} from "@tabler/icons-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
import { Spinner } from "@/components/ui/spinner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
@@ -18,11 +19,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
success: <IconCircleCheck className="size-4" />,
info: <IconInfoCircle className="size-4" />,
warning: <IconAlertTriangle className="size-4" />,
error: <IconCircleX className="size-4" />,
loading: <Spinner className="size-4" />,
}}
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";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
<IconLoader2
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
+6 -10
View File
@@ -1,8 +1,8 @@
"use client";
import NumberFlow from "@number-flow/react";
import { useEffect, useState } from "react";
import { CountUp } from "@/components/count-up";
import { incrementViews } from "@/lib/server/views";
const ViewCounter = ({ slug }: { slug: string }) => {
@@ -25,16 +25,12 @@ const ViewCounter = ({ slug }: { slug: string }) => {
return <span title="Error getting views! :(">?</span>;
}
if (views === null) {
return <span className="motion-safe:animate-pulse">0</span>;
}
return (
<span
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(views)} ${views === 1 ? "view" : "views"}`}
>
<CountUp start={0} end={views} delay={0} duration={1.5} />
</span>
<NumberFlow
className={views === null ? "motion-safe:animate-pulse" : undefined}
locales={process.env.NEXT_PUBLIC_SITE_LOCALE}
value={views ?? 0}
/>
);
};
-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/react": "^3.1.1",
"@next/mdx": "16.2.4",
"@number-flow/react": "^0.6.0",
"@octokit/graphql": "^9.0.3",
"@octokit/graphql-schema": "^15.26.1",
"@tabler/icons-react": "^3.41.1",
"@vercel/analytics": "^2.0.1",
"@vercel/functions": "^3.4.4",
"@vercel/functions": "^3.4.6",
"@vercel/speed-insights": "^2.0.0",
"better-auth": "^1.6.9",
"cheerio": "^1.2.0",
@@ -41,14 +43,12 @@
"fast-glob": "^3.3.3",
"feed": "^5.2.1",
"html-entities": "^2.6.0",
"lucide-react": "1.11.0",
"next": "16.2.4",
"next-themes": "^0.4.6",
"pg": "^8.20.0",
"react": "19.2.5",
"react-activity-calendar": "^3.2.0",
"react-compare-slider": "^4.0.0",
"react-countup": "^6.5.3",
"react-dom": "19.2.5",
"react-lite-youtube-embed": "~3.5.1",
"react-markdown": "^10.1.0",
@@ -72,12 +72,12 @@
"remark-smartypants": "^3.0.2",
"remark-strip-mdx-imports-exports": "^1.0.1",
"server-only": "0.0.1",
"shadcn": "^4.5.0",
"shadcn": "^4.6.0",
"shiki": "^4.0.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"unified": "^11.0.5",
"zod": "^4.3.6"
"zod": "^4.4.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.4",
@@ -90,9 +90,9 @@
"babel-plugin-react-compiler": "1.0.0",
"dotenv": "^17.4.2",
"drizzle-kit": "^0.31.10",
"oxfmt": "^0.46.0",
"oxlint": "^1.61.0",
"postcss": "^8.5.10",
"oxfmt": "^0.47.0",
"oxlint": "^1.62.0",
"postcss": "^8.5.12",
"schema-dts": "^2.0.0",
"tailwindcss": "^4.2.4",
"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,
"rangeStrategy": "bump",
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["@types/node", "lucide-react"]
"ignoreDeps": ["@types/node"]
}