diff --git a/docs/suspensive.org/package.json b/docs/suspensive.org/package.json index 6e4582b86..a13d48e41 100644 --- a/docs/suspensive.org/package.json +++ b/docs/suspensive.org/package.json @@ -25,6 +25,7 @@ "dependencies": { "@codesandbox/sandpack-react": "^2.14.4", "@codesandbox/sandpack-themes": "^2.0.21", + "d3": "^7.9.0", "next": "^14.2.3", "nextra": "^2.13.4", "nextra-theme-docs": "^2.13.4", @@ -36,6 +37,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@suspensive/eslint-config": "workspace:*", "@suspensive/tsconfig": "workspace:*", + "@types/d3": "^7.4.3", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.19", diff --git a/docs/suspensive.org/src/components/BubbleChart.tsx b/docs/suspensive.org/src/components/BubbleChart.tsx new file mode 100644 index 000000000..d13601342 --- /dev/null +++ b/docs/suspensive.org/src/components/BubbleChart.tsx @@ -0,0 +1,98 @@ +import * as d3 from 'd3' +import React, { useEffect, useRef } from 'react' +import useFetchContributors from '../lib/hooks/useFetchContributors' + +type Node = { + name: string + value: number + avatar: string + htmlUrl: string + children?: Node[] +} + +export const BubbleChart = () => { + const result = useFetchContributors() + + if (!result.isSuccess) { + return null + } + + const chartData = result.data.map((contributor) => ({ + name: contributor.author.login, + value: contributor.total, + avatar: contributor.author.avatar_url, + htmlUrl: contributor.author.html_url, + })) + + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) +} + +const BubbleChartSize = (props: { chartData: Array; width: number; height: number; padding: number }) => { + const svgRef = useRef(null) + + useEffect(() => { + if (!props.chartData.length) return + + const root = d3 + .hierarchy({ children: props.chartData } as Node) + .sum((d: Node) => (d.value < 100 ? d.value : 100) + 3) + .sort((a, b) => (b.value ?? 0) - (a.value ?? 0)) + + const pack = d3.pack().size([props.width, props.height]).padding(props.padding) + + const nodes = pack(root).descendants().slice(1) + + const svg = d3 + .select(svgRef.current) + .attr('width', props.width) + .attr('height', props.height) + .style('display', 'block') + .style('margin', 'auto') + .style('overflow', 'visible') + + const nodeGroups = svg + .selectAll('g') + .data(nodes) + .enter() + .append('g') + .attr('transform', (d) => `translate(${d.x},${d.y})`) + .style('cursor', 'pointer') + .on('click', (event, d: d3.HierarchyCircularNode) => { + if (d.data.htmlUrl) { + window.location.href = d.data.htmlUrl + } + }) + .on('mouseover', function () { + d3.select(this).select('image').style('opacity', 0.8) + }) + .on('mouseout', function () { + d3.select(this).select('image').style('opacity', 1) + }) + + nodeGroups + .append('image') + .attr('x', (d) => -d.r * 1) + .attr('y', (d) => -d.r * 1) + .attr('width', (d) => d.r * 2) + .attr('height', (d) => d.r * 2) + .attr('href', (d: d3.HierarchyCircularNode) => d.data.avatar) + .attr('clip-path', (d) => `circle(${d.r * 1}px at ${d.r * 1}px ${d.r * 1}px)`) + }, [props.chartData, props.height, props.width]) + + return +} diff --git a/docs/suspensive.org/src/components/index.ts b/docs/suspensive.org/src/components/index.ts index dfec21e8c..b14bc4374 100644 --- a/docs/suspensive.org/src/components/index.ts +++ b/docs/suspensive.org/src/components/index.ts @@ -1,3 +1,4 @@ export { Callout } from './Callout' export { HomePage } from './HomePage' export { Sandpack } from './Sandpack' +export { BubbleChart } from './BubbleChart' diff --git a/docs/suspensive.org/src/lib/hooks/useFetchContributors.ts b/docs/suspensive.org/src/lib/hooks/useFetchContributors.ts new file mode 100644 index 000000000..8b6fad17c --- /dev/null +++ b/docs/suspensive.org/src/lib/hooks/useFetchContributors.ts @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react' +import type { GithubRepoContributor } from '../../types' + +type Result = + | { + isLoading: false + isSuccess: true + isError: false + data: GithubRepoContributor[] + error: undefined + } + | { + isLoading: false + isSuccess: false + isError: true + data: undefined + error: Error + } + | { + isLoading: true + isSuccess: false + isError: false + data: undefined + error: undefined + } + +const useFetchContributors = () => { + const [result, setResult] = useState({ + isSuccess: false, + data: undefined, + error: undefined, + isError: false, + isLoading: true, + }) + + useEffect(() => { + async function fetchContributors(): Promise { + try { + const response = await fetch('https://api.github.com/repos/toss/suspensive/stats/contributors') + if (!response.ok) { + throw new Error('Failed to fetch contributors') + } + const data: GithubRepoContributor[] = (await response.json()) as GithubRepoContributor[] + const filteredContributors = data.filter((contributor) => { + const login = contributor.author.login + return !['github-actions[bot]', 'dependabot[bot]', 'renovate[bot]'].includes(login) + }) + setResult({ + data: filteredContributors, + error: undefined, + isError: false, + isLoading: false, + isSuccess: true, + }) + } catch (error) { + console.error('Error fetching contributors:', error) + } + } + + void fetchContributors() + }, []) + + return result +} + +export default useFetchContributors diff --git a/docs/suspensive.org/src/pages/docs/contributors.en.mdx b/docs/suspensive.org/src/pages/docs/contributors.en.mdx index 6db844687..fa6b54e3c 100644 --- a/docs/suspensive.org/src/pages/docs/contributors.en.mdx +++ b/docs/suspensive.org/src/pages/docs/contributors.en.mdx @@ -1,11 +1,12 @@ import { Cards, Card } from 'nextra/components' import Image from 'next/image' +import { BubbleChart } from '../../components' # Contributors Thank you to everyone who contributed to Suspensive and we look forward to your continued support. -[![contributors](https://contrib.rocks/image?repo=toss/suspensive)](https://github.com/toss/suspensive/graphs/contributors) + ## Maintainers diff --git a/docs/suspensive.org/src/pages/docs/contributors.ko.mdx b/docs/suspensive.org/src/pages/docs/contributors.ko.mdx index 9e1bfccfb..2fd45b77c 100644 --- a/docs/suspensive.org/src/pages/docs/contributors.ko.mdx +++ b/docs/suspensive.org/src/pages/docs/contributors.ko.mdx @@ -1,11 +1,12 @@ import { Cards, Card } from 'nextra/components' import Image from 'next/image' +import { BubbleChart } from '../../components' # 기여자 Suspensive에 기여해주신 모든 분들 감사하고 앞으로도 잘 부탁드립니다. -[![contributors](https://contrib.rocks/image?repo=toss/suspensive)](https://github.com/toss/suspensive/graphs/contributors) + ## 메인테이너 diff --git a/docs/suspensive.org/src/types/index.ts b/docs/suspensive.org/src/types/index.ts new file mode 100644 index 000000000..6c3bf8af2 --- /dev/null +++ b/docs/suspensive.org/src/types/index.ts @@ -0,0 +1,33 @@ +interface Week { + w: number // Week timestamp in epoch seconds + a: number // Number of additions + d: number // Number of deletions + c: number // Number of commits +} + +interface Author { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} + +export interface GithubRepoContributor { + author: Author + total: number + weeks: Week[] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdf442df0..d7ad585dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,9 @@ importers: '@codesandbox/sandpack-themes': specifier: ^2.0.21 version: 2.0.21 + d3: + specifier: ^7.9.0 + version: 7.9.0 next: specifier: ^14.2.3 version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -245,6 +248,9 @@ importers: '@suspensive/tsconfig': specifier: workspace:* version: link:../../configs/tsconfig + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/react': specifier: ^18.3.3 version: 18.3.3 @@ -1771,15 +1777,99 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.6': + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + '@types/d3-scale-chromatic@3.0.1': resolution: {integrity: sha512-Ob7OrwiTeQXY/WBBbRHGZBOn6rH1h7y3jjpTSKYqDEeqFjktql6k2XSgNwLrLDmAsXhEn8P9NHDY4VTuo0ZY1w==} '@types/d3-scale@4.0.6': resolution: {integrity: sha512-lo3oMLSiqsQUovv8j15X4BNEDOsnHuGjeVg7GRbAuB2PUa1prK5BNSOu6xixgNf3nqxPl4I1BqJWrPvFGlQoGQ==} + '@types/d3-selection@3.0.10': + resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + '@types/d3-time@3.0.2': resolution: {integrity: sha512-kbdRXTmUgNfw5OTE3KZnFQn6XdIc4QGroN5UixgdrXATmYsdlPQS6pEut9tVlIojtzuFD4txs/L+Rq41AHtLpg==} + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.8': + resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.10': resolution: {integrity: sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==} @@ -1792,6 +1882,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/geojson@7946.0.14': + resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} + '@types/hast@2.3.7': resolution: {integrity: sha512-EVLigw5zInURhzfXUM65eixfadfsHKomGKUakToXo84t8gGIJuTcD2xooM2See7GyQ7DRtYjhCHnSUQez8JaLw==} @@ -2771,8 +2864,8 @@ packages: resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} engines: {node: '>=12'} - d3@7.8.5: - resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==} + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} d@1.0.2: @@ -7880,14 +7973,123 @@ snapshots: dependencies: '@types/node': 18.19.39 + '@types/d3-array@3.2.1': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.10 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.10 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.14 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.6': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.10 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.14 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + '@types/d3-scale-chromatic@3.0.1': {} '@types/d3-scale@4.0.6': dependencies: '@types/d3-time': 3.0.2 + '@types/d3-selection@3.0.10': {} + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time-format@4.0.3': {} + '@types/d3-time@3.0.2': {} + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.8': + dependencies: + '@types/d3-selection': 3.0.10 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.10 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.0 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.6 + '@types/d3-scale-chromatic': 3.0.1 + '@types/d3-selection': 3.0.10 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.2 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.8 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.10': dependencies: '@types/ms': 0.7.33 @@ -7903,6 +8105,8 @@ snapshots: '@types/estree@1.0.5': {} + '@types/geojson@7946.0.14': {} + '@types/hast@2.3.7': dependencies: '@types/unist': 2.0.9 @@ -9039,7 +9243,7 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - d3@7.8.5: + d3@7.9.0: dependencies: d3-array: 3.2.4 d3-axis: 3.0.0 @@ -9079,7 +9283,7 @@ snapshots: dagre-d3-es@7.0.10: dependencies: - d3: 7.8.5 + d3: 7.9.0 lodash-es: 4.17.21 dargs@8.1.0: {} @@ -11129,7 +11333,7 @@ snapshots: cytoscape: 3.27.0 cytoscape-cose-bilkent: 4.1.0(cytoscape@3.27.0) cytoscape-fcose: 2.2.0(cytoscape@3.27.0) - d3: 7.8.5 + d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.10 dayjs: 1.11.10