[...badge].get.ts
1 import { TimeRangeEnum, type TimeRange, type Summary } from "~~/lib/stats"; 2 import { badgen } from "badgen"; 3 import { calculateStats } from "~~/server/utils/stats"; 4 5 interface StatsResult { 6 summaries: Summary[]; 7 offsetSeconds: number; 8 debug: Record<string, any>; 9 } 10 11 interface StatsWithProject extends StatsResult { 12 projectSeconds: number; 13 projectFilter: string; 14 } 15 16 export default defineEventHandler(async (event) => { 17 const url = new URL( 18 event.node.req.url || "", 19 `http://${event.node.req.headers.host || "localhost"}` 20 ); 21 const pathname = url.pathname; 22 const segments = pathname.split("/").filter(Boolean); 23 const badgeIdx = segments.indexOf("badge"); 24 const pathParams = segments.slice(badgeIdx + 1); 25 26 // /api/public/badge/:userId/:project/:timeRange/:color/:labelText 27 const userId = pathParams[0]; 28 const projectInput = pathParams[1] || "all"; 29 const timeRangeParam = (pathParams[2] as TimeRange) || TimeRangeEnum.ALL_TIME; 30 let color = pathParams[3] || "blue"; 31 const labelText = pathParams[4]; 32 33 const project = projectInput.toLowerCase(); 34 35 if (!userId) { 36 throw createError({ 37 statusCode: 400, 38 statusMessage: "User ID is required", 39 }); 40 } 41 42 if ( 43 timeRangeParam && 44 !Object.values(TimeRangeEnum).includes(timeRangeParam as any) 45 ) { 46 throw createError({ 47 statusCode: 400, 48 statusMessage: `Invalid time range. Valid options are: ${Object.values(TimeRangeEnum).join(", ")}`, 49 }); 50 } 51 52 const query = Object.fromEntries(url.searchParams.entries()); 53 const style = query.style ? String(query.style) : "classic"; 54 const icon = query.icon as string; 55 56 const stats = (await calculateStats( 57 userId, 58 timeRangeParam, 59 undefined, 60 project 61 )) as StatsResult | StatsWithProject; 62 63 let totalSeconds = 0; 64 65 if ("projectFilter" in stats) { 66 totalSeconds = stats.projectSeconds; 67 } else { 68 if (project === "all") { 69 totalSeconds = stats.summaries.reduce( 70 (total: number, summary: Summary) => total + summary.totalSeconds, 71 0 72 ); 73 } else { 74 for (const summary of stats.summaries) { 75 if (summary.projects) { 76 const projectsData = summary.projects as Record<string, number>; 77 for (const [projectName, seconds] of Object.entries(projectsData)) { 78 if (projectName.toLowerCase() === project) { 79 totalSeconds += seconds; 80 } 81 } 82 } 83 } 84 } 85 } 86 87 let formattedTime; 88 if (totalSeconds === 0) { 89 formattedTime = "0 mins"; 90 } else if (totalSeconds < 60) { 91 formattedTime = `${totalSeconds} secs`; 92 } else if (totalSeconds < 3600) { 93 formattedTime = `${Math.round(totalSeconds / 60)} mins`; 94 } else { 95 const hours = Math.floor(totalSeconds / 3600); 96 const minutes = Math.round((totalSeconds % 3600) / 60); 97 98 if (minutes === 0) { 99 formattedTime = `${hours} hrs`; 100 } else { 101 formattedTime = `${hours} hrs ${minutes} mins`; 102 } 103 } 104 105 const colorMap: Record<string, string> = { 106 blue: "007ec6", 107 green: "97ca00", 108 red: "e05d44", 109 orange: "fe7d37", 110 yellow: "dfb317", 111 purple: "9f9f9f", 112 black: "333333", 113 }; 114 115 const badgeOptions: any = { 116 label: labelText || "Ziit", 117 status: formattedTime, 118 color: colorMap[color as keyof typeof colorMap] || color, 119 style: style === "flat" ? "flat" : "classic", 120 }; 121 122 if (icon) { 123 badgeOptions.icon = icon; 124 } else if (!labelText) { 125 badgeOptions.icon = "/favicon.ico"; 126 } 127 128 const svg = badgen(badgeOptions); 129 130 event.node.res.setHeader("Content-Type", "image/svg+xml"); 131 event.node.res.setHeader("Cache-Control", "max-age=600, s-maxage=600"); 132 event.node.res.setHeader("Access-Control-Allow-Origin", "*"); 133 event.node.res.setHeader("Access-Control-Allow-Methods", "GET"); 134 135 return svg; 136 });