feat: language component
This commit is contained in:
parent
83e3e2d86e
commit
5744816f08
1 changed files with 266 additions and 0 deletions
266
src/routes/languages.svelte
Normal file
266
src/routes/languages.svelte
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let repositoryUrl: string;
|
||||||
|
|
||||||
|
interface LanguageData {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParsedRepo {
|
||||||
|
platform: 'github' | 'gitlab' | 'codeberg' | 'forgejo';
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let languages: LanguageData = {};
|
||||||
|
let loading = true;
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
// Parse repository URL to determine platform and extract owner/repo
|
||||||
|
function parseRepoUrl(url: string): ParsedRepo | null {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const hostname = urlObj.hostname;
|
||||||
|
const pathParts = urlObj.pathname.split('/').filter((p) => p);
|
||||||
|
|
||||||
|
if (pathParts.length < 2) return null;
|
||||||
|
|
||||||
|
let platform: ParsedRepo['platform'];
|
||||||
|
if (hostname.includes('github.com')) {
|
||||||
|
platform = 'github';
|
||||||
|
} else if (hostname.includes('gitlab.com')) {
|
||||||
|
platform = 'gitlab';
|
||||||
|
} else if (hostname.includes('codeberg.org')) {
|
||||||
|
platform = 'codeberg';
|
||||||
|
} else {
|
||||||
|
platform = 'forgejo'; // Assume forgejo for custom instances
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform,
|
||||||
|
owner: pathParts[0],
|
||||||
|
repo: pathParts[1].replace('.git', '')
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch language data from GitHub API
|
||||||
|
async function fetchGitHub(owner: string, repo: string) {
|
||||||
|
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/languages`);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch from GitHub');
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch language data from GitLab API
|
||||||
|
async function fetchGitLab(owner: string, repo: string) {
|
||||||
|
const projectPath = encodeURIComponent(`${owner}/${repo}`);
|
||||||
|
const response = await fetch(`https://gitlab.com/api/v4/projects/${projectPath}/languages`);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch from GitLab');
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch language data from Codeberg API (Gitea-based)
|
||||||
|
async function fetchCodeberg(owner: string, repo: string) {
|
||||||
|
// Codeberg uses Gitea API which doesn't have direct language stats
|
||||||
|
// We'll use GitHub's linguist approach or fallback
|
||||||
|
const response = await fetch(`https://codeberg.org/api/v1/repos/${owner}/${repo}/languages`);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch from Codeberg');
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percentages
|
||||||
|
function calculatePercentages(data: LanguageData) {
|
||||||
|
const total = Object.values(data).reduce((sum, val) => sum + val, 0);
|
||||||
|
return Object.entries(data)
|
||||||
|
.map(([name, bytes]) => ({
|
||||||
|
name,
|
||||||
|
bytes,
|
||||||
|
percentage: ((bytes / total) * 100).toFixed(1)
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.bytes - a.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color mapping for common languages
|
||||||
|
const languageColors: { [key: string]: string } = {
|
||||||
|
'C++': '#f34b7d',
|
||||||
|
CMake: '#da3434',
|
||||||
|
Shell: '#89e051',
|
||||||
|
JavaScript: '#f1e05a',
|
||||||
|
TypeScript: '#3178c6',
|
||||||
|
Python: '#3572A5',
|
||||||
|
Java: '#b07219',
|
||||||
|
Go: '#00ADD8',
|
||||||
|
Rust: '#dea584',
|
||||||
|
HTML: '#e34c26',
|
||||||
|
CSS: '#563d7c',
|
||||||
|
Other: '#858585'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch languages on mount or when URL changes
|
||||||
|
$: if (repositoryUrl) {
|
||||||
|
fetchLanguages();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLanguages() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = parseRepoUrl(repositoryUrl);
|
||||||
|
if (!parsed) {
|
||||||
|
throw new Error('Invalid repository URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: LanguageData;
|
||||||
|
switch (parsed.platform) {
|
||||||
|
case 'github':
|
||||||
|
data = await fetchGitHub(parsed.owner, parsed.repo);
|
||||||
|
break;
|
||||||
|
case 'gitlab':
|
||||||
|
data = await fetchGitLab(parsed.owner, parsed.repo);
|
||||||
|
break;
|
||||||
|
case 'codeberg':
|
||||||
|
data = await fetchCodeberg(parsed.owner, parsed.repo);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported platform');
|
||||||
|
}
|
||||||
|
|
||||||
|
languages = data;
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Failed to fetch language data';
|
||||||
|
languages = {};
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: languageStats = calculatePercentages(languages);
|
||||||
|
$: otherLanguages = languageStats.slice(3);
|
||||||
|
$: otherPercentage = otherLanguages
|
||||||
|
.reduce((sum, lang) => sum + parseFloat(lang.percentage), 0)
|
||||||
|
.toFixed(1);
|
||||||
|
$: displayedLanguages = (() => {
|
||||||
|
const topThree = languageStats.slice(0, 3);
|
||||||
|
if (otherLanguages.length > 0) {
|
||||||
|
return [
|
||||||
|
...topThree,
|
||||||
|
{
|
||||||
|
name: 'Other',
|
||||||
|
bytes: otherLanguages.reduce((sum, lang) => sum + lang.bytes, 0),
|
||||||
|
percentage: otherPercentage
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return topThree;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="language-stats block">
|
||||||
|
{#if loading}
|
||||||
|
<div class="loading">Loading...</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="error">{error}</div>
|
||||||
|
{:else if languageStats.length > 0}
|
||||||
|
<div class="progress-bar">
|
||||||
|
{#each displayedLanguages as lang}
|
||||||
|
<div
|
||||||
|
class="progress-segment"
|
||||||
|
style="width: {lang.percentage}%; background-color: {languageColors[lang.name] ||
|
||||||
|
'#858585'}"
|
||||||
|
title="{lang.name}: {lang.percentage}%"
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="language-list">
|
||||||
|
{#each displayedLanguages as lang}
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
class="language-dot"
|
||||||
|
style="background-color: {languageColors[lang.name] || '#858585'}"
|
||||||
|
></span>
|
||||||
|
<span class="language-name">{lang.name}</span>
|
||||||
|
<span class="language-percentage">{lang.percentage}%</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<div class="no-data">No language data available</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.language-stats {
|
||||||
|
background-color: #282828;
|
||||||
|
color: #c9d1d9;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: flex;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #21262d;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-name {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-percentage {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error,
|
||||||
|
.no-data {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Add table
Reference in a new issue