Skip to content

Commit 01d660c

Browse files
authored
article: unpacking ton drainers
2 parents 97e74e2 + 52d048a commit 01d660c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2052
-83
lines changed

app/(articles)/ArticlePreview.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import Link from "next/link";
66
import React, { useContext, useState } from "react";
77
import { BiLinkExternal, BiSolidLock } from "react-icons/bi";
88
import { IoEyeOffOutline } from "react-icons/io5";
9+
import { PiFlag,
10+
PiMagnifyingGlass,
11+
PiBriefcase
12+
} from "react-icons/pi";
913

1014
import Context from "@/app/components/Context";
1115

@@ -18,6 +22,7 @@ export type PreviewProps = {
1822
author: string;
1923
date: string;
2024
hidden: boolean;
25+
section: string;
2126
};
2227

2328
function MaybeLink({
@@ -41,7 +46,7 @@ export default function ArticlePreview(props: PreviewProps) {
4146

4247
return (
4348
<div
44-
className={`article-preview ${props.hidden && "article-hidden"} transition-transform duration-300 hover:scale-105 ${props.hidden ? "hover:cursor-not-allowed" : "hover:cursor-pointer"}`}
49+
className={`article-preview ${props.hidden && "article-hidden"} transition-all duration-300 hover:scale-105 ${props.hidden ? "hover:cursor-not-allowed" : "hover:cursor-pointer md:grayscale-20 md:hover:grayscale-0"}`}
4550
onClick={(e) => {
4651
if (!props.hidden) {
4752
return;
@@ -62,7 +67,7 @@ export default function ArticlePreview(props: PreviewProps) {
6267
backgroundColor: "oklch(from var(--element) l c h / 0.2)",
6368
opacity: locking ? 1 : undefined,
6469
}}
65-
className={`article-preview-card relative aspect-16/10 h-auto w-full rounded-xl px-6 py-4 transition select-none ${props.hidden ? "opacity-85 grayscale-32" : ""}`}
70+
className={`article-preview-card relative aspect-16/10 h-auto w-full rounded-xl px-6 py-4 transition select-none ${props.hidden && "opacity-85 grayscale-32"}`}
6671
>
6772
<Picture
6873
className="absolute inset-0 z-0 h-full w-full rounded-xl object-cover"
@@ -82,8 +87,14 @@ export default function ArticlePreview(props: PreviewProps) {
8287
className={`mt-4 mb-2 flex justify-between transition-all duration-300 ${props.hidden && "break-words opacity-35"}`}
8388
style={{ WebkitTextStrokeWidth: "0.01em" }}
8489
>
90+
{props.section &&
91+
<span className="inline text-xl mt-1 p-0">{{"ctf": <PiFlag />,
92+
"research": <PiMagnifyingGlass />,
93+
"audit": <PiBriefcase />
94+
}[props.section]}</span>
95+
}
8596
<span
86-
className={`article-preview-title mx-3 font-theme-serif text-lg font-semibold lg:text-xl ${classShaking}`}
97+
className={`article-preview-title pl-1 mx-3 font-theme-serif text-lg font-semibold lg:text-xl ${classShaking}`}
8798
onAnimationEnd={() =>
8899
props.hidden && !canHover && setShaking(false)
89100
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
@reference "../../../globals.css";
2+
3+
@media (prefers-color-scheme: dark) {
4+
.markdown-alert {
5+
--color-note-text: #58a6ff;
6+
--color-note-border: #1f6feb;
7+
--color-tip-text: #3fb950;
8+
--color-tip-border: #238636;
9+
--color-important-text: #a371f7;
10+
--color-important-border: #8957e5;
11+
--color-warning-text: #d29922;
12+
--color-warning-border: #9e6a03;
13+
--color-caution-text: #f85149;
14+
--color-caution-border: #da3633;
15+
}
16+
}
17+
18+
@media (prefers-color-scheme: light) {
19+
.markdown-alert {
20+
--color-note-text: #0969da;
21+
--color-note-border: #0969da;
22+
--color-tip-text: #1a7f37;
23+
--color-tip-border: #1f883d;
24+
--color-important-text: #8250df;
25+
--color-important-border: #8250df;
26+
--color-warning-text: #9a6700;
27+
--color-warning-border: #9a6700;
28+
--color-caution-text: #d1242f;
29+
--color-caution-border: #cf222e;
30+
}
31+
}
32+
33+
.markdown-alert .markdown-alert-title {
34+
align-items: center;
35+
display: flex;
36+
font-weight: 500;
37+
line-height: 1;
38+
}
39+
40+
.markdown-alert .markdown-alert-title svg.octicon {
41+
margin-right: 8px !important;
42+
margin-right: var(--base-size-8, 8px) !important;
43+
fill: currentColor;
44+
}
45+
46+
.markdown-alert.markdown-alert-note {
47+
border-left-color: var(--color-note-border);
48+
49+
.markdown-alert-title {
50+
color: var(--color-note-text);
51+
}
52+
}
53+
54+
.markdown-alert.markdown-alert-tip {
55+
border-left-color: var(--color-tip-border);
56+
57+
.markdown-alert-title {
58+
color: var(--color-tip-text);
59+
}
60+
}
61+
62+
.markdown-alert.markdown-alert-important {
63+
border-left-color: var(--color-important-border);
64+
65+
.markdown-alert-title {
66+
color: var(--color-important-text);
67+
}
68+
}
69+
70+
.markdown-alert.markdown-alert-warning {
71+
border-left-color: var(--color-warning-border);
72+
73+
.markdown-alert-title {
74+
color: var(--color-warning-text);
75+
}
76+
}
77+
78+
.markdown-alert.markdown-alert-caution {
79+
border-left-color: var(--color-caution-border);
80+
81+
.markdown-alert-title {
82+
color: var(--color-caution-text);
83+
}
84+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { ImageResponse } from "next/og";
2+
import path from "path";
3+
4+
import * as articles from "@/app/(articles)";
5+
6+
import * as fs from "fs/promises";
7+
8+
export const dynamic = "force-static";
9+
10+
// Same size as for twitter card to simplify generation.
11+
const size = {
12+
width: 1200,
13+
height: 675,
14+
};
15+
16+
export async function generateStaticParams(): Promise<articles.Params[]> {
17+
return articles.list();
18+
}
19+
20+
function base64url(data: string, mimeType: string = "image/png") {
21+
return `data:${mimeType};base64,${data}`;
22+
}
23+
24+
export async function GET(
25+
request: Request,
26+
{
27+
params,
28+
}: {
29+
params: Promise<articles.Params>;
30+
},
31+
) {
32+
const { slug } = await params;
33+
const cwd = process.cwd();
34+
35+
const [
36+
articleMetadata,
37+
fontHorizon,
38+
fontRubikRegular,
39+
fontRubikMedium,
40+
ogCover,
41+
neploxLogo,
42+
] = await Promise.all([
43+
articles.loadMetadata(slug),
44+
fs.readFile(path.join(cwd, "app", "fonts", "horizon.otf")),
45+
fs.readFile(path.join(cwd, "app", "fonts", "Rubik", "Rubik-Regular.ttf")),
46+
fs.readFile(path.join(cwd, "app", "fonts", "Rubik", "Rubik-Medium.ttf")),
47+
fs.readFile(
48+
path.join(cwd, "articles", "blog", slug, "og-cover.png"),
49+
"base64",
50+
),
51+
fs.readFile(path.join(cwd, "app", "assets", "neplox.png"), "base64"),
52+
]);
53+
54+
const potentialIconNames = ["og-icon.png", "og-icon.svg"];
55+
let ogIcon: string | undefined = undefined;
56+
for (const iconName of potentialIconNames) {
57+
try {
58+
ogIcon = await fs.readFile(
59+
path.join(cwd, "articles", "blog", slug, iconName),
60+
"base64",
61+
);
62+
break;
63+
} catch (e) {
64+
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
65+
continue;
66+
}
67+
throw e;
68+
}
69+
}
70+
71+
if (!ogIcon) {
72+
throw new Error("No og icon found");
73+
}
74+
75+
const articleAuthor = articles.authors[articleMetadata.author];
76+
77+
return new ImageResponse(
78+
(
79+
<div
80+
style={{
81+
width: "1200px",
82+
height: "675px",
83+
backgroundImage: `url(${base64url(ogCover)})`,
84+
padding: "64px 80px",
85+
display: "flex",
86+
flexDirection: "column",
87+
}}
88+
>
89+
{/* Branding */}
90+
<div style={{ display: "flex" }}>
91+
{/* eslint-disable-next-line @next/next/no-img-element */}
92+
<img
93+
src={base64url(neploxLogo)}
94+
alt="NEPLOX"
95+
height={80}
96+
width={94}
97+
style={{
98+
position: "absolute",
99+
// -(80px size - 48px font size) / 2
100+
// simulate scale transform, where the origin would be aligned with the text, but then the image is scaled up
101+
top: "-16px",
102+
}}
103+
/>
104+
<span
105+
style={{
106+
fontFamily: "Horizon",
107+
fontWeight: 600,
108+
fontSize: "48px",
109+
color: "#ef5900", // theme brand
110+
position: "absolute",
111+
left: "110px", // 94px logo width + 16px padding
112+
}}
113+
>
114+
NEPLOX
115+
</span>
116+
</div>
117+
118+
{/* Related icon */}
119+
<div
120+
style={{
121+
display: "flex",
122+
backgroundColor: "#252125", // theme dark (raisin-900)
123+
// (675 (img height) / 2) - 2 * 48px (icon block top/bottom padding)
124+
width: "241px",
125+
height: "241px",
126+
borderRadius: "24px",
127+
position: "absolute",
128+
top: "48px",
129+
right: "80px",
130+
alignItems: "center",
131+
justifyContent: "center",
132+
}}
133+
>
134+
{/* eslint-disable-next-line @next/next/no-img-element */}
135+
<img
136+
src={base64url(ogIcon, "image/svg+xml")}
137+
alt="Related icon"
138+
style={{
139+
maxHeight: "177px", // 241 - 32 * 2 (padding)
140+
maxWidth: "177px",
141+
}}
142+
/>
143+
</div>
144+
145+
{/* Top half text */}
146+
<div style={{ display: "flex", flexBasis: "50%" }}>
147+
<span
148+
style={{
149+
fontFamily: "RubikRegular",
150+
alignSelf: "flex-end",
151+
fontSize: "48px",
152+
color: "#48434a", // theme raisin-600
153+
marginBottom: "16px",
154+
}}
155+
>
156+
CTF Writeup
157+
</span>
158+
</div>
159+
160+
{/* Bottom half text */}
161+
<div
162+
style={{
163+
display: "flex",
164+
flexDirection: "column",
165+
justifyContent: "space-between",
166+
flexBasis: "50%",
167+
}}
168+
>
169+
<span
170+
style={{
171+
fontFamily: "RubikMedium",
172+
fontSize: "64px",
173+
color: "#252125", // theme dark
174+
}}
175+
>
176+
{articleMetadata.title}
177+
</span>
178+
<div
179+
style={{
180+
display: "flex",
181+
justifyContent: "space-between",
182+
fontFamily: "RubikRegular",
183+
fontSize: "32px",
184+
color: "#48434a", // theme raisin-600
185+
}}
186+
>
187+
<span>
188+
By {articleAuthor.name} ({articleMetadata.author})
189+
</span>
190+
<span>
191+
{new Date(articleMetadata.publishedAt).toLocaleDateString(
192+
"en-US",
193+
{
194+
year: "numeric",
195+
month: "long",
196+
day: "numeric",
197+
},
198+
)}
199+
</span>
200+
</div>
201+
</div>
202+
</div>
203+
),
204+
{
205+
...size,
206+
fonts: [
207+
{ data: fontHorizon, name: "Horizon", style: "normal", weight: 600 },
208+
{
209+
data: fontRubikRegular,
210+
name: "RubikRegular",
211+
style: "normal",
212+
weight: 400,
213+
},
214+
{
215+
data: fontRubikMedium,
216+
name: "RubikMedium",
217+
style: "normal",
218+
weight: 500,
219+
},
220+
],
221+
},
222+
);
223+
}

0 commit comments

Comments
 (0)