Skip to content

Commit 865e49d

Browse files
committed
first commit
0 parents  commit 865e49d

23 files changed

+572
-0
lines changed

.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

Dockerfile

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM oven/bun:1 as builder
2+
WORKDIR /app
3+
COPY package.json bun.lockb ./
4+
RUN bun install
5+
COPY . .
6+
RUN bun run build
7+
8+
FROM nginx:alpine
9+
COPY --from=builder /app/dist /usr/share/nginx/html
10+
COPY nginx.conf /etc/nginx/conf.d/default.conf
11+
EXPOSE 80
12+
CMD ["nginx", "-g", "daemon off;"]

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# React + TypeScript + Vite
2+
3+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+
5+
Currently, two official plugins are available:
6+
7+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9+
10+
## Expanding the ESLint configuration
11+
12+
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13+
14+
- Configure the top-level `parserOptions` property like this:
15+
16+
```js
17+
export default tseslint.config({
18+
languageOptions: {
19+
// other options...
20+
parserOptions: {
21+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
22+
tsconfigRootDir: import.meta.dirname,
23+
},
24+
},
25+
})
26+
```
27+
28+
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29+
- Optionally add `...tseslint.configs.stylisticTypeChecked`
30+
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31+
32+
```js
33+
// eslint.config.js
34+
import react from 'eslint-plugin-react'
35+
36+
export default tseslint.config({
37+
// Set the react version
38+
settings: { react: { version: '18.3' } },
39+
plugins: {
40+
// Add the react plugin
41+
react,
42+
},
43+
rules: {
44+
// other rules...
45+
// Enable its recommended rules
46+
...react.configs.recommended.rules,
47+
...react.configs['jsx-runtime'].rules,
48+
},
49+
})
50+
```

bun.lockb

108 KB
Binary file not shown.

docker-compose.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
github-release-stats:
3+
build: .
4+
ports:
5+
- "8592:80"
6+
restart: unless-stopped

eslint.config.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
},
28+
)

index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Github Release Stats</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

nginx.conf

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
server {
2+
listen 80;
3+
server_name localhost;
4+
root /usr/share/nginx/html;
5+
index index.html;
6+
7+
location / {
8+
try_files $uri $uri/ /index.html;
9+
}
10+
}

package.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "github-release-stats",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@emotion/react": "^11.14.0",
14+
"@emotion/styled": "^11.14.0",
15+
"@mui/icons-material": "^6.4.0",
16+
"@mui/material": "^6.4.0",
17+
"react": "^18.3.1",
18+
"react-dom": "^18.3.1"
19+
},
20+
"devDependencies": {
21+
"@eslint/js": "^9.18.0",
22+
"@types/react": "^18.3.18",
23+
"@types/react-dom": "^18.3.5",
24+
"@vitejs/plugin-react": "^4.3.4",
25+
"eslint": "^9.18.0",
26+
"eslint-plugin-react-hooks": "^5.1.0",
27+
"eslint-plugin-react-refresh": "^0.4.18",
28+
"globals": "^15.14.0",
29+
"typescript": "~5.6.3",
30+
"typescript-eslint": "^8.20.0",
31+
"vite": "^6.0.7"
32+
}
33+
}

public/vite.svg

+1
Loading

src/App.tsx

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useState } from 'react'
2+
import { Container, Paper, TextField, Button, Typography, Box, Card, Chip, Stack } from '@mui/material'
3+
import { ThemeProvider, createTheme } from '@mui/material/styles'
4+
import { Download, GitHub } from '@mui/icons-material'
5+
6+
const darkTheme = createTheme({
7+
palette: {
8+
mode: 'dark',
9+
primary: {
10+
main: '#2196f3'
11+
}
12+
}
13+
})
14+
15+
function App() {
16+
const [owner, setOwner] = useState('')
17+
const [repo, setRepo] = useState('')
18+
const [releases, setReleases] = useState<any[]>([])
19+
20+
const fetchReleases = async () => {
21+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
22+
const data = await response.json()
23+
setReleases(data)
24+
}
25+
26+
const calculateTotalDownloads = (releases: any[]) => {
27+
return releases.reduce((total, release) => {
28+
const releaseDownloads = release.assets.reduce(
29+
(sum: number, asset: any) => sum + asset.download_count,
30+
0
31+
)
32+
return total + releaseDownloads
33+
}, 0)
34+
}
35+
36+
return (
37+
<ThemeProvider theme={darkTheme}>
38+
<Container maxWidth="md" sx={{ py: 4 }}>
39+
<Paper sx={{ p: 4, borderRadius: 2 }}>
40+
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', gap: 2 }}>
41+
<GitHub sx={{ fontSize: 40 }} />
42+
<Typography variant="h4" component="h1">
43+
GitHub Release Stats
44+
</Typography>
45+
</Box>
46+
47+
<Stack direction="row" spacing={2} sx={{ mb: 4 }}>
48+
<TextField
49+
fullWidth
50+
label="Owner"
51+
value={owner}
52+
onChange={(e) => setOwner(e.target.value)}
53+
variant="outlined"
54+
/>
55+
<TextField
56+
fullWidth
57+
label="Repository"
58+
value={repo}
59+
onChange={(e) => setRepo(e.target.value)}
60+
variant="outlined"
61+
/>
62+
<Button
63+
variant="contained"
64+
size="large"
65+
onClick={fetchReleases}
66+
disabled={!owner || !repo}
67+
>
68+
Get Stats
69+
</Button>
70+
</Stack>
71+
72+
{releases.length > 0 && (
73+
<Card sx={{ mb: 3, p: 2, bgcolor: 'primary.dark' }}>
74+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
75+
<Download />
76+
<Typography variant="h6">
77+
Total Downloads: {calculateTotalDownloads(releases).toLocaleString()}
78+
</Typography>
79+
</Box>
80+
</Card>
81+
)}
82+
83+
{releases.map((release) => (
84+
<Card key={release.id} sx={{ mb: 2, p: 3 }}>
85+
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
86+
<Box>
87+
<Typography variant="h6" component="h2">
88+
{release.tag_name}
89+
{release.name && ` - ${release.name}`}
90+
</Typography>
91+
<Typography variant="body2" color="text.secondary">
92+
Released on {new Date(release.published_at).toLocaleDateString()}
93+
</Typography>
94+
</Box>
95+
{release.prerelease && (
96+
<Chip label="Pre-release" color="warning" size="small" />
97+
)}
98+
</Box>
99+
100+
<Typography variant="body1" sx={{ mb: 2 }}>
101+
{release.body}
102+
</Typography>
103+
104+
<Stack direction="row" spacing={2}>
105+
{release.assets.map((asset: any) => (
106+
<Card
107+
key={asset.id}
108+
variant="outlined"
109+
sx={{ p: 2, minWidth: 200 }}
110+
>
111+
<Typography variant="subtitle2">{asset.name}</Typography>
112+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
113+
<Download fontSize="small" />
114+
<Typography variant="body2">
115+
{(asset.size / (1024 * 1024)).toFixed(2)} MB
116+
</Typography>
117+
<Typography variant="body2" color="text.secondary">
118+
{asset.download_count} downloads
119+
</Typography>
120+
</Box>
121+
</Card>
122+
))}
123+
</Stack>
124+
</Card>
125+
))}
126+
</Paper>
127+
</Container>
128+
</ThemeProvider>
129+
)
130+
}
131+
132+
export default App

src/assets/react.svg

+1
Loading

src/components/ReleaseCard.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Release } from '../types';
2+
3+
interface Props {
4+
release: Release;
5+
isLatest: boolean;
6+
}
7+
8+
export function ReleaseCard({ release, isLatest }: Props) {
9+
const formatBytes = (bytes: number) => {
10+
const mb = bytes / 1024 / 1024;
11+
return `${mb.toFixed(2)} MB`;
12+
};
13+
14+
const formatNumber = (num: number) => {
15+
return new Intl.NumberFormat().format(num);
16+
};
17+
18+
return (
19+
<div className={`p-6 rounded-lg shadow-md mb-4 ${
20+
isLatest ? 'bg-green-50 border-green-200' :
21+
release.prerelease ? 'bg-yellow-50 border-yellow-200' :
22+
'bg-gray-50 border-gray-200'
23+
} border`}>
24+
<div className="flex items-center justify-between mb-4">
25+
<h3 className="text-xl font-semibold">
26+
<a href={release.html_url} target="_blank" rel="noopener noreferrer"
27+
className="text-blue-600 hover:underline">
28+
{release.tag_name}
29+
</a>
30+
{isLatest && <span className="ml-2 px-2 py-1 text-sm bg-green-500 text-white rounded">Latest</span>}
31+
{release.prerelease && <span className="ml-2 px-2 py-1 text-sm bg-yellow-500 text-white rounded">Pre-release</span>}
32+
</h3>
33+
<span className="text-gray-500">
34+
Released on {new Date(release.published_at).toLocaleDateString()}
35+
</span>
36+
</div>
37+
38+
{release.assets.length > 0 && (
39+
<div className="space-y-2">
40+
<h4 className="font-medium">Downloads</h4>
41+
<ul className="space-y-2">
42+
{release.assets.map(asset => (
43+
<li key={asset.name} className="flex items-center justify-between">
44+
<span className="font-mono text-sm">{asset.name}</span>
45+
<div className="text-sm text-gray-600">
46+
<span>{formatBytes(asset.size)}</span>
47+
<span className="mx-2"></span>
48+
<span>{formatNumber(asset.download_count)} downloads</span>
49+
</div>
50+
</li>
51+
))}
52+
</ul>
53+
</div>
54+
)}
55+
</div>
56+
);
57+
}

0 commit comments

Comments
 (0)