Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 44 additions & 34 deletions packages/app/app/components/BadgeGenerator.vue
Original file line number Diff line number Diff line change
@@ -1,58 +1,68 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { ref, computed, onMounted } from "vue";

const props = defineProps<{
owner: string;
repo: string;
color?: string;
}>();

const copied = ref(false);
const style = "flat";
const color = props.color || "000";
const isLoading = ref(true);
const imgEl = ref<HTMLImageElement | null>(null);

const baseUrl = ref("https://pkg.pr.new");
onMounted(() => {
baseUrl.value = window.location.origin;
});
const badgeUrl = computed(() => `/badge/${props.owner}/${props.repo}`);

const badgeUrl = computed(
() =>
`${baseUrl.value}/badge/${props.owner}/${props.repo}` +
`?style=${style}` +
`&color=${encodeURIComponent(color)}` +
`&logoSize=auto`,
);

const redirectUrl = computed(
() => `${baseUrl.value}/~/${props.owner}/${props.repo}`,
);
const redirectUrl = computed(() => `/~/${props.owner}/${props.repo}`);

function copyBadgeCode() {
const md = `[![pkg.pr.new](${badgeUrl.value})](${redirectUrl.value})`;
navigator.clipboard.writeText(md);
copied.value = true;
setTimeout(() => (copied.value = false), 2000);
}

onMounted(() => {
if (imgEl.value && imgEl.value.complete && imgEl.value.naturalWidth > 0) {
isLoading.value = false;
}
});
</script>

<template>
<div class="inline-flex items-center gap-[2px]">
<a :href="redirectUrl" target="_blank" rel="noopener">
<img
:src="badgeUrl"
:alt="`pkg.pr.new badge`"
class="h-5 w-auto block max-w-none"
<div class="inline-flex items-center">
<a :href="redirectUrl" class="flex" target="_blank" rel="noopener">
<div class="relative inline-block">
<div
v-if="isLoading"
class="h-5 w-[120px] rounded bg-gray-200 dark:bg-gray-700 animate-pulse"
/>
<img
ref="imgEl"
:src="badgeUrl"
:alt="`pkg.pr.new badge`"
@load="isLoading = false"
@error="isLoading = false"
:class="[
'h-5 w-auto block max-w-none transition-opacity',
isLoading ? 'hidden' : 'block',
]"
/>
</div>
</a>
<div
v-if="isLoading"
class="h-5 w-5 rounded bg-gray-200 dark:bg-gray-700 animate-pulse ml-1"
/>
</a>

<UButton
@click="copyBadgeCode"
size="xs"
color="gray"
:icon="copied ? 'i-ph-check-bold' : 'i-ph-copy'"
variant="ghost"
class="!p-1 cursor-pointer"
/>
<UButton
v-else
@click="copyBadgeCode"
size="xs"
color="neutral"
:icon="copied ? 'i-ph-check-bold' : 'i-ph-copy'"
variant="ghost"
class="!p-1 cursor-pointer ml-1"
/>
</div>
</div>
</template>
5 changes: 5 additions & 0 deletions packages/app/server/api/repo/index.get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { H3Event } from "h3";
import { z } from "zod";
import { getRepoReleaseCount } from "../../utils/bucket";

Check warning on line 3 in packages/app/server/api/repo/index.get.ts

View workflow job for this annotation

GitHub Actions / Run Linting

'getRepoReleaseCount' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 3 in packages/app/server/api/repo/index.get.ts

View workflow job for this annotation

GitHub Actions / Run Linting

'getRepoReleaseCount' is defined but never used

const querySchema = z.object({
owner: z.string(),
Expand All @@ -16,6 +17,8 @@
repo,
});

const releaseCount = 0;

return {
id: data.id.toString(),
name: data.name,
Expand All @@ -27,9 +30,10 @@
url: data.html_url,
homepageUrl: data.homepage || "",
description: data.description || "",
releaseCount,
};
} catch (error) {
console.error(

Check warning on line 36 in packages/app/server/api/repo/index.get.ts

View workflow job for this annotation

GitHub Actions / Run Linting

Unexpected console statement
`Error fetching repository info for ${owner}/${repo}:`,
error,
);
Expand Down Expand Up @@ -65,6 +69,7 @@
url: "",
homepageUrl: "",
description: "Error fetching repository data",
releaseCount: 0,
};
}
});
51 changes: 10 additions & 41 deletions packages/app/server/routes/badge/[owner]/[repo].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
createError,
getQuery,
} from "h3";
import { getRepoReleaseCount } from "../../../utils/bucket";
import { LOGO_BASE64 } from "../../../../shared/constants";

export default defineEventHandler(async (event) => {
const { owner, repo } = getRouterParams(event) as {
Expand All @@ -18,56 +20,23 @@ export default defineEventHandler(async (event) => {
});
}

const { style = "flat", color = "000" } = getQuery(event) as Record<
string,
string
>;
const logoBase64 = getPkgPrNewLogoBase64();
const releaseCount = await getRepoReleaseCount(event, owner, repo);

const style = "flat";
const color = "000";

const shieldsUrl =
`https://img.shields.io/static/v1?` +
`label=&message=${encodeURIComponent("pkg.pr.new")}` +
`label=&message=${encodeURIComponent(`${releaseCount} | pkg.pr.new`)}` +
`&color=${color}` +
`&style=${style}` +
`&logo=data:image/svg+xml;base64,${logoBase64}` +
`&logo=data:image/svg+xml;base64,${LOGO_BASE64}` +
`&logoSize=auto`;

const res = await fetch(shieldsUrl);
const svg = await res.text();

setHeader(event, "Content-Type", "image/svg+xml");
setHeader(event, "Cache-Control", "public, max-age=86400");
setHeader(event, "Cache-Control", "public, max-age=86400, immutable");
return svg;
});

function getPkgPrNewLogoBase64(): string {
const logo = `<svg xmlns="http://www.w3.org/2000/svg" width="71" height="73" viewBox="0 0 71 73" fill="none">
<g transform="translate(0,7)">
<g filter="url(#filter0_di_18_22)">
<path d="M29.6539 2.52761C26.8827 3.91317 24.6248 5.06781 24.6248 5.10629C24.6248 5.14478 31.2447 8.57019 39.3399 12.7269C47.448 16.8707 54.1706 20.3731 54.2988 20.4886C54.5169 20.681 54.5298 21.1942 54.5298 28.9046C54.5298 36.6149 54.5169 37.1281 54.2988 37.3847C54.1706 37.5386 52.7208 38.3597 51.0915 39.2193C49.4494 40.0788 47.9612 40.7716 47.7816 40.7716C47.5891 40.7716 47.3839 40.6562 47.2684 40.4894C47.1016 40.2328 47.076 39.1166 47.076 32.2402L47.0632 24.286L16.9914 8.80112C5.66316 14.446 4.62399 15.0105 4.35458 15.3825L4.03385 15.8187C3.98253 41.7723 3.99536 49.4314 4.03385 49.5981C4.07233 49.7521 4.25194 50.06 4.45721 50.2781C4.7138 50.586 8.54974 52.5617 19.7882 58.1681C31.0266 63.7873 34.8754 65.6604 35.2089 65.6604C35.5425 65.6604 38.1725 64.4159 45.3184 60.8879C50.6169 58.2579 57.5062 54.8453 60.6108 53.2801C65.9863 50.586 66.2685 50.432 66.538 49.9445L66.833 49.4314C66.833 17.1273 66.8202 16.0496 66.6021 15.6263C66.3968 15.2029 65.4988 14.7282 51.1813 7.58234C36.928 0.474933 35.9402 -0.0125789 35.3372 0.000250395C34.7727 0.0130796 34.0928 0.320982 29.6539 2.52761Z" fill="url(#paint0_linear_18_22)"/>
</g>
<defs>
<filter id="filter0_di_18_22" x="0.460111" y="1.52588e-05" width="69.9128" height="72.7401" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3.53989"/>
<feGaussianBlur stdDeviation="1.76994"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_18_22"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_18_22" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.76994"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.95 0"/>
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_18_22"/>
</filter>
<linearGradient id="paint0_linear_18_22" x1="35.4165" y1="1.52588e-05" x2="35.4165" y2="66.8238" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#999999"/>
</linearGradient>
</defs>
</g>
</svg>`;

return Buffer.from(logo).toString("base64");
}
37 changes: 37 additions & 0 deletions packages/app/server/utils/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,40 @@ export function useDebugBucket(event: Event) {

useDebugBucket.key = "debug";
useDebugBucket.base = joinKeys(useBucket.base, useDebugBucket.key);

export async function getRepoReleaseCount(
event: Event,
owner: string,
repo: string,
): Promise<number> {
try {
const binding = useBinding(event);
const prefix = `${usePackagesBucket.base}:${owner}:${repo}:`;

const uniqueCommitShas = new Set<string>();
let cursor: string | undefined;

do {
const response = await binding.list({
cursor,
limit: 1000,
prefix,
} as any);

for (const { key } of response.objects) {
if (!key.startsWith(prefix)) continue;

const trimmedKey = key.slice(prefix.length);
const [sha] = trimmedKey.split(":");
if (sha) uniqueCommitShas.add(sha);
}

cursor = response.truncated ? response.cursor : undefined;
} while (cursor);

return uniqueCommitShas.size;
} catch (error) {
console.error(`Error counting releases for ${owner}/${repo}:`, error);
return 0;
}
}
36 changes: 36 additions & 0 deletions packages/app/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="71" height="73" viewBox="0 0 71 73" fill="none">
<g transform="translate(0,7)">
<g filter="url(#filter0_di_18_22)">
<path d="M29.6539 2.52761C26.8827 3.91317 24.6248 5.06781 24.6248 5.10629C24.6248 5.14478 31.2447 8.57019 39.3399 12.7269C47.448 16.8707 54.1706 20.3731 54.2988 20.4886C54.5169 20.681 54.5298 21.1942 54.5298 28.9046C54.5298 36.6149 54.5169 37.1281 54.2988 37.3847C54.1706 37.5386 52.7208 38.3597 51.0915 39.2193C49.4494 40.0788 47.9612 40.7716 47.7816 40.7716C47.5891 40.7716 47.3839 40.6562 47.2684 40.4894C47.1016 40.2328 47.076 39.1166 47.076 32.2402L47.0632 24.286L16.9914 8.80112C5.66316 14.446 4.62399 15.0105 4.35458 15.3825L4.03385 15.8187C3.98253 41.7723 3.99536 49.4314 4.03385 49.5981C4.07233 49.7521 4.25194 50.06 4.45721 50.2781C4.7138 50.586 8.54974 52.5617 19.7882 58.1681C31.0266 63.7873 34.8754 65.6604 35.2089 65.6604C35.5425 65.6604 38.1725 64.4159 45.3184 60.8879C50.6169 58.2579 57.5062 54.8453 60.6108 53.2801C65.9863 50.586 66.2685 50.432 66.538 49.9445L66.833 49.4314C66.833 17.1273 66.8202 16.0496 66.6021 15.6263C66.3968 15.2029 65.4988 14.7282 51.1813 7.58234C36.928 0.474933 35.9402 -0.0125789 35.3372 0.000250395C34.7727 0.0130796 34.0928 0.320982 29.6539 2.52761Z" fill="url(#paint0_linear_18_22)"/>
</g>
<defs>
<filter id="filter0_di_18_22" x="0.460111" y="1.52588e-05" width="69.9128" height="72.7401" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3.53989"/>
<feGaussianBlur stdDeviation="1.76994"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_18_22"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_18_22" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.76994"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.95 0"/>
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_18_22"/>
</filter>
<linearGradient id="paint0_linear_18_22" x1="35.4165" y1="1.52588e-05" x2="35.4165" y2="66.8238" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#999999"/>
</linearGradient>
</defs>
</g>
</svg>`;

function svgToBase64(svgString: string): string {
if (typeof Buffer !== "undefined") {
return Buffer.from(svgString, "utf-8").toString("base64");
}
return btoa(unescape(encodeURIComponent(svgString)));
}
export const LOGO_BASE64 = svgToBase64(LOGO_SVG);
Loading