Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
6 changes: 0 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ DEFAULT_GITHUB_RETRY_DELAY=1000
# Maximum number of retry attempts for GitHub API calls (e.g., 3)
DEFAULT_GITHUB_MAX_RETRY=3

# URL for retrieving Devicon assets (ensure it ends with a slash)
DEVICON_URL='https://raw.githubusercontent.com/devicons/devicon/master/icons/'

# URL to fetch GitHub Linguist language definitions
LINGUIST_GITHUB='https://raw.githubusercontent.com/github/linguist/main/lib/linguist/languages.yml'

# Base URL for language mappings file location
LANGUAGE_MAPPINGS_URL='https://raw.githubusercontent.com/teociaps/github-bubble-chart/main/'

Expand Down
5 changes: 5 additions & 0 deletions .github/config/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ configuration:
- .prettierrc*
- .editorconfig
- .stylelintrc*

theme:
- changed-files:
- any-glob-to-any-file:
- src/chart/themes.ts
23 changes: 6 additions & 17 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,10 @@ Before submitting your PR, confirm the following:
- [ ] Commit messages follow the [contribution guidelines](https://github.com/teociaps/github-bubble-chart/blob/main/CONTRIBUTING.md).
- [ ] Tests are added or updated to cover all changes.
- [ ] Documentation is updated (e.g., README, inline comments, external docs).
- [ ] CI checks pass successfully.

---

## **Summary**

Provide a concise summary of this PR, including its purpose, the problem it solves, and the key changes made.

---
_Provide a concise summary of this PR, including its purpose, the problem it solves, and the key changes made._

## **Change Type**

Expand All @@ -28,22 +23,16 @@ Select the type(s) of change included in this PR:
- [ ] Maintenance / Chores
- [ ] Breaking change

If this PR introduces a breaking change, describe the impact and required migration steps:

---
If this PR introduces a **breaking change**, describe the impact and required migration steps:

## **Linked Issues**

List any related issues or feature requests (e.g., Fixes #123).

---

## **Screenshots/Logs (Optional)**
_List any related issues or feature requests (e.g., Fixes #123)._

Include relevant screenshots, logs, or other visuals, if applicable.
## **Screenshots/Logs**

---
_Include relevant screenshots, logs, or other visuals, if applicable._

## **Additional Notes**

Provide any additional information or feedback requests for reviewers.
_Provide any additional information or feedback requests for reviewers._
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false
"useTabs": false,
"endOfLine": "auto"
}
2 changes: 0 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ We are thrilled that you are considering contributing to this project! Your cont
5. [Style Guide](#style-guide)
6. [Need Help?](#need-help)

---

## Getting Started

To get started, follow these steps:
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@ Insert a bubble chart into your GitHub profile readme. This makes for a great [G
You can insert the bubble chart into your GitHub profile readme by using the following URL format:

```
https://github-bubble-chart.vercel.app?username=<your-github-username>
https://github-bubble-chart.vercel.app?username=YOUR_GITHUB_USERNAME
```

For detailed usage instructions, please refer to our [Wiki](https://github.com/teociaps/github-bubble-chart/wiki).
For detailed usage instructions, please refer to the [Wiki](https://github.com/teociaps/github-bubble-chart/wiki).

### Examples

#### Basic Example

![teociaps](https://github-bubble-chart.vercel.app?username=teociaps&theme=dark_dimmed&title-size=34&title-color=red&legend-align=left)

```
https://github-bubble-chart.vercel.app?username=teociaps&theme=dark_dimmed&title-size=34&title-color=red&legend-align=left
```

#### Custom Configuration Example

![Custom](https://github-bubble-chart.vercel.app/?username=teociaps&mode=custom-config&config-path=ghbc-my-tech-and-tools.json)
![Custom](https://github-bubble-chart.vercel.app?username=teociaps&mode=custom-config&config-path=ghbc-my-tech-and-tools.json)

```
https://github-bubble-chart.vercel.app?username=teociap&mode=custom-config&config-path=ghbc-my-tech-and-tools.json
https://github-bubble-chart.vercel.app?username=teociaps&mode=custom-config&config-path=ghbc-my-tech-and-tools.json
```

## Breaking Changes & Releases
Expand Down
41 changes: 34 additions & 7 deletions api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
DisplayMode,
} from '../src/chart/types/chartOptions.js';
import { CustomConfig, Mode } from '../src/chart/types/config.js';
import { isDevEnvironment } from '../src/common/environment.js';
import {
isDevEnvironment,
mapConfigToBubbleChartOptions,
convertImageToBase64,
} from '../src/common/utils.js';
import { BaseError } from '../src/errors/base-error.js';
import {
Expand Down Expand Up @@ -162,15 +163,40 @@ export async function fetchConfigFromRepo(
filePath: string,
branch?: string,
): Promise<{ options: BubbleChartOptions; data: BubbleData[] }> {
const processConfig = (
const processConfig = async (
customConfig: CustomConfig,
): { options: BubbleChartOptions; data: BubbleData[] } => {
const options = mapConfigToBubbleChartOptions(customConfig.options);
): Promise<{ options: BubbleChartOptions; data: BubbleData[] }> => {
// First filter invalid data items
customConfig.data = customConfig.data.filter(
(dataItem) =>
typeof dataItem.value === 'number' && !isNaN(dataItem.value),
);
return { options: options, data: customConfig.data };

// Process icons in data items if they exist
for (const dataItem of customConfig.data) {
// Check if item has an icon property that's a URL string and not already base64
if (
dataItem.icon &&
typeof dataItem.icon === 'string' &&
!dataItem.icon.startsWith('data:')
) {
try {
const base64Icon = await convertImageToBase64(dataItem.icon);
if (base64Icon) {
dataItem.icon = base64Icon;
}
} catch (error) {
logger.warn(
`Failed to convert icon to base64: ${dataItem.icon}`,
error,
);
// Continue with original URL if conversion fails
}
}
}

const options = mapConfigToBubbleChartOptions(customConfig.options);
return { options, data: customConfig.data };
};

if (isDevEnvironment()) {
Expand All @@ -182,7 +208,7 @@ export async function fetchConfigFromRepo(
const customConfig = JSON.parse(
fs.readFileSync(localPath, 'utf-8'),
) as CustomConfig;
return processConfig(customConfig);
return await processConfig(customConfig);
} catch (error) {
throw new ValidationError(
'Failed to parse local JSON configuration.',
Expand Down Expand Up @@ -219,7 +245,7 @@ export async function fetchConfigFromRepo(

try {
const customConfig = (await response.json()) as CustomConfig;
return processConfig(customConfig);
return await processConfig(customConfig);
} catch (error) {
throw new ValidationError(
'Failed to parse fetched JSON configuration.',
Expand All @@ -240,3 +266,4 @@ export function handleErrorResponse(
res.status(500).send({ error: 'An unexpected error occurred' });
}
}
export { convertImageToBase64 };
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ comment:
require_changes: true
layout: 'condensed_header, condensed_files, condensed_footer'
hide_project_coverage: true
require_bundle_changes: 'bundle_increase'
bundle_change_threshold: '1Mb'
6 changes: 4 additions & 2 deletions config/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ const CONSTANTS = {
DEFAULT_GITHUB_MAX_RETRY: parseInt(process.env.DEFAULT_GITHUB_MAX_RETRY!, 10),
REVALIDATE_TIME: HOUR_IN_MILLISECONDS,
LANGS_OUTPUT_FILE: 'src/languageMappings.json',
DEVICON_URL: process.env.DEVICON_URL!,
LINGUIST_GITHUB: process.env.LINGUIST_GITHUB!,
DEVICON_URL:
'https://raw.githubusercontent.com/devicons/devicon/master/icons/',
LINGUIST_GITHUB:
'https://raw.githubusercontent.com/github/linguist/main/lib/linguist/languages.yml',
LANGUAGE_MAPPINGS_URL: process.env.LANGUAGE_MAPPINGS_URL!,
DEFAULT_THEME: new DefaultTheme(),
};
Expand Down
11 changes: 1 addition & 10 deletions scripts/fetchLanguageMappings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable no-console */
import fs from 'fs';
import imageToBase64 from 'image-to-base64';
import { parse as yamlParse } from 'yaml';
import { CONSTANTS } from '../config/consts';
import { convertImageToBase64 } from '../src/common/utils';

// Known language name discrepancies map (GitHub vs Devicon)
const languageDiscrepancies: Record<string, string> = {
Expand Down Expand Up @@ -45,15 +45,6 @@ async function fetchLanguageColors(): Promise<
}
}

async function convertImageToBase64(url: string): Promise<string | undefined> {
try {
const base64 = await imageToBase64(url);
return `data:image/svg+xml;base64,${base64}`;
} catch (error) {
console.error('Error converting image:', error);
}
}

const svgVersions = [
'-original',
'-plain',
Expand Down
15 changes: 15 additions & 0 deletions src/common/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Checks if the current environment is development
* @returns true if NODE_ENV is 'dev'
*/
export const isDevEnvironment = (): boolean => {
return process.env.NODE_ENV === 'dev';
};

/**
* Checks if the current environment is production
* @returns true if NODE_ENV is 'prod'
*/
export const isProdEnvironment = (): boolean => {
return process.env.NODE_ENV === 'prod';
};
73 changes: 65 additions & 8 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import imageToBase64 from 'image-to-base64';
import { themeMap } from '../chart/themes.js';
import { BubbleChartOptions } from '../chart/types/chartOptions.js';
import { CustomConfigOptions } from '../chart/types/config.js';

export const isDevEnvironment = (): boolean => {
return process.env.NODE_ENV === 'dev';
};

export const isProdEnvironment = (): boolean => {
return process.env.NODE_ENV === 'prod';
};
import logger from '../logger.js';

export function mapConfigToBubbleChartOptions(
config: CustomConfigOptions,
Expand Down Expand Up @@ -52,3 +46,66 @@ export function getPxValue(value: string): number {
}
return 0;
}

export async function convertImageToBase64(
url: string,
options?: { timeout?: number },
): Promise<string | undefined> {
if (!url || typeof url !== 'string') {
logger.error('Invalid URL provided to convertImageToBase64');
return undefined;
}

try {
new URL(url);
} catch {
logger.error('Invalid URL format', { url });
return undefined;
}

try {
const timeoutMs = options?.timeout || 10000; // 10 second timeout

const base64 = await Promise.race([
imageToBase64(url),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error('Image conversion timed out')),
timeoutMs,
),
),
]);

// MIME type detection
const mimeTypeMap: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
bmp: 'image/bmp',
svg: 'image/svg+xml',
ico: 'image/x-icon',
tiff: 'image/tiff',
tif: 'image/tiff',
avif: 'image/avif',
};

// Extract extension from URL, handling query params and fragments
const urlPath = new URL(url).pathname;
const extension = urlPath.split('.').pop()?.toLowerCase().split(/[?#]/)[0];

// Default to a generic image type if we can't determine the specific type
const mimeType =
extension && mimeTypeMap[extension]
? mimeTypeMap[extension]
: 'image/png';

return `data:${mimeType};base64,${base64}`;
} catch (_error) {
const errorMessage =
_error instanceof Error ? _error.message : 'Unknown error';
logger.error(`Error converting image to base64: ${errorMessage}`, _error);
return undefined;
}
}
2 changes: 1 addition & 1 deletion src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pino } from 'pino';
import { isDevEnvironment } from './common/utils.js';
import { isDevEnvironment } from './common/environment.js';

const isDev = isDevEnvironment();

Expand Down
Loading