master
1---
2import { Icon } from 'astro-icon/components';
3
4type Platform = {
5 /**
6 * The platform type of the repository
7 *
8 * 存储库的平台类型
9 */
10 platform: string;
11};
12
13interface GithubPlatform extends Platform {
14 platform: 'github';
15}
16
17interface GiteaPlatform extends Platform {
18 platform: 'gitea' | 'forgejo';
19 /**
20 * The host of the Gitea/Forgejo instance
21 *
22 * Gitea/Forgejo 实例的主机地址
23 *
24 * @example 'https://gitea.example.com'
25 */
26 host: string;
27 /**
28 * The Iconify icon name of the Gitea/Forgejo instance
29 *
30 * Gitea/Forgejo 实例的 Iconify 图标名称
31 */
32 icon?: string;
33}
34
35type AllPlatform = GithubPlatform | GiteaPlatform;
36
37type Props = {
38 repo:
39 | {
40 owner: string;
41 name: string;
42 }
43 | `${string}/${string}`;
44} & AllPlatform;
45
46const { repo, platform } = Astro.props;
47const repoName = typeof repo === 'string' ? repo : `${repo.owner}/${repo.name}`;
48
49let url: string;
50let icon: string;
51
52switch (platform) {
53 case 'github': {
54 url = `https://github.com/${repoName}/`;
55 icon = 'mdi:github';
56 break;
57 }
58 case 'gitea':
59 case 'forgejo': {
60 const { host, icon: hostIcon } = Astro.props;
61 url = `${host}/${repoName}/`;
62 icon = hostIcon || `simple-icons:${platform}`;
63 break;
64 }
65 default: {
66 throw new Error(`Unsupported platform: ${platform} for RepoCard`);
67 }
68}
69---
70
71<a
72 href={url}
73 class="card border-base-content/25 my-4 overflow-hidden border"
74 data-repo={repoName}
75 data-platform={platform}
76>
77 <div class="card-body p-4">
78 <div class="card-title mb-4 justify-between">
79 <span class="text-xl">{repoName}</span>
80 <Icon name={icon} height="3rem" width="3rem" />
81 </div>
82 <div class="repo-card-desc flex flex-col gap-2">
83 <div class="skeleton h-4 w-full"></div>
84 <div class="skeleton h-4 w-2/3"></div>
85 </div>
86 <div class="card-actions">
87 <div class="repo-card-star flex items-center justify-center gap-1.5">
88 <Icon name="material-symbols:star-outline-rounded" height="1.25rem" width="1.25rem" />
89 <span class="skeleton h-4 w-6"></span>
90 </div>
91 <div class="repo-card-fork flex items-center justify-center gap-1.5">
92 <Icon name="material-symbols:fork-right-rounded" height="1.25rem" width="1.25rem" />
93 <span class="skeleton h-4 w-6"></span>
94 </div>
95 <div class="repo-card-license hidden items-center justify-center gap-1.5">
96 <Icon name="material-symbols:balance-rounded" height="1.25rem" width="1.25rem" />
97 <span></span>
98 </div>
99 <div class="repo-card-lang hidden items-center justify-center gap-1.5">
100 <Icon name="mingcute:code-line" height="1.25rem" width="1.25rem" />
101 <span></span>
102 </div>
103 </div>
104 </div>
105</a>
106
107<script>
108 const allPlatform = ['github', 'gitea', 'forgejo'] as const;
109 type Platform = (typeof allPlatform)[number];
110
111 interface RepoMeta {
112 description: string | null;
113 language: string | null;
114 license: string | null;
115 stars: number;
116 forks: number;
117 }
118
119 function updateCardUI(card: Element, meta: RepoMeta) {
120 const descriptionNode = card.querySelector('.repo-card-desc')!;
121 const languageNode = card.querySelector('.repo-card-lang')!;
122 const licenseNode = card.querySelector('.repo-card-license')!;
123 const forksNode = card.querySelector('.repo-card-fork')!.querySelector('span')!;
124 const starsNode = card.querySelector('.repo-card-star')!.querySelector('span')!;
125
126 descriptionNode.innerHTML = meta.description || '';
127
128 if (meta.language) {
129 languageNode.classList.remove('hidden');
130 languageNode.classList.add('flex');
131 languageNode.querySelector('span')!.innerText = meta.language;
132 }
133
134 if (meta.license) {
135 licenseNode.classList.remove('hidden');
136 licenseNode.classList.add('flex');
137 licenseNode.querySelector('span')!.innerText = meta.license;
138 }
139
140 forksNode.classList.remove('skeleton', 'h-4', 'w-6');
141 forksNode.innerText = meta.forks.toString();
142
143 starsNode.classList.remove('skeleton', 'h-4', 'w-6');
144 starsNode.innerText = meta.stars.toString();
145 }
146
147 async function init() {
148 const repoCards = document.querySelectorAll('.card[data-repo]');
149 const cardsByPlatform = allPlatform.reduce(
150 (acc, platform) => {
151 acc[platform] = Array.from(repoCards).filter(
152 (card) => card.getAttribute('data-platform') === platform
153 );
154 return acc;
155 },
156 {} as Record<Platform, Element[]>
157 );
158
159 Object.entries(cardsByPlatform).forEach(async ([platform, cards]) => {
160 switch (platform as Platform) {
161 case 'github': {
162 const { request: githubApiRequest } = await import('@octokit/request');
163 cards.forEach(async (card) => {
164 const repoName = card.getAttribute('data-repo')!;
165 const [owner, repo] = repoName.split('/');
166
167 const meta = await githubApiRequest('GET /repos/{owner}/{repo}', {
168 owner,
169 repo,
170 headers: {
171 'X-GitHub-Api-Version': '2022-11-28',
172 },
173 });
174
175 const repoMeta: RepoMeta = {
176 description: meta.data.description,
177 language: meta.data.language,
178 license: meta.data.license?.spdx_id || null,
179 forks: meta.data.forks_count,
180 stars: meta.data.stargazers_count,
181 };
182
183 updateCardUI(card, repoMeta);
184 });
185 break;
186 }
187 case 'gitea':
188 case 'forgejo': {
189 cards.forEach(async (card) => {
190 const url = card.getAttribute('href');
191 const repoName = card.getAttribute('data-repo')!;
192 const host = url?.substring(0, url.indexOf(repoName) - 1);
193 if (!host) return;
194
195 try {
196 const response = await fetch(`${host}/api/v1/repos/${repoName}`);
197 const data = await response.json();
198 const repoMeta: RepoMeta = {
199 description: data.description,
200 language: data.language,
201 // Gitea and Forgejo do not provide license information in the API response.
202 // And infer license type on client side is expensive.
203 license: null,
204 forks: data.forks_count,
205 stars: data.stars_count,
206 };
207
208 updateCardUI(card, repoMeta);
209 } catch (error) {
210 console.error(`Failed to fetch repo data for ${repoName}:`, error);
211 }
212 });
213 }
214 }
215 });
216 }
217
218 document.addEventListener('astro:after-swap', init);
219 init();
220</script>