diff --git a/composables/useFlippedCards.ts b/composables/useFlippedCards.ts
new file mode 100644
index 0000000..33f8188
--- /dev/null
+++ b/composables/useFlippedCards.ts
@@ -0,0 +1,9 @@
+export const useFlippedCards = defineStore('flippedCards', () => {
+ const value = ref
([]);
+
+ onUnmounted(() => {
+ value.value = [];
+ });
+
+ return { value };
+});
diff --git a/package.json b/package.json
index 9d8e084..4481349 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"dependencies": {
"@heroicons/vue": "^2.1.3",
"@pinia/nuxt": "^0.5.1",
+ "@tsparticles/confetti": "^3.3.0",
"node-emoji": "^2.1.3",
"nuxt": "^3.11.2",
"vue": "^3.4.21",
diff --git a/pages/index.vue b/pages/index.vue
index 88e88d8..cc6fbc6 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -116,14 +116,14 @@ onUnmounted(() => {
{{ repo.ownerName }}/
- {{ repo.name }}
+ {{ repo.name }}
diff --git a/pages/insights.vue b/pages/insights.vue
new file mode 100644
index 0000000..5cb5187
--- /dev/null
+++ b/pages/insights.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
Across all these repositories...
+
+
+
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 38c2f50..dcb57af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@pinia/nuxt':
specifier: ^0.5.1
version: 0.5.1(rollup@4.17.2)(typescript@5.4.5)(vue@3.4.26(typescript@5.4.5))
+ '@tsparticles/confetti':
+ specifier: ^3.3.0
+ version: 3.3.0
node-emoji:
specifier: ^2.1.3
version: 2.1.3
@@ -1029,6 +1032,75 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
+ '@tsparticles/basic@3.3.0':
+ resolution: {integrity: sha512-YB6+pFnkby6hnHhDqH2Q7+Y6Zcm7RAgZLQ8gkecHQxywD0RFItpYQfxpIf82mOTZ39NoeVdH6AF3mydgxVNAMQ==}
+
+ '@tsparticles/confetti@3.3.0':
+ resolution: {integrity: sha512-LgV4h9h6X8kHSeeTWmAOt3dxq9/yhDe4guybYUPqus5WvMVqDGnx9WIdKzJhdFywPaqfR0OYDdHBsHAxdHK8bQ==}
+
+ '@tsparticles/engine@3.3.0':
+ resolution: {integrity: sha512-Sr24epYquTelGrUbMaknXTscib8IMQJrbmShJnEemU+wpZNIPtAh09sQgGtq1pUxrGQRUSQIgaybYuXcNgk8rg==}
+
+ '@tsparticles/move-base@3.3.0':
+ resolution: {integrity: sha512-yxfynO0CHWYlhyXy53/HzsN8zyD+v1RX0wT9X1Wry5lgnxhJoFTAP/Pk+srgyLOdaD0WwoRjB3yA/0f/haBWkg==}
+
+ '@tsparticles/plugin-emitters@3.3.0':
+ resolution: {integrity: sha512-Ze2f47YBxjgfUHRKy0YdR+j6kGq7yrrdnbR5Ttq5d6vSjC13tFL+T14YM0tqZBr4QaZZB35UpKr3r2C4WVdDug==}
+
+ '@tsparticles/plugin-motion@3.3.0':
+ resolution: {integrity: sha512-N3Q+FGXObu6n1ErhfnIHgltrR2zvSsXMPLjdkgqyg6l2i1oD3ybw57siGM/th0k7m7hvJ96HmDaJrY3sUuqX6Q==}
+
+ '@tsparticles/shape-cards@3.3.0':
+ resolution: {integrity: sha512-WPiGYZJFfVaFjnzbTm5gg0HIVgy4qbwSKTjAaeVxf3J0oMiu8nFV7Ya9My2ondvDWrUeTpRem2Zzbb81yYnyZg==}
+
+ '@tsparticles/shape-circle@3.3.0':
+ resolution: {integrity: sha512-m/T3SbZf8Zrn0m4Rd+8KTCMy54cofkaXa1Z7oikQYr/gPigT2C7Bo4vwQpiP8HKU+Xh5CEHFyc0s6ogfOaA2fA==}
+
+ '@tsparticles/shape-emoji@3.3.0':
+ resolution: {integrity: sha512-F9tl3jUTMCRzbwhpKk3t1z2d+7vbyeAoHAEeG4UdUVorO0ovaqzj16KcpfSu2wyGkPSzUKIyHX8doB9MR8DfGw==}
+
+ '@tsparticles/shape-heart@3.3.0':
+ resolution: {integrity: sha512-R47xiFP7xxbBKjYSQFcfoBxLkVZLNNhzzDFqKMz3feAUXQxLxoV8bPNVH6Sx2Xn4coxqK72lhi1tNGq3buWiTw==}
+
+ '@tsparticles/shape-image@3.3.0':
+ resolution: {integrity: sha512-w5PHiDZjILIUEDIn10bFasY1qnSY9lwV0ekoTLGDepiS/EmyNJb0+D7gWOy/mhFlpK1637Ngbz1Axw3Zfl8ObA==}
+
+ '@tsparticles/shape-polygon@3.3.0':
+ resolution: {integrity: sha512-JAHKIO8Pmzft0METZn6BqTHpn9PxWXxhAlPMG0XOEuziHpvMxmTXYbJNDFlmWu9RFpAjfIeGg8zUcS1xNuGWUQ==}
+
+ '@tsparticles/shape-square@3.3.0':
+ resolution: {integrity: sha512-72tLkzQ5QkkhgIFy+qqdA+vmBk8VE4PuJcLJ12FVH8e3uPJDO2WiLJmnCg9MHyp26uU5CEuIalrQpZ0TX1A5PQ==}
+
+ '@tsparticles/shape-star@3.3.0':
+ resolution: {integrity: sha512-RX9RLuJ9oWQbOVvVyiOBdPK8dc+RLc7DaqEOxGhMTPQeGvryjdkBU/FmiG4a7KaVyZeCI2diDW4oWEkulzZIIg==}
+
+ '@tsparticles/updater-color@3.3.0':
+ resolution: {integrity: sha512-4ZggZr4jTXXOCLXqMXkH/jfhylf9wLt2G7D/F9ZZPihh8l8F2x0YM/JvzRePynhIFXfZaMD1PRfdXNTr6HnFFQ==}
+
+ '@tsparticles/updater-life@3.3.0':
+ resolution: {integrity: sha512-6DDx4FfezLmXxetVx0kFZdWKWLIPa/ujFxHto0fFIVFtjLaffJPEONoOIj6/TmAlcJ+XF2jY2Md4z6vyS6bMlA==}
+
+ '@tsparticles/updater-opacity@3.3.0':
+ resolution: {integrity: sha512-aQZJheqvoD69YYPiSlcRuWU7yEPs9dSmeOALP+fcaQwUQbVvr+wNJSUVkVNzeawtv4tPwou4QnytoWaBmZoqog==}
+
+ '@tsparticles/updater-out-modes@3.3.0':
+ resolution: {integrity: sha512-G+UDZO6pmBUdSeT1Y7SRXvZz2EUw5RBCF8AOQMyLntehJQgLPc+PbMCFenfjpRQCCiWQ8RSumZ040iv5CLRUug==}
+
+ '@tsparticles/updater-roll@3.3.0':
+ resolution: {integrity: sha512-z4UNZnC1/ZtXRZqMMITjqTxjs+qhggoL2W6c6AjvQ9TUyz+Ixg8tIrcPcwUu69496o9HJf6mJ4C3G9Pb9CMm7Q==}
+
+ '@tsparticles/updater-rotate@3.3.0':
+ resolution: {integrity: sha512-4i3+0rbvzmaNGunlK443kURoEVFeAvCY5VGNX73y8S5g94RFejtGYBUUsC/LAcCxgfxa3HgYwNTT6ypslnoNuw==}
+
+ '@tsparticles/updater-size@3.3.0':
+ resolution: {integrity: sha512-8s0dSh8bV4CN13oM86x1MPkI1T6KwuKPIiSdTcO1qKTcN1WBYzuuIPOU7Q3+fbQhSZ6F+da3zdG9unqU5sPYUA==}
+
+ '@tsparticles/updater-tilt@3.3.0':
+ resolution: {integrity: sha512-ALcz+lsdaZn3pZkBRQssF55DVVQf0nX7rXCn6dhgIcwvpTSU1Fm5wYP2Q7cr6UDOu1lOAk5BHEQaG2EgHTanMQ==}
+
+ '@tsparticles/updater-wobble@3.3.0':
+ resolution: {integrity: sha512-S9TpaGCWUnfFsk/ZVQVW+KrBG5mkTbbrj6lUx8OuhJmAft6v6zKVByhrMN8LIxYwr8S6mGmguOfFt/ZG2//xkw==}
+
'@tufjs/canonical-json@2.0.0':
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -5661,6 +5733,117 @@ snapshots:
'@trysound/sax@0.2.0': {}
+ '@tsparticles/basic@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+ '@tsparticles/move-base': 3.3.0
+ '@tsparticles/shape-circle': 3.3.0
+ '@tsparticles/updater-color': 3.3.0
+ '@tsparticles/updater-opacity': 3.3.0
+ '@tsparticles/updater-out-modes': 3.3.0
+ '@tsparticles/updater-size': 3.3.0
+
+ '@tsparticles/confetti@3.3.0':
+ dependencies:
+ '@tsparticles/basic': 3.3.0
+ '@tsparticles/engine': 3.3.0
+ '@tsparticles/plugin-emitters': 3.3.0
+ '@tsparticles/plugin-motion': 3.3.0
+ '@tsparticles/shape-cards': 3.3.0
+ '@tsparticles/shape-emoji': 3.3.0
+ '@tsparticles/shape-heart': 3.3.0
+ '@tsparticles/shape-image': 3.3.0
+ '@tsparticles/shape-polygon': 3.3.0
+ '@tsparticles/shape-square': 3.3.0
+ '@tsparticles/shape-star': 3.3.0
+ '@tsparticles/updater-life': 3.3.0
+ '@tsparticles/updater-roll': 3.3.0
+ '@tsparticles/updater-rotate': 3.3.0
+ '@tsparticles/updater-tilt': 3.3.0
+ '@tsparticles/updater-wobble': 3.3.0
+
+ '@tsparticles/engine@3.3.0': {}
+
+ '@tsparticles/move-base@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/plugin-emitters@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/plugin-motion@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-cards@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-circle@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-emoji@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-heart@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-image@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-polygon@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-square@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/shape-star@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-color@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-life@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-opacity@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-out-modes@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-roll@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-rotate@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-size@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-tilt@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
+ '@tsparticles/updater-wobble@3.3.0':
+ dependencies:
+ '@tsparticles/engine': 3.3.0
+
'@tufjs/canonical-json@2.0.0': {}
'@tufjs/models@2.0.0':
diff --git a/server/api/insights.ts b/server/api/insights.ts
new file mode 100644
index 0000000..e342ea0
--- /dev/null
+++ b/server/api/insights.ts
@@ -0,0 +1,67 @@
+import type { Repository } from './repositories';
+
+type NumericKeys
= {
+ [K in keyof T]: T[K] extends number ? K : never;
+}[keyof T];
+
+type RepositoryNumbers = keyof Pick, undefined>>;
+
+export default defineEventHandler(async (event) => {
+ const { githubToken } = useRuntimeConfig(event);
+ const repositories = await $fetch('/api/repositories');
+
+ function findMost(key: RepositoryNumbers, invert = false): { name: string; value: number } {
+ const [repo] = repositories.sort((a, b) => (invert ? a[key] - b[key] : b[key] - a[key]));
+ return { name: `${repo.ownerName}/${repo.name}`, value: repo[key] };
+ }
+
+ async function getCountFromRestApi(type: 'commits' | 'contributors', repo: Repository) {
+ const searchParams = new URLSearchParams({ per_page: '1', page: '1' });
+ if (type === 'contributors') {
+ searchParams.set('anon', '1');
+ }
+ const response = await fetch(
+ `https://api.github.com/repos/${repo.ownerName}/${repo.name}/${type}?${searchParams.toString()}`,
+ {
+ headers: {
+ Accept: 'application/vnd.github.v4.idl',
+ Authorization: `Bearer ${githubToken}`,
+ },
+ },
+ );
+ const link = response.headers.get('Link');
+ const value = parseInt(link?.match(/page=(\d+)(?:&anon=1)?>; rel="last"/)?.[1] ?? '0');
+ return { name: `${repo.ownerName}/${repo.name}`, value };
+ }
+
+ async function findMostFromRestApi(type: 'commits' | 'contributors') {
+ const reposWithCount = await Promise.all(
+ repositories.map((repo) => getCountFromRestApi(type, repo)),
+ );
+ return reposWithCount.sort((a, b) => b.value - a.value)[0];
+ }
+
+ const mostForked = findMost('forkCount');
+ const mostRecent = findMost('age', true);
+ const oldest = findMost('age');
+
+ const [mostCommits, mostContributors] = await Promise.all([
+ findMostFromRestApi('commits'),
+ findMostFromRestApi('contributors'),
+ ]);
+
+ const [mostUsedLanguage] = repositories
+ .reduce<{ name: string; value: number }[]>((previous, { language }) => {
+ if (!language) return previous;
+ const existing = previous.find((l) => l.name === language.name);
+ if (existing) {
+ existing.value++;
+ } else {
+ previous.push({ name: language.name, value: 1 });
+ }
+ return previous;
+ }, [])
+ .sort((a, b) => b.value - a.value);
+
+ return { mostForked, mostRecent, oldest, mostCommits, mostContributors, mostUsedLanguage };
+});
diff --git a/server/api/repositories.ts b/server/api/repositories.ts
index 6356738..4b73af0 100644
--- a/server/api/repositories.ts
+++ b/server/api/repositories.ts
@@ -1,15 +1,16 @@
import { emojify } from 'node-emoji';
-type Repository = {
+export type Repository = {
rank: number;
name: string;
ownerName: string;
image: string;
description: string;
- starsNumber: number;
+ stargazerCount: number;
url: string;
age: number;
language?: { name: string; color: string };
+ forkCount: number;
};
export default defineEventHandler(async (event) => {
@@ -32,9 +33,7 @@ export default defineEventHandler(async (event) => {
description
createdAt
url
- stargazers {
- totalCount
- }
+ stargazerCount
owner {
login
avatarUrl
@@ -45,6 +44,7 @@ export default defineEventHandler(async (event) => {
color
}
}
+ forkCount
}
}
}
@@ -61,10 +61,11 @@ export default defineEventHandler(async (event) => {
ownerName: node.owner.login,
image: node.owner.avatarUrl,
description: emojify(node.description),
- starsNumber: node.stargazers.totalCount,
+ stargazerCount: node.stargazerCount,
url: node.url,
age: Date.now() - new Date(node.createdAt).getTime(),
language: node.languages.nodes[0],
+ forkCount: node.forkCount,
}));
return result;
});