1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 19:15:30 -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
@@ -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}
/>
);
};