mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-06-05 19:15:30 -04:00
refactor: simplify next link component usage
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { env } from "@/lib/env";
|
||||
import { getCommentCounts } from "@/lib/server/comments";
|
||||
|
||||
const CommentCount = async ({ slug }: { slug: string }) => {
|
||||
const count = await getCommentCounts(slug);
|
||||
|
||||
return (
|
||||
<span
|
||||
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(count)} ${count === 1 ? "comment" : "comments"}`}
|
||||
>
|
||||
{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(count)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentCount;
|
||||
@@ -7,7 +7,6 @@ import { InfoIcon, Loader2Icon } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import Button from "@/components/ui/button";
|
||||
import Textarea from "@/components/ui/textarea";
|
||||
import Link from "@/components/link";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { MarkdownIcon } from "@/components/icons";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
@@ -132,9 +131,9 @@ const CommentForm = ({
|
||||
</li>
|
||||
<li>
|
||||
[
|
||||
<Link href="https://jarv.is" className="hover:no-underline">
|
||||
<a href="https://jarv.is" target="_blank" rel="noopener" className="hover:no-underline">
|
||||
links
|
||||
</Link>
|
||||
</a>
|
||||
](https://jarv.is)
|
||||
</li>
|
||||
<li>
|
||||
@@ -148,9 +147,13 @@ const CommentForm = ({
|
||||
</ul>
|
||||
|
||||
<p className="text-sm leading-loose">
|
||||
<Link href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax">
|
||||
<a
|
||||
href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more.
|
||||
</Link>
|
||||
</a>
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getImageProps } from "next/image";
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import Link from "@/components/link";
|
||||
import RelativeTime from "@/components/relative-time";
|
||||
import Actions from "./comment-actions";
|
||||
import { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
||||
@@ -35,9 +35,14 @@ const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Link href={`https://github.com/${comment.user.name}`} className="font-medium hover:no-underline">
|
||||
<a
|
||||
href={`https://github.com/${comment.user.name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:no-underline"
|
||||
>
|
||||
@{comment.user.name}
|
||||
</Link>
|
||||
</a>
|
||||
<Link href={`#${divId}`} className="text-muted-foreground text-xs leading-none hover:no-underline">
|
||||
<RelativeTime date={comment.createdAt} />
|
||||
</Link>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useActionState, useState } from "react";
|
||||
import { useDebounce } from "react-use";
|
||||
import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react";
|
||||
import Form from "next/form";
|
||||
import Link from "@/components/link";
|
||||
import Input from "@/components/ui/input";
|
||||
import Textarea from "@/components/ui/textarea";
|
||||
import Button from "@/components/ui/button";
|
||||
@@ -113,13 +112,19 @@ const ContactForm = () => {
|
||||
|
||||
<div className="text-foreground/85 my-2 text-[0.8rem] leading-relaxed">
|
||||
<MarkdownIcon className="mr-1.5 inline-block size-4 align-text-top" /> Basic{" "}
|
||||
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" className="font-semibold">
|
||||
<a
|
||||
href="https://commonmark.org/help/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Markdown reference sheet"
|
||||
className="font-semibold"
|
||||
>
|
||||
Markdown syntax
|
||||
</Link>{" "}
|
||||
</a>{" "}
|
||||
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
|
||||
<Link href="https://jarv.is" className="hover:no-underline">
|
||||
<a href="https://jarv.is" target="_blank" rel="noopener" className="hover:no-underline">
|
||||
links
|
||||
</Link>
|
||||
</a>
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +150,7 @@ const ContactForm = () => {
|
||||
<div
|
||||
className={cn(
|
||||
"space-x-0.5 text-[0.9rem] font-semibold",
|
||||
formState.success ? "text-success" : "text-destructive"
|
||||
formState.success ? "text-green-600 dark:text-green-400" : "text-destructive"
|
||||
)}
|
||||
>
|
||||
{formState.success ? <CheckIcon className="inline size-4" /> : <XIcon className="inline size-4" />}{" "}
|
||||
|
||||
+29
-24
@@ -2,20 +2,18 @@
|
||||
|
||||
import * as React from "react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { CheckIcon, ClipboardCheckIcon, CopyIcon } from "lucide-react";
|
||||
import Button from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function CopyButton({
|
||||
value,
|
||||
className,
|
||||
variant = "ghost",
|
||||
tooltip = "Copy to Clipboard",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button> & {
|
||||
value: string;
|
||||
tooltip?: string;
|
||||
}) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false);
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
@@ -29,8 +27,15 @@ function CopyButton({
|
||||
}, []);
|
||||
|
||||
const handleCopy = () => {
|
||||
if (hasCopied) return;
|
||||
|
||||
copy(value);
|
||||
setHasCopied(true);
|
||||
toast.success("Copied!", {
|
||||
icon: <ClipboardCheckIcon className="text-foreground/85 size-4" aria-hidden="true" />,
|
||||
duration: 2000,
|
||||
id: "copy-button-toast-success",
|
||||
});
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
@@ -39,26 +44,26 @@ function CopyButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
data-slot="copy-button"
|
||||
data-copied={hasCopied}
|
||||
size="icon"
|
||||
variant={variant}
|
||||
className={cn(
|
||||
"bg-code absolute top-3 right-2 z-10 size-7 hover:opacity-100 focus-visible:opacity-100",
|
||||
className
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
aria-label={hasCopied ? "Copied" : tooltip}
|
||||
{...props}
|
||||
>
|
||||
{hasCopied ? <CheckIcon aria-hidden="true" /> : <CopyIcon aria-hidden="true" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{hasCopied ? "Copied" : tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
data-slot="copy-button"
|
||||
data-copied={hasCopied}
|
||||
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",
|
||||
hasCopied ? "cursor-default" : "cursor-pointer",
|
||||
className
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
aria-label={hasCopied ? "Copied" : "Copy to clipboard"}
|
||||
{...props}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="text-green-600 dark:text-green-400" aria-hidden="true" />
|
||||
) : (
|
||||
<CopyIcon aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { env } from "@/lib/env";
|
||||
import Link from "@/components/link";
|
||||
import Link from "next/link";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="text-muted-foreground mt-8 w-full py-6 text-center text-[13px] leading-loose">
|
||||
Content{" "}
|
||||
<Link href="/license" prefetch={false}>
|
||||
licensed under {siteConfig.license}
|
||||
</Link>
|
||||
,{" "}
|
||||
<Link href="/previously" prefetch={false} title="Previously on...">
|
||||
Content <Link href="/license">licensed under {siteConfig.license}</Link>,{" "}
|
||||
<Link href="/previously" title="Previously on...">
|
||||
{siteConfig.copyrightYearStart}
|
||||
</Link>{" "}
|
||||
– 2026.{" "}
|
||||
<Link
|
||||
<a
|
||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="View Source on GitHub"
|
||||
className="font-medium underline underline-offset-4"
|
||||
>
|
||||
View source.
|
||||
</Link>
|
||||
</a>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import Link from "@/components/link";
|
||||
import Link from "next/link";
|
||||
import Button from "@/components/ui/button";
|
||||
import Separator from "@/components/ui/separator";
|
||||
import Menu from "@/components/layout/menu";
|
||||
@@ -60,7 +60,9 @@ const Header = ({ className }: { className?: string }) => {
|
||||
quality={75}
|
||||
priority
|
||||
/>
|
||||
<span className="text-[17px] font-medium whitespace-nowrap max-md:sr-only">{siteConfig.name}</span>
|
||||
<span className="text-[17.5px] font-medium tracking-tight whitespace-nowrap max-md:sr-only">
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</Link>
|
||||
<Separator orientation="vertical" className="!h-6" />
|
||||
<Menu />
|
||||
@@ -68,9 +70,9 @@ const Header = ({ className }: { className?: string }) => {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" aria-label="Open GitHub profile" asChild>
|
||||
<Link href={`https://github.com/${authorConfig.social.github}`}>
|
||||
<a href={`https://github.com/${authorConfig.social.github}`} target="_blank" rel="noopener noreferrer">
|
||||
<GitHubIcon />
|
||||
</Link>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import Button from "@/components/ui/button";
|
||||
import Link from "@/components/link";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -49,9 +49,7 @@ const Menu = () => {
|
||||
data-current={isCurrent || undefined}
|
||||
className="data-current:bg-accent/60 data-current:text-accent-foreground"
|
||||
>
|
||||
<Link href={item.href} prefetch={false}>
|
||||
{item.text}
|
||||
</Link>
|
||||
<Link href={item.href}>{item.text}</Link>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
@@ -66,15 +64,8 @@ const Menu = () => {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="min-w-[140px]">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="/"
|
||||
prefetch={false}
|
||||
data-current={segment === ""}
|
||||
aria-current={segment === "" ? "page" : undefined}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<DropdownMenuItem asChild data-current={segment === ""} aria-current={segment === "" ? "page" : undefined}>
|
||||
<Link href="/">Home</Link>
|
||||
</DropdownMenuItem>
|
||||
{menuItems.map((item, index) => {
|
||||
const isCurrent = item.href?.split("/")[1] === segment;
|
||||
@@ -83,13 +74,10 @@ const Menu = () => {
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
key={index}
|
||||
className="data-current:bg-accent/40 data-current:text-accent-foreground data-current:font-medium"
|
||||
data-current={isCurrent || undefined}
|
||||
aria-current={isCurrent ? "page" : undefined}
|
||||
>
|
||||
<Link href={item.href} prefetch={false}>
|
||||
{item.text}
|
||||
</Link>
|
||||
<Link href={item.href}>{item.text}</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Link from "@/components/link";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const PageTitle = ({
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import NextLink from "next/link";
|
||||
|
||||
const Link = ({ href, rel, target, ...rest }: React.ComponentProps<typeof NextLink>) => {
|
||||
// This component auto-detects whether or not this link should open in the same window (the default for internal
|
||||
// links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
|
||||
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
|
||||
|
||||
const linkProps = {
|
||||
href,
|
||||
target: target || (isExternal ? "_blank" : undefined),
|
||||
rel: `${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}`.trim() || undefined,
|
||||
...rest,
|
||||
};
|
||||
|
||||
// don't waste time with next's component if it's just an external link
|
||||
if (isExternal) {
|
||||
return <a {...(linkProps as unknown as React.ComponentProps<"a">)} />;
|
||||
}
|
||||
|
||||
return <NextLink {...linkProps} />;
|
||||
};
|
||||
|
||||
export default Link;
|
||||
Vendored
+7
-2
@@ -1,4 +1,3 @@
|
||||
import Link from "@/components/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Gist = async ({
|
||||
@@ -24,7 +23,13 @@ const Gist = async ({
|
||||
return (
|
||||
<p className="text-center">
|
||||
Failed to load gist.{" "}
|
||||
<Link href={`https://gist.github.com/${id}${file ? `?file=${file}` : ""}`}>Try opening it manually?</Link>
|
||||
<a
|
||||
href={`https://gist.github.com/${id}${file ? `?file=${file}` : ""}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Try opening it manually?
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user