Skip to content

Commit 4a59674

Browse files
committed
fix
1 parent bfbc534 commit 4a59674

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
const COPY_FEEDBACK_DURATION_MS = 2000;
6+
const BADGE_BASE_URL = "https://www.react.doctor/share/badge";
7+
const SHARE_BASE_URL = "https://www.react.doctor/share";
8+
9+
interface BadgeSnippetProps {
10+
searchParamsString: string;
11+
}
12+
13+
const BadgeSnippet = ({ searchParamsString }: BadgeSnippetProps) => {
14+
const [didCopy, setDidCopy] = useState(false);
15+
16+
const badgeFullUrl = `${BADGE_BASE_URL}?${searchParamsString}`;
17+
const shareFullUrl = `${SHARE_BASE_URL}?${searchParamsString}`;
18+
const badgePreviewPath = `/share/badge?${searchParamsString}`;
19+
const markdownSnippet = `[![React Doctor](${badgeFullUrl})](${shareFullUrl})`;
20+
21+
const handleCopy = async () => {
22+
await navigator.clipboard.writeText(markdownSnippet);
23+
setDidCopy(true);
24+
setTimeout(() => setDidCopy(false), COPY_FEEDBACK_DURATION_MS);
25+
};
26+
27+
return (
28+
<div className="mt-8">
29+
<div className="text-neutral-500">Add a badge to your README:</div>
30+
<div className="mt-3 flex flex-wrap items-center gap-3">
31+
<img src={badgePreviewPath} alt="React Doctor score badge" height={20} className="block" />
32+
<a
33+
href={badgePreviewPath}
34+
target="_blank"
35+
rel="noreferrer"
36+
className="text-xs text-neutral-500 underline underline-offset-2 transition-colors hover:text-neutral-300"
37+
>
38+
Open SVG
39+
</a>
40+
</div>
41+
42+
<div className="mt-3 flex flex-wrap items-start gap-2">
43+
<code className="min-w-0 flex-1 break-all border border-white/20 px-3 py-1.5 text-xs text-neutral-300">
44+
{markdownSnippet}
45+
</code>
46+
<button
47+
type="button"
48+
onClick={handleCopy}
49+
className="shrink-0 border border-white/20 px-3 py-1.5 text-xs text-neutral-300 transition-all hover:bg-white/10 active:scale-[0.98]"
50+
>
51+
{didCopy ? "Copied" : "Copy"}
52+
</button>
53+
</div>
54+
</div>
55+
);
56+
};
57+
58+
export default BadgeSnippet;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const PERFECT_SCORE = 100;
2+
const SCORE_GOOD_THRESHOLD = 75;
3+
const SCORE_OK_THRESHOLD = 50;
4+
5+
const BADGE_HEIGHT_PX = 20;
6+
const LABEL_TEXT = "react doctor";
7+
const LABEL_RECT_WIDTH_PX = 97;
8+
const LABEL_TEXT_CENTER_10X = 575;
9+
const LABEL_TEXT_LENGTH_10X = 670;
10+
const SECTION_PADDING_PX = 11;
11+
const DIGIT_WIDTH_10X = 65;
12+
const SLASH_WIDTH_10X = 38;
13+
const FONT_SIZE_10X = 110;
14+
const TEXT_Y_10X = 140;
15+
const SHADOW_Y_10X = 150;
16+
const CACHE_MAX_AGE_SECONDS = 86400;
17+
18+
const LOGO_SIZE_PX = 14;
19+
const LOGO_X_PX = 6;
20+
const LOGO_Y_PX = 3;
21+
22+
const WHITE_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><mask id="a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><path d="M39.2 0H0V39.2H39.2V0Z" fill="#fff"/></mask><g mask="url(#a)"><mask id="b" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><path d="M39.2 0H0V39.2H39.2V0Z" fill="#fff"/></mask><g mask="url(#b)"><mask id="c" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><path d="M39.2 0H0V39.2H39.2V0Z" fill="#fff"/><circle cx="26.9609" cy="23.9609" r="12.9658" fill="#000"/></mask><g mask="url(#c)"><path d="M19.2799 6.33229C22.6283 3.65276 25.9398 2.67017 28.2843 4.02393C30.3796 5.23404 31.3175 8.04321 30.9235 11.9354C30.8903 12.2676 30.8438 12.6056 30.792 12.9474L30.4702 14.6853C30.469 14.6848 30.4674 14.6842 30.466 14.6836C30.4648 14.6886 30.4639 14.6937 30.4624 14.6986L28.834 14.0988C28.8342 14.0981 28.8331 14.097 28.8331 14.0964C27.722 13.75 26.5895 13.4766 25.4427 13.2785L25.4262 13.2745L23.1368 12.9686L23.1348 12.9684C23.1323 12.9648 23.129 12.9623 23.1263 12.9587C21.8483 12.8275 20.5644 12.7622 19.2799 12.7629C17.9924 12.7621 16.706 12.8292 15.4258 12.9638C14.6767 14.0044 13.9812 15.0824 13.3418 16.1937C12.6991 17.3064 12.115 18.4521 11.5919 19.6257C12.115 20.7994 12.6991 21.945 13.3418 23.0577C13.9822 24.1736 14.6799 25.2556 15.4322 26.2993C15.4332 26.2993 15.4344 26.2994 15.4355 26.2996L15.4336 26.3026L15.4327 26.3044L15.4339 26.3061L16.8668 28.135L16.8766 28.1452C17.6182 29.0356 18.4168 29.8766 19.2676 30.6632L19.2812 30.6776L20.6096 31.7716C20.6052 31.7758 20.6008 31.78 20.5963 31.784C20.598 31.7856 20.5999 31.7869 20.6018 31.7882L19.1795 32.9976L19.177 32.9996C16.8747 34.817 14.5963 35.8326 12.6403 35.8326C11.8123 35.8449 10.9959 35.6362 10.2755 35.2277C8.17999 34.0176 7.24223 31.2082 7.63613 27.3161C7.67044 26.9742 7.71745 26.6258 7.77209 26.2738C3.77821 24.7102 1.26978 22.3346 1.26978 19.6257C1.26978 17.2056 3.23305 14.9861 6.79872 13.3833C7.11261 13.2421 7.43921 13.1074 7.77209 12.9803C7.71745 12.6269 7.67044 12.2773 7.63613 11.9354C7.24223 8.04321 8.17999 5.23404 10.2755 4.02393C12.62 2.67017 15.9315 3.65276 19.2799 6.33229ZM9.41901 26.842C9.38977 27.0606 9.36309 27.2754 9.34276 27.489C9.02252 30.6089 9.67308 32.8837 11.1149 33.7317L11.1357 33.7416C12.672 34.6289 15.202 33.9234 17.971 31.784C16.7064 30.5933 15.5459 29.2966 14.5019 27.9085C12.7803 27.6989 11.0797 27.3421 9.41901 26.842ZM10.7138 21.7917C10.312 22.8902 9.98225 24.0138 9.72649 25.1552C10.8239 25.4965 11.9421 25.7662 13.0743 25.9632L13.1283 25.975C12.6949 25.3153 12.2693 24.6213 11.8575 23.917C11.4458 23.2129 11.0671 22.5024 10.7138 21.7917ZM8.10124 14.6937C7.89791 14.7785 7.69841 14.8633 7.50271 14.948C4.63721 16.2408 2.98525 17.9454 2.98525 19.6257C2.98525 21.399 4.86213 23.2396 8.09996 24.568C8.49923 22.8774 9.04169 21.2242 9.72143 19.6257C9.04297 18.0306 8.50092 16.3806 8.10124 14.6937ZM13.1219 13.2854C11.9744 13.486 10.841 13.7608 9.72905 14.1078C9.98057 15.227 10.3033 16.329 10.6951 17.4072L10.7075 17.4585C11.0659 16.7466 11.4407 16.045 11.8512 15.3344C12.2616 14.6238 12.6873 13.9425 13.1219 13.2854ZM12.6568 5.13617C12.1245 5.12376 11.5983 5.25271 11.1319 5.50988C9.68392 6.34684 9.02637 8.61269 9.33903 11.7259L9.33892 11.7626C9.35924 11.9761 9.38592 12.191 9.41516 12.4083C11.0765 11.9115 12.7769 11.556 14.4982 11.3456C15.5427 9.95703 16.704 8.6604 17.9698 7.46996C15.9836 5.93447 14.1194 5.13617 12.6568 5.13617ZM27.4316 5.50861C26.9691 5.25304 26.4474 5.12408 25.9192 5.13463L25.9054 5.13491C24.4428 5.13491 22.5787 5.93319 20.5926 7.46868C21.8574 8.65829 23.0179 9.95407 24.0616 11.3418C25.7834 11.5512 27.4838 11.908 29.1446 12.4083C29.175 12.191 29.2004 11.9749 29.222 11.7613C29.5398 8.62796 28.8867 6.34883 27.4316 5.50861ZM19.2736 8.5746C18.4139 9.36816 17.6072 10.2174 16.8591 11.1168C17.652 11.066 18.4568 11.0406 19.2736 11.0406C20.097 11.0406 20.9038 11.0685 21.6943 11.1168C20.944 10.2172 20.1354 9.36792 19.2736 8.5746Z" fill="#fff"/></g></g></g><g clip-path="url(#d)"><path d="M26.9609 33.9219C32.459 33.9219 36.9219 29.459 36.9219 23.9609C36.9219 18.4629 32.459 14 26.9609 14C21.4629 14 17 18.4629 17 23.9609C17 29.459 21.4629 33.9219 26.9609 33.9219ZM26.9609 32.2617C22.3711 32.2617 18.6602 28.5508 18.6602 23.9609C18.6602 19.3711 22.3711 15.6602 26.9609 15.6602C31.5508 15.6602 35.2617 19.3711 35.2617 23.9609C35.2617 28.5508 31.5508 32.2617 26.9609 32.2617Z" fill="#fff"/><path d="M21.5605 24.9863C21.5605 25.582 21.9707 25.9824 22.5566 25.9824H24.9102V28.3262C24.9102 28.9414 25.3105 29.332 25.9062 29.332H27.9766C28.582 29.332 28.9727 28.9414 28.9727 28.3262V25.9824H31.3262C31.9316 25.9824 32.332 25.582 32.332 24.9863V22.9063C32.332 22.3203 31.9316 21.9102 31.3262 21.9102H28.9727V19.5762C28.9727 18.9707 28.582 18.5703 27.9766 18.5703H25.9062C25.3105 18.5703 24.9102 18.9707 24.9102 19.5762V21.9102H22.5566C21.9609 21.9102 21.5605 22.3203 21.5605 22.9063V24.9863Z" fill="#fff"/></g><defs><clipPath id="d"><rect x="17" y="14" width="20.2832" height="19.9316" rx="9.9658" fill="#fff"/></clipPath></defs></svg>`;
23+
24+
const LOGO_DATA_URI = `data:image/svg+xml,${encodeURIComponent(WHITE_LOGO_SVG)}`;
25+
26+
const getBadgeScoreColor = (score: number): string => {
27+
if (score >= SCORE_GOOD_THRESHOLD) return "#2ea043";
28+
if (score >= SCORE_OK_THRESHOLD) return "#d29922";
29+
return "#cf222e";
30+
};
31+
32+
const computeScoreTextLength = (scoreText: string): number =>
33+
scoreText.split("").reduce((totalWidth, character) => {
34+
if (character === "/") return totalWidth + SLASH_WIDTH_10X;
35+
return totalWidth + DIGIT_WIDTH_10X;
36+
}, 0);
37+
38+
export const GET = (request: Request): Response => {
39+
const { searchParams } = new URL(request.url);
40+
const score = Math.max(0, Math.min(PERFECT_SCORE, Number(searchParams.get("s")) || 0));
41+
42+
const scoreText = `${score}/${PERFECT_SCORE}`;
43+
const scoreColor = getBadgeScoreColor(score);
44+
const scoreTextLength = computeScoreTextLength(scoreText);
45+
const valueRectWidth = Math.round(scoreTextLength / 10) + SECTION_PADDING_PX * 2;
46+
const totalWidth = LABEL_RECT_WIDTH_PX + valueRectWidth;
47+
48+
const valueCenterX = (LABEL_RECT_WIDTH_PX + valueRectWidth / 2) * 10;
49+
50+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${totalWidth}" height="${BADGE_HEIGHT_PX}" role="img" aria-label="${LABEL_TEXT}: ${scoreText}">
51+
<title>${LABEL_TEXT}: ${scoreText}</title>
52+
<linearGradient id="s" x2="0" y2="100%">
53+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
54+
<stop offset="1" stop-opacity=".1"/>
55+
</linearGradient>
56+
<clipPath id="r">
57+
<rect width="${totalWidth}" height="${BADGE_HEIGHT_PX}" rx="3" fill="#fff"/>
58+
</clipPath>
59+
<g clip-path="url(#r)">
60+
<rect width="${LABEL_RECT_WIDTH_PX}" height="${BADGE_HEIGHT_PX}" fill="#555"/>
61+
<rect x="${LABEL_RECT_WIDTH_PX}" width="${valueRectWidth}" height="${BADGE_HEIGHT_PX}" fill="${scoreColor}"/>
62+
<rect width="${totalWidth}" height="${BADGE_HEIGHT_PX}" fill="url(#s)"/>
63+
</g>
64+
<image x="${LOGO_X_PX}" y="${LOGO_Y_PX}" width="${LOGO_SIZE_PX}" height="${LOGO_SIZE_PX}" href="${LOGO_DATA_URI}"/>
65+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="${FONT_SIZE_10X}">
66+
<text aria-hidden="true" x="${LABEL_TEXT_CENTER_10X}" y="${SHADOW_Y_10X}" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${LABEL_TEXT_LENGTH_10X}">${LABEL_TEXT}</text>
67+
<text x="${LABEL_TEXT_CENTER_10X}" y="${TEXT_Y_10X}" transform="scale(.1)" fill="#fff" textLength="${LABEL_TEXT_LENGTH_10X}">${LABEL_TEXT}</text>
68+
<text aria-hidden="true" x="${valueCenterX}" y="${SHADOW_Y_10X}" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${scoreTextLength}">${scoreText}</text>
69+
<text x="${valueCenterX}" y="${TEXT_Y_10X}" transform="scale(.1)" fill="#fff" textLength="${scoreTextLength}">${scoreText}</text>
70+
</g>
71+
</svg>`;
72+
73+
return new Response(svg, {
74+
headers: {
75+
"Content-Type": "image/svg+xml",
76+
"Cache-Control": `public, max-age=${CACHE_MAX_AGE_SECONDS}, s-maxage=${CACHE_MAX_AGE_SECONDS}`,
77+
},
78+
});
79+
};

packages/website/src/app/share/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from "next";
22
import AnimatedScore from "./animated-score";
3+
import BadgeSnippet from "./badge-snippet";
34

45
const PERFECT_SCORE = 100;
56
const SCORE_GOOD_THRESHOLD = 75;
@@ -179,6 +180,8 @@ const SharePage = async ({ searchParams }: { searchParams: Promise<ShareSearchPa
179180
Share on LinkedIn
180181
</a>
181182
</div>
183+
184+
<BadgeSnippet searchParamsString={shareSearchParams.toString()} />
182185
</div>
183186
);
184187
};

0 commit comments

Comments
 (0)