diff --git a/README.md b/README.md index b10087a..f038f2a 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ For those interested in creating a similar catalog website or contributing to th The website is styled using the [tailwindcss](https://tailwindcss.com/) package. -* **Real-time Data Fetching:** Displays all public organization repositories, fetched through the GitHub and Hugging Face APIs. Includes semantically meaningful virtual markers: +* **Real-time Data Fetching:** Displays all public organization repositories, fetched through the code platform and Hugging Face APIs. Includes semantically meaningful virtual markers: * "New" badge highlights products created within the last 30 days; - * "🚀 version-tag" badge indicates a new release within the last 2 weeks for GitHub repos, and links to that release; - * Star (⭐️) or like (❤️) counts displayed for GitHub or Hugging Face repos, respectively; - * Archived badge flags GitHub repos no longer under active development (read-only). + * "🚀 version-tag" badge indicates a new release within the last 2 weeks for code repos, and links to that release; + * Star (⭐️) or like (❤️) counts displayed for code platform or Hugging Face repos, respectively; + * Archived badge flags code repos no longer under active development (read-only). * **Search Functionality:** Quickly find items by keyword. -* **Filtering:** Filter by repository type (Code, Datasets, Models, Spaces) and tags. Optionally include archived GitHub repos. +* **Filtering:** Filter by repository type (Code, Datasets, Models, Spaces) and tags. Optionally include archived code repos. * **Sorting:** Sort items by last updated, date created, stars/likes ascending or descending, or alphabetically. * **URL Parameter Support:** Persist and share search states via URL hash (`#type=datasets&q=fish`) or query parameters (`?type=datasets`). Supports `type`, `q` (search query), `sort`, and `tag` parameters. * **Responsive Design:** The layout is optimized for use on computers and mobile devices. @@ -30,7 +30,7 @@ The site runs based on four primary files: * `style.css`: Custom styling for the application, including color schemes, layout, and animations. Colors are set via CSS custom properties that are populated from `config.yaml`. * `main.js`: Application manager, handles config loading, event listeners, and coordinates UI updates based on API fetches for dynamic rendering of the catalog items. * `src/`: Modular application logic, organized by purpose: - * `api/`: Data fetching modules for external platforms (GitHub and Hugging Face). + * `api/`: Data fetching modules for external platforms (code and Hugging Face). * `ui/`: DOM manipulation, HTML templating, and URL/State routing. * `utils/`: Utility functions such as data filtering, sorting, and tag (keyword) normalization. diff --git a/docs/personalization.md b/docs/personalization.md index cc26212..d8a0756 100644 --- a/docs/personalization.md +++ b/docs/personalization.md @@ -4,7 +4,7 @@ Welcome to your new catalog repo! The primary way to personalize this catalog is ## Primary Configuration File -[**`public/config.yaml`**](../public/config.yaml): This is the main file to edit. It contains all configuration options (e.g., organization names, colors, branding, and API settings) with inline comments explaining each setting. Replace Imageomics-specific values with those appropriate for your organzation and catalog repository. +[**`public/config.yaml`**](../public/config.yaml): This is the main file to edit. It contains all configuration options (e.g., organization names, colors, branding, and API settings) with inline comments explaining each setting. Replace Imageomics-specific values with those appropriate for your organization and catalog repository. ### Organization & Repository Settings @@ -32,7 +32,7 @@ Welcome to your new catalog repo! The primary way to personalize this catalog is ### API & Behavior Settings - * `PLATFORM`: Coding platform used (default: 'github', Codeberg and GitLab support in development) + * `PLATFORM`: Coding platform used: 'github' or 'codeberg' (default: 'github', GitLab support in development). Please see [notes on platform setup](#non-github-code-platform-setup) if using for non-GitHub code repositories * `API_BASE_URL`: Hugging Face API base URL (default: `"https://huggingface.co/api/"`) * `REFRESH_INTERVAL_DAYS`: Number of days to consider an item "new" (default: `30`) * `ADDITIONAL_REPOS`: Array of forked or non-org GitHub repositories to include, formatted `/` (non-forks are included by default). Use `[]` if there are none you wish to include @@ -88,3 +88,7 @@ When first setting up your catalog, run the export script to generate a full lis > **Required token**: The weekly tag scan workflow requires a fine-grained access token with **Pull requests: Read and write** permission on the catalog repo. Follow the instructions in [App Authentication](app-authentication.md) to create and install a private Catalog Automation App for token generation. See **[tag-grouping-process.md](tag-grouping-process.md)** for full setup instructions, conventions, and guidance on using AI assistance for the initial grouping pass. + +## Code Repository Platform Setup + +The default code repository platform for this catalog is GitHub. If you wish to use another supported platform (Codeberg), please note that the [tag export](../scripts/export-tags.js) and [fetch release](../scripts/fetch-releases.js) scripts may require header definition modifications to function properly. Workflows would also require token and other platform-specific updates if running from a non-GitHub repository. Otherwise, this app is set up to be able to run from Codeberg to fetch and display Codeberg repositories. diff --git a/docs/tag-grouping-process.md b/docs/tag-grouping-process.md index e73b013..452a227 100644 --- a/docs/tag-grouping-process.md +++ b/docs/tag-grouping-process.md @@ -2,11 +2,16 @@ ## What Are Tag Groups? -Tags on GitHub repos (topics) and Hugging Face repos (card metadata) are free-form text, so +Tags on code platform[^1] repos (topics) and Hugging Face repos (card metadata) are free-form text, so the same concept often appears under multiple spellings: `computer-vision`, `computer vision`, `cv`. Tag groups normalize this noise so the catalog filter dropdown shows one clean canonical tag instead of a dozen near-duplicates. +> [!NOTE] +> This workflow has only been run on GitHub. It may need platform-based adjustments to function as expected on other code platforms, such as Codeberg or GitLab. + +[^1]: GitHub, Codeberg, and GitLab. + Tag groups live in **`public/tag-groups.js`** as a plain JavaScript object: ```js diff --git a/index.html b/index.html index 7e96d4a..367c169 100644 --- a/index.html +++ b/index.html @@ -28,9 +28,8 @@ class="fixed top-6 right-6 hidden md:flex items-center gap-3 text-white py-2 px-4 rounded-lg shadow-md transition-all z-50 no-underline group font-sans border border-white/10">
- +
diff --git a/public/config.yaml b/public/config.yaml index 06b960d..7514f69 100644 --- a/public/config.yaml +++ b/public/config.yaml @@ -22,7 +22,7 @@ COLORS: tag: "#9bcb5e" # Tag background color (Light Green) # API & Behavior Settings -# Codebase platform for API calls and link generation. Supported values: "github", pending: "codeberg" or "gitlab". +# Codebase platform for API calls and link generation. Supported values: "github" or "codeberg", pending: "gitlab". PLATFORM: github # Dataset, model, and space (demo) default: Hugging Face, other platforms would require a custom module API_BASE_URL: "https://huggingface.co/api/" diff --git a/src/api/fetchCodeRepos.js b/src/api/fetchCodeRepos.js index a427a2b..a0f87ea 100644 --- a/src/api/fetchCodeRepos.js +++ b/src/api/fetchCodeRepos.js @@ -2,6 +2,23 @@ import { handleError } from '../ui/render.js'; import { normalizeTag, filterDisplayTags } from '../utils/normalizeTag.js'; import { getPlatformDisplay } from '../utils/defineRibbonVals.js'; +// Define platform-specific values +const PLATFORM_CONFIGS = { + github: { + starsKey: 'stargazers_count', + profileRepo: '.github' + }, + /* gitlab: { + starsKey: 'star_count', + profileRepo: 'gitlab-profile', + //TODO + }, */ + codeberg: { + starsKey: 'stars_count', + profileRepo: '.profile' + } +}; + /** * Function for fetching code repositories from the specified platform (GitHub, GitLab, or Codeberg). * Both org-owned (non-forks) and additional repos are fetched; metadata is processed for each repo. @@ -28,6 +45,9 @@ export async function fetchCodeRepos( let allRepos = []; let nextUrl = `${orgApiUrl}`; + // get platform-specific keys + const platformConfig = PLATFORM_CONFIGS[platform.toLowerCase()]; + const { starsKey, profileRepo } = platformConfig; try { while (nextUrl) { const ghResponse = await fetch(nextUrl); @@ -72,7 +92,10 @@ export async function fetchCodeRepos( // Keep only non-forks from org; deduplicate against additional repos by full_name const orgRepoNames = new Set(filteredAdditionalRepos.map(r => r.full_name)); - const orgNonForks = allRepos.filter(repo => repo.name !== ".github" && !repo.fork && !orgRepoNames.has(repo.full_name)); + const orgNonForks = allRepos.filter(repo => + repo.name !== profileRepo && + !repo.fork && + !orgRepoNames.has(repo.full_name)); // Process additional repos and all remaining org non-forks to include metadata and 'new' flag as appropriate let processedItems = [...filteredAdditionalRepos, ...orgNonForks] @@ -105,7 +128,7 @@ export async function fetchCodeRepos( cardData: { pretty_name: repo.name, // , the one used for card title display description: repo.description, - stars: repo.stargazers_count || 0 + stars: repo[starsKey] ?? 0 } }; }); diff --git a/src/api/fetchStats.js b/src/api/fetchStats.js index fc0c0bd..76f6888 100644 --- a/src/api/fetchStats.js +++ b/src/api/fetchStats.js @@ -24,13 +24,13 @@ export const fetchCatalogStats = async (repoApiUrl, organizationName, catalogRep //TODO: Update stars and forks to support other platforms (GitLab, Codeberg) once implemented // 1. Get Stars & Forks const repo = await fetch(`${repoApiUrl}${organizationName}/${catalogRepoName}`).then(r => r.ok ? r.json() : {}); - if (repo.stargazers_count !== undefined) update('gh-stars', 'gh-star-container', repo.stargazers_count); + if (repo.stargazers_count !== undefined || repo.stars_count !== undefined) update('gh-stars', 'gh-star-container', repo.stargazers_count || repo.stars_count); if (repo.forks_count !== undefined) update('gh-forks', 'gh-fork-container', repo.forks_count); // 2. Get Version (Tag) // TODO: Import from package.json const release = await fetch(`${repoApiUrl}${organizationName}/${catalogRepoName}/releases/latest`).then(r => r.ok ? r.json() : {}); - if (release.tag_name) update('gh-tag', 'gh-version-container', release.tag_name); + if (release.tag_name !== undefined) update('gh-tag', 'gh-version-container', release.tag_name); } catch (e) { console.warn("Could not fetch Code Repo stats", e); diff --git a/src/assets/GitHub_Invertocat.svg b/src/assets/GitHub_Invertocat.svg new file mode 100755 index 0000000..5300969 --- /dev/null +++ b/src/assets/GitHub_Invertocat.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/codeberg-logo_icon_white.svg b/src/assets/codeberg-logo_icon_white.svg new file mode 100644 index 0000000..74a0fa9 --- /dev/null +++ b/src/assets/codeberg-logo_icon_white.svg @@ -0,0 +1,164 @@ + + + Codeberg logo + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Codeberg logo + + + + Robert Martinez + + + + + Codeberg and the Codeberg Logo are trademarks of Codeberg e.V. + + + 2020-04-09 + + + Codeberg e.V. + + + codeberg.org + + + + + + + + + + + + + diff --git a/src/ui/initUserInterface.js b/src/ui/initUserInterface.js index 8dd1691..c8c250d 100644 --- a/src/ui/initUserInterface.js +++ b/src/ui/initUserInterface.js @@ -55,8 +55,8 @@ export const initializeUIFromConfig = (config) => { const platformDisplay = getPlatformDisplay(config.PLATFORM); if (repoRibbon && platformDisplay) { repoRibbon.href = `${platformDisplay.ribbonUrl}${config.ORGANIZATION_NAME}/${config.CATALOG_REPO_NAME}`; - const pathElement = document.getElementById('repo-ribbon-icon'); - pathElement.setAttribute('d', platformDisplay.path); + const svgElement = document.getElementById('repo-ribbon-icon-container'); + svgElement.innerHTML = platformDisplay.svg; const platformDisplayName = document.getElementById('platform-display-name'); platformDisplayName.textContent = platformDisplay.displayName || config.PLATFORM; repoRibbon.style.backgroundColor = config.COLORS.secondary; diff --git a/src/utils/defineApiUrls.js b/src/utils/defineApiUrls.js index 5b20e43..2d68025 100644 --- a/src/utils/defineApiUrls.js +++ b/src/utils/defineApiUrls.js @@ -1,17 +1,17 @@ /** - * Defines API URLs based on the selected platform (GitHub, note: GitLab and Codeberg support under development). + * Defines API URLs based on the selected platform (GitHub or Codeberg, note: GitLab support under development). * This allows the rest of the codebase to use these constants when making API calls, * abstracting away platform-specific URL structures. - * + * * Usage: import { getPlatformApiUrls } from './defineApiUrls.js'; - * + * * Input: platform and organizationName (e.g., 'github' and 'imageomics'), defined from config.yaml and passed to this function. * Output: platformApiUrls[platform] = { org: ORG_API_URL, repo: REPO_API_URL } */ /** * Utility function to get the platform-specific API URLs for organization repos and individual repo details. - * @param {string} platform - 'github', pending: 'gitlab', or 'codeberg' + * @param {string} platform - 'github' or 'codeberg', pending: 'gitlab' * @param {string} organizationName - The name of the organization (used in URL construction) * @returns {object} An object containing ORG_API_URL and REPO_API_URL */ @@ -25,10 +25,10 @@ export function getPlatformApiUrls(platform, organizationName) { // org: `https://gitlab.com/api/v4/groups/${organizationName}/projects?per_page=100`, // repo: "https://gitlab.com/api/v4/projects/" // }, - // codeberg: { - // org: `https://codeberg.org/api/v1/orgs/${organizationName}/repos?limit=50`, - // repo: "https://codeberg.org/api/v1/repos/" - // } + codeberg: { + org: `https://codeberg.org/api/v1/orgs/${organizationName}/repos?limit=50`, + repo: "https://codeberg.org/api/v1/repos/" + } }; return platformApiUrls[platform.toLowerCase()]; } diff --git a/src/utils/defineRibbonVals.js b/src/utils/defineRibbonVals.js index 9ab64a0..7a4486d 100644 --- a/src/utils/defineRibbonVals.js +++ b/src/utils/defineRibbonVals.js @@ -1,16 +1,19 @@ /** - * A collection of platform-specific display information (e.g., SVG path data) - * for code developer platforms including GitHub, pending: GitLab and Codeberg. + * A collection of platform-specific display information (e.g., SVGs and URLs) + * for code developer platforms including GitHub and Codeberg, pending: GitLab. * Used for linking to repo and rendering icon and platform name in the ribbon component of the UI. - * All paths are optimized for a 0 0 24 24 viewBox. * * Usage: import { getPlatformDisplay } from './defineRibbonVals.js'; * * Input: platform (e.g., 'github'), defined from config.yaml and passed to this function. - * Output: platformDisplays[platform] = { path: SVG_PATH_DATA, viewBox: VIEWBOX_DATA, + * Output: platformDisplays[platform] = { svg: CODE_PLATFORM_SVG, * displayName: DISPLAY_NAME, ribbonUrl: RIBBON_URL } */ +import githubSvg from '../assets/GitHub_Invertocat.svg?raw'; +// import gitlabSvg from '../assets/gitlab-logo-700-rgb.svg?raw'; +import codebergSvg from '../assets/codeberg-logo_icon_white.svg?raw'; + /** * Utility function to get the full platform display information * @param {string} platform - 'github', 'gitlab', or 'codeberg' @@ -19,25 +22,20 @@ export function getPlatformDisplay(platform) { const platformDisplays = { github: { - path: "M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z", - viewBox: "0 0 24 24", + svg: githubSvg, displayName: "GitHub", ribbonUrl: "https://github.com/" }, // gitlab: { - // path: "m23.71 11.83-2.13-6.52a.89.89 0 0 0-1.7 0l-2.13 6.52H6.25L4.12 5.31a.89.89 0 0 0-1.7 0L.29 11.83a1 1 0 0 0 .37 1.13l11.34 8.24 11.34-8.24a1 1 0 0 0 .37-1.13Z", - // viewBox: "0 0 24 24", - // color: "#FC6D26", + // svg: gitlabSvg, // displayName: "GitLab", // ribbonUrl: "https://gitlab.com/" //}, - // codeberg: { - // path: "M12.004 2.378a.636.636 0 0 0-.547.31L1.258 19.874a.636.636 0 0 0 .547.954h1.94c.143 0 .278-.074.354-.197L12 6.544l7.89 14.087a.406.406 0 0 0 .354.197h2.012a.636.636 0 0 0 .546-.954L12.551 2.688a.636.636 0 0 0-.547-.31Z", - // viewBox: "0 0 24 24", - // color: "#2185d0", - // displayName: "Codeberg", - // ribbonUrl: "https://codeberg.org/" - //} + codeberg: { + svg: codebergSvg, + displayName: "Codeberg", + ribbonUrl: "https://codeberg.org/" + } }; return platformDisplays[platform.toLowerCase()]; } diff --git a/src/validateConfig.js b/src/validateConfig.js index d903afe..78264d8 100644 --- a/src/validateConfig.js +++ b/src/validateConfig.js @@ -31,11 +31,11 @@ export function validateConfig(config) { } if (!config.ORG_NAME) errors.push('ORG_NAME'); if (!config.CATALOG_REPO_NAME) errors.push('CATALOG_REPO_NAME'); - - /** Update to include 'codeberg' and 'gitlab' once supported + + /** Update to include 'gitlab' once supported * PLATFORM will be parsed without whitespace, but must be string to avoid undefined errors */ - const supportedPlatforms = ['github']; + const supportedPlatforms = ['github', 'codeberg']; const platform = config.PLATFORM; if (!platform || typeof platform !== 'string') { errors.push('PLATFORM'); diff --git a/tests/api/fetchCodeRepos.test.js b/tests/api/fetchCodeRepos.test.js index 98c594b..55e815e 100644 --- a/tests/api/fetchCodeRepos.test.js +++ b/tests/api/fetchCodeRepos.test.js @@ -14,7 +14,32 @@ vi.mock('../../src/utils/defineRibbonVals.js', () => ({ getPlatformDisplay: vi.fn(() => 'GitHub') })); -describe('fetchCodeRepos', () => { +// define configs for each platform +const platformConfigs = [ + { + name: 'github', + orgApiUrl: 'https://api.github.com/orgs/test-org/repos', + repoApiUrl: 'https://api.github.com/repos/', + platformProfileRepo: '.github', + starsKey: 'stargazers_count' + }, + /* { + name: 'gitlab', + orgApiUrl: 'https://gitlab.com/api/v4/groups/test-org/projects', + repoApiUrl: 'https://gitlab.com/api/v4/projects/', + platformProfileRepo: 'gitlab-profile', + starsKey: 'star_count' + }, */ + { + name: 'codeberg', + orgApiUrl: 'https://codeberg.org/api/v1/orgs/test-org/repos', + repoApiUrl: 'https://codeberg.org/api/v1/repos/', + platformProfileRepo: '.profile', + starsKey: 'stars_count' + } +]; + +describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { const originalFetch = global.fetch; beforeEach(() => { @@ -25,8 +50,11 @@ describe('fetchCodeRepos', () => { vi.clearAllMocks(); }); - const orgApiUrl = 'https://api.github.com/orgs/test-org/repos'; - const repoApiUrl = 'https://api.github.com/repos/'; + const orgApiUrl = platformConfig.orgApiUrl; + const repoApiUrl = platformConfig.repoApiUrl; + const platform = platformConfig.name; + const starsKey = platformConfig.starsKey; + const platformProfileRepo = platformConfig.platformProfileRepo; const refreshIntervalDays = 30; const releasesMap = {}; @@ -37,7 +65,7 @@ describe('fetchCodeRepos', () => { const oldestDateISO = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(); // 90 days ago it('maps raw repo data and attaches release data from releasesMap', async () => { - // Mock a single page GitHub response + // Mock a single page platform response global.fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => null }, // No 'link' header means no pagination @@ -47,7 +75,7 @@ describe('fetchCodeRepos', () => { updated_at: now.toISOString(), created_at: recentDateISO, topics: ['python'], - stargazers_count: 42, + [starsKey]: 42, }]) }); @@ -57,7 +85,7 @@ describe('fetchCodeRepos', () => { }; const items = await fetchCodeRepos( - 'github', + platform, [], // No additional repos orgApiUrl, repoApiUrl, @@ -108,7 +136,7 @@ describe('fetchCodeRepos', () => { }); const items = await fetchCodeRepos( - 'github', + platform, [], orgApiUrl, repoApiUrl, @@ -164,7 +192,7 @@ describe('fetchCodeRepos', () => { ]; const items = await fetchCodeRepos( - 'github', + platform, additionalRepos, orgApiUrl, repoApiUrl, @@ -189,8 +217,8 @@ describe('fetchCodeRepos', () => { expect(items.map(i => i.id)).toContain('external-org/external-additional'); }); - // Test that forks and .github repos are excluded from the final list - it('excludes repository targets designated as forks and .github', async () => { + // Test that forks and platform profile repos are excluded from the final list + it('excludes repository targets designated as forks and platform profile repos', async () => { global.fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => null }, @@ -217,8 +245,8 @@ describe('fetchCodeRepos', () => { updated_at: recentDateISO }, { - full_name: 'test-org/.github', - name: '.github', + full_name: `test-org/${platformProfileRepo}`, + name: platformProfileRepo, fork: false, created_at: recentDateISO, updated_at: recentDateISO @@ -231,7 +259,7 @@ describe('fetchCodeRepos', () => { ]; const items = await fetchCodeRepos( - 'github', + platform, additionalRepos, orgApiUrl, repoApiUrl, @@ -242,7 +270,7 @@ describe('fetchCodeRepos', () => { expect(items.map(i => i.id)).toContain('test-org/valid-repo'); expect(items.map(i => i.id)).toContain('test-org/forked-in-list'); expect(items.map(i => i.id)).not.toContain('test-org/forked-repo'); - expect(items.map(i => i.id)).not.toContain('test-org/.github'); + expect(items.map(i => i.id)).not.toContain(`test-org/${platformProfileRepo}`); }); // Test that isNew flag is set correctly based on creation date and refresh interval @@ -267,7 +295,7 @@ describe('fetchCodeRepos', () => { }); const items = await fetchCodeRepos( - 'github', + platform, [], orgApiUrl, repoApiUrl, @@ -292,7 +320,7 @@ describe('fetchCodeRepos', () => { }); const items = await fetchCodeRepos( - 'github', + platform, [], orgApiUrl, repoApiUrl, @@ -302,7 +330,7 @@ describe('fetchCodeRepos', () => { expect(handleError).toHaveBeenCalledWith( expect.any(Error), - expect.stringContaining('Failed to fetch code from github') + expect.stringContaining(`Failed to fetch code from ${platform}`) ); expect(items).toEqual([]); });