Skip to content

Commit 307da68

Browse files
committed
refactor(links): introduce SmartLink component to handle internal and external links
1 parent 4b22795 commit 307da68

File tree

11 files changed

+107
-126
lines changed

11 files changed

+107
-126
lines changed

src/app/join-us/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AnimatedFillButton } from "@/components/primitives/animated-fill-button";
22
import { PageHeader } from "@/components/primitives/page-header";
3+
import { SmartLink } from "@/components/primitives/smart-link";
34
import { buildPageMetadata, siteConfig } from "@/lib/config";
45
import { Metadata } from "next";
56

@@ -32,12 +33,12 @@ export default function JoinUs() {
3233
</div>
3334
<p className="text-muted-foreground pt-3 text-sm md:text-base">
3435
For more information, contact us at{" "}
35-
<a
36+
<SmartLink
3637
href={`mailto:${siteConfig.email}`}
3738
className="text-primary hover:underline"
3839
>
3940
{siteConfig.email}
40-
</a>
41+
</SmartLink>
4142
.
4243
</p>
4344
</section>

src/app/page.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { description } from "@/app/layout";
22
import { PageHeader } from "@/components/primitives/page-header";
3+
import { SmartLink } from "@/components/primitives/smart-link";
34
import { FeaturedProgram } from "@/components/sections/featured-program";
45
import { Hero } from "@/components/sections/hero";
56
import { WhatsHappening } from "@/components/sections/whats-happening";
67
import { Button } from "@/components/ui/button";
78
import { buildPageMetadata, siteConfig } from "@/lib/config";
89
import { Metadata } from "next";
9-
import Link from "next/link";
1010

1111
export const metadata: Metadata = buildPageMetadata("/", { description, title: "Home - ALPHA HKU" });
1212

@@ -51,14 +51,12 @@ export default function Home() {
5151
description={
5252
<>
5353
Carrying{" "}
54-
<a
54+
<SmartLink
5555
href={siteConfig.parentOrg}
56-
target="_blank"
57-
rel="noopener"
5856
className="text-primary hover:text-primary/80 underline transition-colors"
5957
>
6058
ALPHA Education
61-
</a>
59+
</SmartLink>
6260
{"'"}s mandate, we are an independent student organization, the largest student initiative in
6361
HKU, formed by an installation size of 30 students to spread the message of peace and humanity.
6462
<br />
@@ -69,7 +67,7 @@ export default function Home() {
6967
}
7068
>
7169
<Button asChild>
72-
<Link href="/about-us/our-story">Explore Our Story</Link>
70+
<SmartLink href="/about-us/our-story">Explore Our Story</SmartLink>
7371
</Button>
7472
</PageHeader>
7573
</section>
Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
import { SmartLink } from "@/components/primitives/smart-link";
12
import { Button } from "@/components/ui/button";
2-
import { isInternalHref } from "@/lib/utils";
3-
import Link from "next/link";
43
import type { ReactNode } from "react";
54

65
type AnimatedFillButtonProps = {
@@ -16,17 +15,7 @@ export function AnimatedFillButton({ href, children }: AnimatedFillButtonProps)
1615
size="lg"
1716
className="from-primary to-primary text-primary hover:text-primary-foreground rounded-full bg-gradient-to-r bg-[length:0%_100%] bg-no-repeat transition-[background-size,color] duration-300 hover:bg-[length:100%_100%]"
1817
>
19-
{isInternalHref(href) ? (
20-
<Link href={href}>{children}</Link>
21-
) : (
22-
<a
23-
href={href}
24-
target="_blank"
25-
rel="noopener"
26-
>
27-
{children}
28-
</a>
29-
)}
18+
<SmartLink href={href}>{children}</SmartLink>
3019
</Button>
3120
);
3221
}

src/components/primitives/exco-card.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { SmartLink } from "@/components/primitives/smart-link";
34
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
45
import type { ExcoLink, ExcoMember } from "@/lib/exco";
56
import { cn } from "@/lib/utils";
@@ -112,7 +113,7 @@ function BackFace({ member }: { member: ExcoMember }) {
112113
{hasLinks ? (
113114
<div className={cn("flex items-center justify-center gap-3", !hasBio && "m-auto")}>
114115
{member.links!.map((link) => (
115-
<a
116+
<SmartLink
116117
key={`${member.name}-${link.type}-${link.url}`}
117118
href={link.type === "email" ? `mailto:${link.url}` : link.url}
118119
target={link.type === "email" ? undefined : "_blank"}
@@ -123,7 +124,7 @@ function BackFace({ member }: { member: ExcoMember }) {
123124
onTouchStart={(e) => e.stopPropagation()}
124125
>
125126
<SocialIcon link={link} />
126-
</a>
127+
</SmartLink>
127128
))}
128129
</div>
129130
) : null}

src/components/primitives/partner-card.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SmartLink } from "@/components/primitives/smart-link";
12
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
23
import type { Partner } from "@/lib/partners";
34
import { cn } from "@/lib/utils";
@@ -10,11 +11,7 @@ type PartnerCardProps = {
1011

1112
export function PartnerCard({ partner, className }: PartnerCardProps) {
1213
return (
13-
<a
14-
href={partner.href}
15-
target="_blank"
16-
rel="noopener"
17-
>
14+
<SmartLink href={partner.href}>
1815
<Card className={cn("h-full", className)}>
1916
<CardHeader className="text-center">
2017
{partner.logo && (
@@ -34,6 +31,6 @@ export function PartnerCard({ partner, className }: PartnerCardProps) {
3431
<p className="text-muted-foreground text-sm">{partner.href}</p>
3532
</CardContent>
3633
</Card>
37-
</a>
34+
</SmartLink>
3835
);
3936
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client";
2+
3+
import { isInternalHref } from "@/lib/utils";
4+
import Link from "next/link";
5+
import { forwardRef, type AnchorHTMLAttributes, type ReactNode } from "react";
6+
7+
type SmartLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
8+
href: string; // mark href as required for strict validation
9+
children: ReactNode;
10+
};
11+
12+
export const SmartLink = forwardRef<HTMLAnchorElement, SmartLinkProps>(function SmartLink(
13+
{ href, children, className, target, rel, ...rest },
14+
ref
15+
) {
16+
const isInternal = isInternalHref(href);
17+
18+
if (isInternal) {
19+
return (
20+
<Link
21+
href={href}
22+
className={className}
23+
ref={ref}
24+
{...rest}
25+
>
26+
{children}
27+
</Link>
28+
);
29+
}
30+
31+
const computedTarget = target ?? "_blank";
32+
const computedRel = rel ?? (computedTarget === "_blank" ? "noopener" : undefined);
33+
34+
return (
35+
<a
36+
href={href}
37+
className={className}
38+
target={computedTarget}
39+
rel={computedRel}
40+
ref={ref}
41+
{...rest}
42+
>
43+
{children}
44+
</a>
45+
);
46+
});
47+
48+
export default SmartLink;

src/components/sections/featured-program.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { SmartLink } from "@/components/primitives/smart-link";
12
import { Button } from "@/components/ui/button";
23
import { Card } from "@/components/ui/card";
34
import Image from "next/image";
4-
import Link from "next/link";
55

66
type FeaturedProgramProps = {
77
heading: string;
@@ -32,7 +32,7 @@ export function FeaturedProgram({
3232
<p className="text-muted-foreground">{description}</p>
3333
{ctaHref && ctaLabel ? (
3434
<Button asChild>
35-
<Link href={ctaHref}>{ctaLabel}</Link>
35+
<SmartLink href={ctaHref}>{ctaLabel}</SmartLink>
3636
</Button>
3737
) : null}
3838
</div>

src/components/sections/footer.tsx

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { SmartLink } from "@/components/primitives/smart-link";
12
import { siteConfig } from "@/lib/config";
23
import { cn } from "@/lib/utils";
34
import { SiGithub, SiInstagram } from "@icons-pack/react-simple-icons";
45
import { Mail } from "lucide-react";
5-
import Link from "next/link";
66
import { FaLinkedin } from "react-icons/fa";
77

88
const sitemapLinks = [...siteConfig.mainNav, ...siteConfig.utilityNav];
@@ -19,68 +19,60 @@ export function Footer() {
1919
<ul className="grid grid-cols-2 gap-x-8 gap-y-2">
2020
{sitemapLinks.map((link) => (
2121
<li key={link.href}>
22-
<Link
22+
<SmartLink
2323
href={link.href}
2424
className={baseTextLinkClass}
2525
>
2626
{link.label}
27-
</Link>
27+
</SmartLink>
2828
</li>
2929
))}
3030
</ul>
3131
</div>
3232
<div className="flex flex-col items-center space-y-4">
3333
<h3 className="font-semibold">Connect with us!</h3>
3434
<div className="flex flex-col items-start gap-3">
35-
<a
35+
<SmartLink
3636
href={`mailto:${siteConfig.email}`}
3737
className={cn(baseTextLinkClass, "inline-flex items-center gap-2")}
3838
>
3939
<Mail size={18} />
4040
{siteConfig.email}
41-
</a>
42-
<a
41+
</SmartLink>
42+
<SmartLink
4343
href={siteConfig.instagram}
44-
target="_blank"
45-
rel="noopener"
4644
className={cn(baseTextLinkClass, "inline-flex items-center gap-2")}
4745
>
4846
<SiInstagram size={18} />
4947
Instagram
50-
</a>
51-
<a
48+
</SmartLink>
49+
<SmartLink
5250
href={siteConfig.github}
53-
target="_blank"
54-
rel="noopener"
5551
className={cn(baseTextLinkClass, "inline-flex items-center gap-2")}
5652
>
5753
<SiGithub size={18} />
5854
GitHub
59-
</a>
60-
<a
55+
</SmartLink>
56+
<SmartLink
6157
href={siteConfig.linkedin}
62-
target="_blank"
63-
rel="noopener"
6458
className={cn(baseTextLinkClass, "inline-flex items-center gap-2")}
6559
>
6660
<FaLinkedin size={18} />
6761
LinkedIn
68-
</a>
62+
</SmartLink>
6963
</div>
7064
</div>
7165
</div>
7266
<div className="border-border/40 text-muted-foreground mt-8 border-t pt-8 text-center text-sm">
7367
© {new Date().getFullYear()} ALPHA University Chapter at the University of Hong Kong. All rights
7468
reserved.
7569
<br />
76-
<a
70+
<SmartLink
7771
href={siteConfig.parentOrg}
78-
target="_blank"
79-
rel="noopener"
8072
className={cn(baseTextLinkClass, "inline-flex items-center gap-2 underline")}
8173
>
8274
{siteConfig.parentOrg.replace("https://", "")}
83-
</a>
75+
</SmartLink>
8476
</div>
8577
</div>
8678
</div>

0 commit comments

Comments
 (0)