diff --git a/README.md b/README.md index ab1e933..96dfd20 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,20 @@ Generate a top languages chart for your GitHub profile that you can embed in a R ### Customization Options Append these query parameters to the URL to customize the look and data of your chart: -| Parameter | Type | Description | Default | Example | -| :--- | :--- | :--- | :--- | :--- | -| `theme` | String | Sets the colour scheme. Available options: `default`, `light`, `dark`. | `default` | `?theme=dark` | -| `title` | String | Sets a custom title for the chart. | `Top Languages` | `?title=My%20Code%20Stack` | -| `hide_title` | Boolean | Hides the chart title completely. | `false` | `?hide_title=true` | -| `count` | Number | Sets the maximum number of languages to display. Max is **16**. | `8` | `?count=10` | -| `width` | Number | Sets the width of the SVG in pixels. | `400` | `?width=500` | -| `height` | Number | Sets the height of the SVG in pixels. | `300` | `?height=350` | -| `bg` | String | Sets the chart background colour. Accepts hex (`ffffff`) or theme names (`dark`, `light`). | `default` | `?bg=dark` | -| `text` | String | Sets the chart text colour. Accepts hex (`ffffff`). | `#000000` | `?text=ffffff` | -| `c1`-`c16` | String | Sets individual colours for languages 1-16. Accepts hex codes. | Auto-assigned | `?c1=ff0000&c2=00ff00` | -| `test` | Boolean | Uses samples data instead of fetching from GitHub API. | `false` | `?test=true` | -| `stroke` | Boolean | Adds an outline stroke to chart segments. | `false` | `?stroke=true` | +| Parameter | Type | Description | Default | Example | +| :--- | :--- | :--- | :--- | :--- | +| `theme` | String | Sets the colour scheme. Available options: `default`, `light`, `dark`. | `default` | `?theme=dark` | +| `type` | String | Sets the chart type. Available options: `donut`, `pie`. | `donut` | `?type=pie` | +| `title` | String | Sets a custom title for the chart. | `Top Languages` | `?title=My%20Code%20Stack` | +| `hide_title` | Boolean | Hides the chart title completely. | `false` | `?hide_title=true` | +| `text` | String | Sets the chart text colour. Accepts hex (`ffffff`). | `#000000` | `?text=ffffff` | +| `bg` | String | Sets the chart background colour. Accepts hex (`ffffff`) or theme names (`dark`). | `default` | `?bg=dark` | +| `c1`-`c16` | String | Sets individual colours for languages 1-16. Accepts hex codes. | Auto-assigned | `?c1=ff0000&c2=00ff00` | +| `count` | Number | Sets the maximum number of languages to display. Max is **16**. | `8` | `?count=10` | +| `width` | Number | Sets the width of the SVG in pixels. | `400` | `?width=500` | +| `height` | Number | Sets the height of the SVG in pixels. | `300` | `?height=350` | +| `stroke` | Boolean | Adds an outline stroke to chart segments. | `false` | `?stroke=true` | +| `test` | Boolean | Uses samples data instead of fetching from GitHub API. | `false` | `?test=true` | #### Example URL To get 10 languages, a dark theme, and a custom title: @@ -77,9 +78,6 @@ To get 10 languages, a dark theme, and a custom title: ![My Custom Chart](https://your-deployment-url.vercel.app/api/languages?count=10&theme=dark&title=My%20Top%2010%20Languages) ``` -
- - ## Deployment & Configuration ### Prerequisites @@ -96,8 +94,9 @@ npm install ### Configuration Copy `.env.example` to `.env`, and update the variables. -- `GITHUB_USERNAME`: User to fetch repositories and statistics. -- `IGNORED_REPOS`: Optional repos you don't want counted in your chart. +- `GITHUB_USERNAMES`: Comma-separated GitHub usernames to fetch repositories from. +- `GITHUB_ORGS`: Optional comma-separated GitHub organization names to include. +- `IGNORED_REPOS`: Optional comma-separated repo names to exclude from the chart. ### Running Locally ```bash @@ -106,12 +105,10 @@ vercel dev ``` ### Deployment -- **Vercel**: Simply import your repo and deploy. The default endpoint is /api/languages - -
- +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/masonlet/github-top-languages&env=GITHUB_USERNAMES,IGNORED_REPOS&envDescription[GITHUB_USERNAMES]=Comma-separated%20GitHub%20usernames&envDescription[IGNORED_REPOS]=Optional%20comma-separated%20repos%20to%20exclude) +> The default endpoint is /api/languages ## License MIT License - see [LICENSE](./LICENSE) for details. diff --git a/api/languages/index.ts b/api/languages/index.ts index a62eaf0..97dcb56 100644 --- a/api/languages/index.ts +++ b/api/languages/index.ts @@ -1,6 +1,6 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; import { parseQueryParams, type QueryParams } from "../../src/utils/params.js"; -import { fetchLanguageData, processLanguageData } from "../../src/api/github.js"; +import { fetchLanguageData, processLanguageData } from "../../src/services/github.js"; import { generateChartData } from "../../src/render/chart.js"; import { renderSvg } from "../../src/render/svg.js"; import { renderError } from "../../src/render/error.js"; diff --git a/src/render/error.ts b/src/render/error.ts index 54f969a..55a3459 100644 --- a/src/render/error.ts +++ b/src/render/error.ts @@ -1,6 +1,7 @@ import type { Theme } from "../types.js"; import { THEMES } from "../constants/themes.js"; import { ERROR_STYLES } from "../constants/styles.js" +import { sanitize } from "../utils/sanitize.js"; export function renderError( message: string, @@ -8,13 +9,13 @@ export function renderError( height: number, selectedTheme?: Theme ): string { - const background = selectedTheme?.bg || THEMES.default.bg; + const background = selectedTheme?.bg || THEMES.default.bg; return ` - Error: ${message} + Error: ${sanitize(message)} - `; + `.trim(); } diff --git a/src/render/svg.ts b/src/render/svg.ts index 8f30107..9a78ad8 100644 --- a/src/render/svg.ts +++ b/src/render/svg.ts @@ -27,5 +27,5 @@ export function renderSvg( ${segments} ${legend} - `; + `.trim(); } diff --git a/src/api/github.ts b/src/services/github.ts similarity index 100% rename from src/api/github.ts rename to src/services/github.ts diff --git a/src/utils/params.ts b/src/utils/params.ts index e9be187..c3bb486 100644 --- a/src/utils/params.ts +++ b/src/utils/params.ts @@ -14,14 +14,16 @@ const parseIntSafe = ( return Number.isNaN(parsed) ? fallback : parsed; } +const normalizeHex = (val: string) => `#${val.replace(/^#/, '')}`; + export function parseQueryParams(query: QueryParams) { const baseTheme = THEMES[query["theme"] as keyof typeof THEMES] ?? THEMES.default; const count = parseIntSafe(query["count"], DEFAULT_CONFIG.COUNT); - const customColours = [...baseTheme.colours] as string[]; + const customColours: string[] = [...baseTheme.colours]; for (let i = 1; i <= MAX_COUNT; i++) { const colourVal = query[`c${i}`]; - if(colourVal) customColours[i - 1] = `#${colourVal.replace(/^#/, '')}` as string; + if(colourVal) customColours[i - 1] = normalizeHex(colourVal); } const typeParam = query["type"] as ChartType | undefined; @@ -34,8 +36,8 @@ export function parseQueryParams(query: QueryParams) { height: parseIntSafe(query["height"], DEFAULT_CONFIG.HEIGHT), count: Math.min(Math.max(count, 1), MAX_COUNT), selectedTheme: { - bg: THEMES[query["bg"] as keyof typeof THEMES]?.bg ?? query["bg"] ?? baseTheme.bg, - text: query["text"] ?? baseTheme.text, + bg: THEMES[query["bg"] as keyof typeof THEMES]?.bg ?? (query["bg"] ? normalizeHex(query["bg"]) : baseTheme.bg), + text: query["text"] ? normalizeHex(query["text"]) : baseTheme.text, colours: customColours }, stroke: query["stroke"] === "true", diff --git a/tests/api/languages/index.test.ts b/tests/api/languages/index.test.ts index 83e9ce4..2ecc208 100644 --- a/tests/api/languages/index.test.ts +++ b/tests/api/languages/index.test.ts @@ -3,13 +3,13 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; import type { ChartResult } from "../../../src/types.js"; import handler from "../../../api/languages/index.js"; import { parseQueryParams } from "../../../src/utils/params.js"; -import { fetchLanguageData, processLanguageData } from "../../../src/api/github.js"; +import { fetchLanguageData, processLanguageData } from "../../../src/services/github.js"; import { generateChartData } from "../../../src/render/chart.js"; import { renderSvg } from "../../../src/render/svg.js"; import { renderError } from "../../../src/render/error.js"; vi.mock("../../../src/utils/params.js"); -vi.mock("../../../src/api/github.js"); +vi.mock("../../../src/services/github.js"); vi.mock("../../../src/render/chart.js"); vi.mock("../../../src/render/svg.js"); vi.mock("../../../src/render/error.js"); diff --git a/tests/api/github.test.ts b/tests/services/github.test.ts similarity index 99% rename from tests/api/github.test.ts rename to tests/services/github.test.ts index c2a8613..8f49e16 100644 --- a/tests/api/github.test.ts +++ b/tests/services/github.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { fetchLanguageData, processLanguageData, resetCache } from "../../src/api/github.js"; +import { fetchLanguageData, processLanguageData, resetCache } from "../../src/services/github.js"; const repos = [ { name: "repo1", fork: false, full_name: "user/repo1" },