Skip to content

Commit 414ca7c

Browse files
author
Oleksandr Ratushnyi
committed
Add interaction components and enhance LinkCard styles for improved UX
1 parent 8fbcd5b commit 414ca7c

File tree

10 files changed

+246
-29
lines changed

10 files changed

+246
-29
lines changed

src/app/blog/[slug]/page.module.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
margin: 6rem auto 0;
8787
padding: 3rem 2rem 0;
8888
border-top: 1px solid var(--N100);
89+
display: flex;
90+
align-items: center;
91+
justify-content: space-between;
8992
}
9093

9194
.footerLink {
@@ -126,3 +129,10 @@
126129
padding: 4rem 3rem 0;
127130
}
128131
}
132+
133+
.interactions {
134+
display: flex;
135+
align-items: center;
136+
justify-content: space-between;
137+
gap: 2rem;
138+
}

src/app/blog/[slug]/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getPostBySlug, getPostSlugs, formatDate } from "@/lib/blog";
88
import { mdxComponents } from "@/lib/mdx-components";
99
import { Article } from "@/components/Article";
1010
import styles from "./page.module.css";
11+
import { LikeButton, ViewCounter } from "@/components";
1112

1213
interface PageProps {
1314
params: Promise<{ slug: string }>;
@@ -113,6 +114,10 @@ export default async function BlogPostPage({ params }: PageProps) {
113114
<Link href="/blog" className={styles.footerLink}>
114115
← Back to Blog
115116
</Link>
117+
<div className={styles.interactions}>
118+
<ViewCounter slug={slug} />
119+
<LikeButton slug={slug} />
120+
</div>
116121
</footer>
117122
</article>
118123
</main>

src/components/Article/Article.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,10 @@
130130
font-size: 2.4rem;
131131
}
132132
}
133+
134+
.backLink {
135+
font-size: 1.6rem;
136+
color: var(--N500);
137+
text-decoration: none;
138+
transition: color 0.2s ease;
139+
}

src/components/Article/Article.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import clsx from "clsx";
33
import styles from "./Article.module.css";
44
import { LikeButton } from "../LikeButton";
55
import { ViewCounter } from "../ViewCounter";
6+
import Link from "next/link";
67

78
interface IArticleProps {
89
children: React.ReactNode;
@@ -14,10 +15,6 @@ export const Article = function Article({ children, className, slug }: IArticleP
1415
return (
1516
<article className={clsx(styles.article, className)}>
1617
{children}
17-
<footer className={styles.footer}>
18-
<ViewCounter slug={slug} />
19-
<LikeButton slug={slug} />
20-
</footer>
2118
</article>
2219
);
2320
};

src/components/Callout/Callout.module.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99
}
1010

1111
.callout.info {
12-
background-color: rgba(0, 101, 255, 0.06);
12+
background-color: rgba(0, 101, 255, 0.2);
1313
}
1414

1515
.callout.warning {
16-
background-color: rgba(255, 171, 0, 0.08);
16+
background-color: rgba(255, 171, 0, 0.2);
1717
}
1818

1919
.callout.tip {
20-
background-color: rgba(0, 135, 90, 0.06);
20+
background-color: rgba(0, 135, 90, 0.2);
2121
}
2222

2323
.callout.note {
24-
background-color: var(--N20);
24+
background-color: var(--N30);
2525
}
2626

2727
.emoji {

src/components/LikeButton/LikeButton.module.css

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
.button {
2+
--size: 1.5rem;
23
display: inline-flex;
34
align-items: center;
4-
gap: 0.375rem;
5+
gap: calc(var(--size) / 3);
56
padding: 0.5rem 0.75rem;
67
border: 1px solid var(--N40);
78
border-radius: 6px;
89
background-color: var(--N0);
910
color: var(--N300);
10-
font-size: 1.2rem;
11+
font-size: var(--size);
1112
font-weight: 500;
1213
cursor: pointer;
1314
transition: all 0.15s ease;
@@ -29,8 +30,8 @@
2930
}
3031

3132
.heart {
32-
width: 1.2rem;
33-
height: 1.2rem;
33+
width: var(--size);
34+
height: var(--size);
3435
flex-shrink: 0;
3536
}
3637

src/components/LinkCard/LinkCard.module.css

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
/* ===== INLINE: just favicon + domain ===== */
2+
.inline {
3+
display: inline-flex;
4+
align-items: center;
5+
gap: 0.375rem;
6+
padding: 0.25rem 0.5rem;
7+
border-radius: 4px;
8+
text-decoration: none;
9+
color: var(--N600);
10+
font-size: 0.9rem;
11+
transition: background-color 0.15s ease;
12+
}
13+
14+
.inline:hover {
15+
background-color: var(--N30);
16+
}
17+
18+
.faviconSmall {
19+
width: 1rem;
20+
height: 1rem;
21+
flex-shrink: 0;
22+
border-radius: 2px;
23+
}
24+
25+
.inlineDomain {
26+
color: var(--N600);
27+
}
28+
29+
.iconSmall {
30+
width: 0.875rem;
31+
height: 0.875rem;
32+
color: var(--N400);
33+
flex-shrink: 0;
34+
}
35+
36+
/* ===== INLINE-PREVIEW: favicon + title + domain ===== */
137
.card {
238
display: inline-flex;
339
align-items: center;
@@ -53,3 +89,100 @@
5389
flex-shrink: 0;
5490
margin-left: auto;
5591
}
92+
93+
/* ===== PREVIEW: full block with OG image ===== */
94+
.preview {
95+
display: flex;
96+
width: 100%;
97+
border: 1px solid var(--N100);
98+
border-radius: 10px;
99+
overflow: hidden;
100+
text-decoration: none;
101+
color: inherit;
102+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
103+
margin: 1.5rem 0;
104+
}
105+
106+
.preview:hover {
107+
border-color: var(--N200);
108+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
109+
}
110+
111+
.imageWrapper {
112+
position: relative;
113+
width: 140px;
114+
min-height: 100px;
115+
flex-shrink: 0;
116+
background-color: var(--N50);
117+
}
118+
119+
.image {
120+
object-fit: cover;
121+
}
122+
123+
.previewContent {
124+
display: flex;
125+
flex-direction: column;
126+
gap: 0.375rem;
127+
padding: 1rem;
128+
min-width: 0;
129+
flex: 1;
130+
}
131+
132+
.previewHeader {
133+
display: flex;
134+
align-items: center;
135+
gap: 0.375rem;
136+
}
137+
138+
.previewDomain {
139+
font-size: 0.85rem;
140+
color: var(--N400);
141+
}
142+
143+
.previewTitle {
144+
font-size: 1.1rem;
145+
font-weight: 600;
146+
color: var(--N800);
147+
line-height: 1.3;
148+
display: -webkit-box;
149+
-webkit-line-clamp: 2;
150+
-webkit-box-orient: vertical;
151+
overflow: hidden;
152+
}
153+
154+
.previewDescription {
155+
font-size: 0.95rem;
156+
color: var(--N500);
157+
margin: 0;
158+
line-height: 1.4;
159+
display: -webkit-box;
160+
-webkit-line-clamp: 2;
161+
-webkit-box-orient: vertical;
162+
overflow: hidden;
163+
}
164+
165+
.previewIcon {
166+
width: 1.2rem;
167+
height: 1.2rem;
168+
color: var(--N300);
169+
flex-shrink: 0;
170+
margin: 1rem;
171+
align-self: flex-start;
172+
}
173+
174+
@media screen and (max-width: 480px) {
175+
.preview {
176+
flex-direction: column;
177+
}
178+
179+
.imageWrapper {
180+
width: 100%;
181+
height: 140px;
182+
min-height: auto;
183+
}
184+
185+
.previewIcon {
186+
display: none;
187+
}
188+
}

src/components/LinkCard/LinkCard.tsx

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React from "react";
2+
import Image from "next/image";
23
import clsx from "clsx";
34
import { FiExternalLink } from "react-icons/fi";
45
import styles from "./LinkCard.module.css";
56

7+
type LinkCardType = "inline" | "inline-preview" | "preview";
8+
69
interface ILinkCardProps {
710
href: string;
8-
title: string;
11+
title?: string;
12+
description?: string;
13+
image?: string;
14+
type?: LinkCardType;
915
className?: string;
1016
}
1117

@@ -30,27 +36,88 @@ function getFaviconUrl(url: string): string {
3036
export const LinkCard = function LinkCard({
3137
href,
3238
title,
39+
description,
40+
image,
41+
type = "inline-preview",
3342
className,
3443
}: ILinkCardProps) {
3544
const domain = getDomain(href);
3645
const favicon = getFaviconUrl(href);
3746

47+
// inline: just favicon + domain link
48+
if (type === "inline") {
49+
return (
50+
<a
51+
href={href}
52+
target="_blank"
53+
rel="noopener noreferrer"
54+
className={clsx(styles.inline, className)}
55+
>
56+
{favicon && (
57+
// eslint-disable-next-line @next/next/no-img-element
58+
<img src={favicon} alt="" className={styles.faviconSmall} loading="lazy" />
59+
)}
60+
<span className={styles.inlineDomain}>{domain}</span>
61+
<FiExternalLink className={styles.iconSmall} />
62+
</a>
63+
);
64+
}
65+
66+
// inline-preview: favicon + title + domain (current default)
67+
if (type === "inline-preview") {
68+
return (
69+
<a
70+
href={href}
71+
target="_blank"
72+
rel="noopener noreferrer"
73+
className={clsx(styles.card, className)}
74+
>
75+
{favicon && (
76+
// eslint-disable-next-line @next/next/no-img-element
77+
<img src={favicon} alt="" className={styles.favicon} loading="lazy" />
78+
)}
79+
<div className={styles.content}>
80+
<span className={styles.title}>{title || domain}</span>
81+
<span className={styles.domain}>{domain}</span>
82+
</div>
83+
<FiExternalLink className={styles.icon} />
84+
</a>
85+
);
86+
}
87+
88+
// preview: full block with OG image, title, description
3889
return (
3990
<a
4091
href={href}
4192
target="_blank"
4293
rel="noopener noreferrer"
43-
className={clsx(styles.card, className)}
94+
className={clsx(styles.preview, className)}
4495
>
45-
{favicon && (
46-
// eslint-disable-next-line @next/next/no-img-element
47-
<img src={favicon} alt="" className={styles.favicon} loading="lazy" />
96+
{image && (
97+
<div className={styles.imageWrapper}>
98+
<Image
99+
src={image}
100+
alt={title || domain}
101+
fill
102+
sizes="(max-width: 768px) 100vw, 300px"
103+
className={styles.image}
104+
/>
105+
</div>
48106
)}
49-
<div className={styles.content}>
50-
<span className={styles.title}>{title}</span>
51-
<span className={styles.domain}>{domain}</span>
107+
<div className={styles.previewContent}>
108+
<div className={styles.previewHeader}>
109+
{favicon && (
110+
// eslint-disable-next-line @next/next/no-img-element
111+
<img src={favicon} alt="" className={styles.faviconSmall} loading="lazy" />
112+
)}
113+
<span className={styles.previewDomain}>{domain}</span>
114+
</div>
115+
<span className={styles.previewTitle}>{title || domain}</span>
116+
{description && (
117+
<p className={styles.previewDescription}>{description}</p>
118+
)}
52119
</div>
53-
<FiExternalLink className={styles.icon} />
120+
<FiExternalLink className={styles.previewIcon} />
54121
</a>
55122
);
56123
};
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
.counter {
2+
--size: 1.5rem;
23
display: inline-flex;
34
align-items: center;
4-
gap: 0.375rem;
5+
gap: calc(var(--size) / 3);
56
color: var(--N300);
6-
font-size: 1.2rem;
7+
font-size: var(--size);
78
font-weight: 500;
89
}
910

1011
.icon {
11-
width: 1.2rem;
12-
height: 1.2rem;
12+
width: var(--size);
13+
height: var(--size);
1314
flex-shrink: 0;
1415
}

0 commit comments

Comments
 (0)