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>