/ server / api / public / [...badge].get.ts
[...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  });