diff --git a/.github/workflows/static-data.yml b/.github/workflows/static-data.yml index fbd323af4..b340d9718 100644 --- a/.github/workflows/static-data.yml +++ b/.github/workflows/static-data.yml @@ -35,6 +35,10 @@ jobs: run: pnpm --filter @verdaccio/local-scripts run downloads:npmjs - name: Get docker downloads run: pnpm --filter @verdaccio/local-scripts run pull:docker + - name: Get npmjs monhtly downloads + run: pnpm --filter @verdaccio/local-scripts run downloads:mounthly + - name: Get npmjs year downloads + run: pnpm --filter @verdaccio/local-scripts run downloads:yearly - name: update contributors run: pnpm run contributors env: diff --git a/packages/tools/local-scripts/package.json b/packages/tools/local-scripts/package.json index 44a33e2e8..83ec2b710 100644 --- a/packages/tools/local-scripts/package.json +++ b/packages/tools/local-scripts/package.json @@ -12,6 +12,8 @@ "start": "node build/index.js", "translations": "node build/run.js translations", "downloads:npmjs": "node build/run.js npmjs-api-download", + "downloads:monthly": "node build/run.js fetch-monthly-data", + "downloads:yearly": "node build/run.js fetch-yearly-data", "pull:docker": "node build/run.js docker-pull-api-download" }, "keywords": [], diff --git a/packages/tools/local-scripts/src/api/npmjsApiDownloadPoints.ts b/packages/tools/local-scripts/src/api/npmjsApiDownloadPoints.ts new file mode 100644 index 000000000..c176a5f87 --- /dev/null +++ b/packages/tools/local-scripts/src/api/npmjsApiDownloadPoints.ts @@ -0,0 +1,31 @@ +import { Command } from 'clipanion'; + +import { fetchMonthlyData, fetchYearlyData } from './utils'; + +export class FetchMonthlyDataCommand extends Command { + public static paths = [['fetch-monthly-data']]; + + public async execute() { + try { + await fetchMonthlyData(); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + } + } +} + +export class FetchYearlyDataCommand extends Command { + public static paths = [['fetch-yearly-data']]; + + public async execute() { + try { + await fetchYearlyData(); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + } + } +} diff --git a/packages/tools/local-scripts/src/api/utils.ts b/packages/tools/local-scripts/src/api/utils.ts index fa64d2be0..1b0217b50 100644 --- a/packages/tools/local-scripts/src/api/utils.ts +++ b/packages/tools/local-scripts/src/api/utils.ts @@ -6,12 +6,68 @@ import path from 'path'; const debug = require('debug')('verdaccio:local-scripts'); const token = process.env.TOKEN || ''; +const START_YEAR = 2016; +const END_YEAR = new Date().getFullYear(); +const END_MONTH = new Date().getMonth() + 1; +const API_URL = 'https://api.npmjs.org/downloads/point'; +const PACKAGE_NAME = 'verdaccio'; -const getISODateOnly = () => { +export const getISODateOnly = () => { const now = new Date(); return now.toISOString().split('T')[0]; }; +async function fetchDownloadData( + year: number, + month: number +): Promise<{ downloads: number; start: string; end: string; package: string } | null> { + const startDate = `${year}-${String(month).padStart(2, '0')}-01`; + const endDate = `${year}-${String(month).padStart(2, '0')}-${new Date(year, month, 0).getDate()}`; + debug('fetching data for %s to %s', startDate, endDate); + const url = `${API_URL}/${startDate}:${endDate}/${PACKAGE_NAME}`; + debug('fetching data from %s', url); + + try { + const response = await got.get(url).json(); + return response; + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to fetch data for ${startDate} to ${endDate}:`, error); + return null; + } +} + +export async function fetchMonthlyData() { + const npmjsFile = path.join(__dirname, '../../src/monthly_downloads.json'); + const results = []; + for (let year = START_YEAR; year <= END_YEAR; year++) { + for (let month = 1; month <= 12; month++) { + if (year === END_YEAR && month > END_MONTH) break; + debug('fetching data for %s %s', year, month); + const data = await fetchDownloadData(year, month); + if (data) results.push(data); + } + } + await fs.writeFile(npmjsFile, JSON.stringify(results)); + debug('Monthly data saved to monthly_downloads.json'); +} + +export async function fetchYearlyData() { + const npmjsFile = path.join(__dirname, '../../src/yearly_downloads.json'); + const results: { [key: string]: number } = {}; + for (let year = START_YEAR; year <= END_YEAR; year++) { + let yearlyTotal = 0; + for (let month = 1; month <= 12; month++) { + if (year === END_YEAR && month > END_MONTH) break; + const data = await fetchDownloadData(year, month); + if (data) yearlyTotal += data.downloads; + } + results[year.toString()] = yearlyTotal; + } + await fs.writeFile(npmjsFile, JSON.stringify(results)); + debug('Yearly data saved to yearly_downloads.json'); +} + export async function fetchNpmjsApiDownloadsWeekly() { try { const npmjsFile = path.join(__dirname, '../../src/npmjs_downloads.json'); @@ -30,7 +86,7 @@ export async function fetchNpmjsApiDownloadsWeekly() { npmjsDownloads[currentDate] = response.body.downloads; - await fs.writeFile(npmjsFile, JSON.stringify(npmjsDownloads, null, 4)); + await fs.writeFile(npmjsFile, JSON.stringify(npmjsDownloads)); debug('npmjs downloads written at %s ends', npmjsFile); } catch (err: any) { // eslint-disable-next-line no-console @@ -64,7 +120,7 @@ export async function dockerPullWeekly() { ipCount, }; }); - await fs.writeFile(npmjsFile, JSON.stringify(pullCounts, null, 4)); + await fs.writeFile(npmjsFile, JSON.stringify(pullCounts)); debug('npmjs downloads written at %s ends', npmjsFile); } catch (err: any) { // eslint-disable-next-line no-console @@ -88,9 +144,9 @@ export async function fetchTranslationsAPI() { return acc; }, {}); const location = path.join(__dirname, '../../src/progress_lang.json'); - await fs.writeFile(location, JSON.stringify(final, null, 4)); + await fs.writeFile(location, JSON.stringify(final)); // eslint-disable-next-line no-console - console.log('translations written at %s ends', location); + debug('translations written at %s ends', location); } catch (err: any) { // eslint-disable-next-line no-console console.error(`error on process crowdin translations run`, err); diff --git a/packages/tools/local-scripts/src/index.ts b/packages/tools/local-scripts/src/index.ts index cb40db3a8..233c7e873 100644 --- a/packages/tools/local-scripts/src/index.ts +++ b/packages/tools/local-scripts/src/index.ts @@ -2,5 +2,7 @@ const data = require('./progress_lang.json'); const translationsData = data; const npmjsDownloads = require('./npmjs_downloads.json'); const dockerPulls = require('./docker_pull.json'); +const yearlyDownloads = require('./yearly_downloads.json'); +const monthlyDownloads = require('./monthly_downloads.json'); -export { data, npmjsDownloads, dockerPulls, translationsData }; +export { data, npmjsDownloads, dockerPulls, translationsData, yearlyDownloads, monthlyDownloads }; diff --git a/packages/tools/local-scripts/src/monthly_downloads.json b/packages/tools/local-scripts/src/monthly_downloads.json new file mode 100644 index 000000000..77ebfe8ab --- /dev/null +++ b/packages/tools/local-scripts/src/monthly_downloads.json @@ -0,0 +1,112 @@ +[ + { "downloads": 0, "start": "2016-01-01", "end": "2016-01-31", "package": "verdaccio" }, + { "downloads": 0, "start": "2016-02-01", "end": "2016-02-29", "package": "verdaccio" }, + { "downloads": 0, "start": "2016-03-01", "end": "2016-03-31", "package": "verdaccio" }, + { "downloads": 0, "start": "2016-04-01", "end": "2016-04-30", "package": "verdaccio" }, + { "downloads": 0, "start": "2016-05-01", "end": "2016-05-31", "package": "verdaccio" }, + { "downloads": 0, "start": "2016-06-01", "end": "2016-06-30", "package": "verdaccio" }, + { "downloads": 31, "start": "2016-07-01", "end": "2016-07-31", "package": "verdaccio" }, + { "downloads": 180, "start": "2016-08-01", "end": "2016-08-31", "package": "verdaccio" }, + { "downloads": 255, "start": "2016-09-01", "end": "2016-09-30", "package": "verdaccio" }, + { "downloads": 434, "start": "2016-10-01", "end": "2016-10-31", "package": "verdaccio" }, + { "downloads": 395, "start": "2016-11-01", "end": "2016-11-30", "package": "verdaccio" }, + { "downloads": 332, "start": "2016-12-01", "end": "2016-12-31", "package": "verdaccio" }, + { "downloads": 1322, "start": "2017-01-01", "end": "2017-01-31", "package": "verdaccio" }, + { "downloads": 711, "start": "2017-02-01", "end": "2017-02-28", "package": "verdaccio" }, + { "downloads": 1940, "start": "2017-03-01", "end": "2017-03-31", "package": "verdaccio" }, + { "downloads": 1882, "start": "2017-04-01", "end": "2017-04-30", "package": "verdaccio" }, + { "downloads": 1889, "start": "2017-05-01", "end": "2017-05-31", "package": "verdaccio" }, + { "downloads": 2613, "start": "2017-06-01", "end": "2017-06-30", "package": "verdaccio" }, + { "downloads": 5099, "start": "2017-07-01", "end": "2017-07-31", "package": "verdaccio" }, + { "downloads": 3670, "start": "2017-08-01", "end": "2017-08-31", "package": "verdaccio" }, + { "downloads": 2923, "start": "2017-09-01", "end": "2017-09-30", "package": "verdaccio" }, + { "downloads": 5210, "start": "2017-10-01", "end": "2017-10-31", "package": "verdaccio" }, + { "downloads": 5100, "start": "2017-11-01", "end": "2017-11-30", "package": "verdaccio" }, + { "downloads": 5131, "start": "2017-12-01", "end": "2017-12-31", "package": "verdaccio" }, + { "downloads": 13745, "start": "2018-01-01", "end": "2018-01-31", "package": "verdaccio" }, + { "downloads": 8786, "start": "2018-02-01", "end": "2018-02-28", "package": "verdaccio" }, + { "downloads": 11528, "start": "2018-03-01", "end": "2018-03-31", "package": "verdaccio" }, + { "downloads": 7366, "start": "2018-04-01", "end": "2018-04-30", "package": "verdaccio" }, + { "downloads": 11966, "start": "2018-05-01", "end": "2018-05-31", "package": "verdaccio" }, + { "downloads": 10532, "start": "2018-06-01", "end": "2018-06-30", "package": "verdaccio" }, + { "downloads": 11541, "start": "2018-07-01", "end": "2018-07-31", "package": "verdaccio" }, + { "downloads": 13041, "start": "2018-08-01", "end": "2018-08-31", "package": "verdaccio" }, + { "downloads": 26650, "start": "2018-09-01", "end": "2018-09-30", "package": "verdaccio" }, + { "downloads": 34952, "start": "2018-10-01", "end": "2018-10-31", "package": "verdaccio" }, + { "downloads": 19367, "start": "2018-11-01", "end": "2018-11-30", "package": "verdaccio" }, + { "downloads": 34753, "start": "2018-12-01", "end": "2018-12-31", "package": "verdaccio" }, + { "downloads": 45819, "start": "2019-01-01", "end": "2019-01-31", "package": "verdaccio" }, + { "downloads": 41869, "start": "2019-02-01", "end": "2019-02-28", "package": "verdaccio" }, + { "downloads": 38958, "start": "2019-03-01", "end": "2019-03-31", "package": "verdaccio" }, + { "downloads": 123377, "start": "2019-04-01", "end": "2019-04-30", "package": "verdaccio" }, + { "downloads": 127805, "start": "2019-05-01", "end": "2019-05-31", "package": "verdaccio" }, + { "downloads": 132708, "start": "2019-06-01", "end": "2019-06-30", "package": "verdaccio" }, + { "downloads": 185707, "start": "2019-07-01", "end": "2019-07-31", "package": "verdaccio" }, + { "downloads": 148043, "start": "2019-08-01", "end": "2019-08-31", "package": "verdaccio" }, + { "downloads": 136353, "start": "2019-09-01", "end": "2019-09-30", "package": "verdaccio" }, + { "downloads": 147244, "start": "2019-10-01", "end": "2019-10-31", "package": "verdaccio" }, + { "downloads": 60357, "start": "2019-11-01", "end": "2019-11-30", "package": "verdaccio" }, + { "downloads": 52255, "start": "2019-12-01", "end": "2019-12-31", "package": "verdaccio" }, + { "downloads": 62815, "start": "2020-01-01", "end": "2020-01-31", "package": "verdaccio" }, + { "downloads": 59476, "start": "2020-02-01", "end": "2020-02-29", "package": "verdaccio" }, + { "downloads": 73003, "start": "2020-03-01", "end": "2020-03-31", "package": "verdaccio" }, + { "downloads": 77269, "start": "2020-04-01", "end": "2020-04-30", "package": "verdaccio" }, + { "downloads": 92001, "start": "2020-05-01", "end": "2020-05-31", "package": "verdaccio" }, + { "downloads": 94261, "start": "2020-06-01", "end": "2020-06-30", "package": "verdaccio" }, + { "downloads": 102521, "start": "2020-07-01", "end": "2020-07-31", "package": "verdaccio" }, + { "downloads": 93523, "start": "2020-08-01", "end": "2020-08-31", "package": "verdaccio" }, + { "downloads": 108664, "start": "2020-09-01", "end": "2020-09-30", "package": "verdaccio" }, + { "downloads": 123953, "start": "2020-10-01", "end": "2020-10-31", "package": "verdaccio" }, + { "downloads": 125049, "start": "2020-11-01", "end": "2020-11-30", "package": "verdaccio" }, + { "downloads": 106645, "start": "2020-12-01", "end": "2020-12-31", "package": "verdaccio" }, + { "downloads": 112861, "start": "2021-01-01", "end": "2021-01-31", "package": "verdaccio" }, + { "downloads": 139975, "start": "2021-02-01", "end": "2021-02-28", "package": "verdaccio" }, + { "downloads": 167038, "start": "2021-03-01", "end": "2021-03-31", "package": "verdaccio" }, + { "downloads": 177302, "start": "2021-04-01", "end": "2021-04-30", "package": "verdaccio" }, + { "downloads": 151617, "start": "2021-05-01", "end": "2021-05-31", "package": "verdaccio" }, + { "downloads": 134956, "start": "2021-06-01", "end": "2021-06-30", "package": "verdaccio" }, + { "downloads": 125070, "start": "2021-07-01", "end": "2021-07-31", "package": "verdaccio" }, + { "downloads": 225969, "start": "2021-08-01", "end": "2021-08-31", "package": "verdaccio" }, + { "downloads": 234809, "start": "2021-09-01", "end": "2021-09-30", "package": "verdaccio" }, + { "downloads": 293088, "start": "2021-10-01", "end": "2021-10-31", "package": "verdaccio" }, + { "downloads": 251822, "start": "2021-11-01", "end": "2021-11-30", "package": "verdaccio" }, + { "downloads": 181139, "start": "2021-12-01", "end": "2021-12-31", "package": "verdaccio" }, + { "downloads": 248329, "start": "2022-01-01", "end": "2022-01-31", "package": "verdaccio" }, + { "downloads": 280671, "start": "2022-02-01", "end": "2022-02-28", "package": "verdaccio" }, + { "downloads": 351946, "start": "2022-03-01", "end": "2022-03-31", "package": "verdaccio" }, + { "downloads": 312241, "start": "2022-04-01", "end": "2022-04-30", "package": "verdaccio" }, + { "downloads": 323348, "start": "2022-05-01", "end": "2022-05-31", "package": "verdaccio" }, + { "downloads": 375749, "start": "2022-06-01", "end": "2022-06-30", "package": "verdaccio" }, + { "downloads": 370502, "start": "2022-07-01", "end": "2022-07-31", "package": "verdaccio" }, + { "downloads": 501449, "start": "2022-08-01", "end": "2022-08-31", "package": "verdaccio" }, + { "downloads": 568526, "start": "2022-09-01", "end": "2022-09-30", "package": "verdaccio" }, + { "downloads": 487293, "start": "2022-10-01", "end": "2022-10-31", "package": "verdaccio" }, + { "downloads": 468798, "start": "2022-11-01", "end": "2022-11-30", "package": "verdaccio" }, + { "downloads": 434725, "start": "2022-12-01", "end": "2022-12-31", "package": "verdaccio" }, + { "downloads": 479056, "start": "2023-01-01", "end": "2023-01-31", "package": "verdaccio" }, + { "downloads": 483402, "start": "2023-02-01", "end": "2023-02-28", "package": "verdaccio" }, + { "downloads": 567208, "start": "2023-03-01", "end": "2023-03-31", "package": "verdaccio" }, + { "downloads": 468070, "start": "2023-04-01", "end": "2023-04-30", "package": "verdaccio" }, + { "downloads": 453703, "start": "2023-05-01", "end": "2023-05-31", "package": "verdaccio" }, + { "downloads": 471742, "start": "2023-06-01", "end": "2023-06-30", "package": "verdaccio" }, + { "downloads": 478651, "start": "2023-07-01", "end": "2023-07-31", "package": "verdaccio" }, + { "downloads": 522940, "start": "2023-08-01", "end": "2023-08-31", "package": "verdaccio" }, + { "downloads": 462706, "start": "2023-09-01", "end": "2023-09-30", "package": "verdaccio" }, + { "downloads": 578650, "start": "2023-10-01", "end": "2023-10-31", "package": "verdaccio" }, + { "downloads": 598748, "start": "2023-11-01", "end": "2023-11-30", "package": "verdaccio" }, + { "downloads": 514988, "start": "2023-12-01", "end": "2023-12-31", "package": "verdaccio" }, + { "downloads": 709529, "start": "2024-01-01", "end": "2024-01-31", "package": "verdaccio" }, + { "downloads": 628021, "start": "2024-02-01", "end": "2024-02-29", "package": "verdaccio" }, + { "downloads": 674788, "start": "2024-03-01", "end": "2024-03-31", "package": "verdaccio" }, + { "downloads": 708686, "start": "2024-04-01", "end": "2024-04-30", "package": "verdaccio" }, + { "downloads": 738101, "start": "2024-05-01", "end": "2024-05-31", "package": "verdaccio" }, + { "downloads": 684335, "start": "2024-06-01", "end": "2024-06-30", "package": "verdaccio" }, + { "downloads": 752409, "start": "2024-07-01", "end": "2024-07-31", "package": "verdaccio" }, + { "downloads": 812871, "start": "2024-08-01", "end": "2024-08-31", "package": "verdaccio" }, + { "downloads": 774378, "start": "2024-09-01", "end": "2024-09-30", "package": "verdaccio" }, + { "downloads": 895188, "start": "2024-10-01", "end": "2024-10-31", "package": "verdaccio" }, + { "downloads": 890807, "start": "2024-11-01", "end": "2024-11-30", "package": "verdaccio" }, + { "downloads": 825901, "start": "2024-12-01", "end": "2024-12-31", "package": "verdaccio" }, + { "downloads": 876894, "start": "2025-01-01", "end": "2025-01-31", "package": "verdaccio" }, + { "downloads": 0, "start": "2025-02-01", "end": "2025-02-03", "package": "verdaccio" } +] diff --git a/packages/tools/local-scripts/src/run.ts b/packages/tools/local-scripts/src/run.ts index a584fa570..b6da65795 100644 --- a/packages/tools/local-scripts/src/run.ts +++ b/packages/tools/local-scripts/src/run.ts @@ -2,6 +2,7 @@ import { Cli } from 'clipanion'; import { DockerPullCommand } from './api/dockerPullCommand'; import { NpmjsApiDownloadCommand } from './api/npmjsApiDownloadCommand'; +import { FetchMonthlyDataCommand, FetchYearlyDataCommand } from './api/npmjsApiDownloadPoints'; import { TranslationsApiCommand } from './api/translationsCommand'; const [node, app, ...args] = process.argv; @@ -15,6 +16,8 @@ const cli = new Cli({ cli.register(TranslationsApiCommand); cli.register(NpmjsApiDownloadCommand); cli.register(DockerPullCommand); +cli.register(FetchMonthlyDataCommand); +cli.register(FetchYearlyDataCommand); cli.runExit(args, Cli.defaultContext); process.on('uncaughtException', function (err) { diff --git a/packages/tools/local-scripts/src/yearly_downloads.json b/packages/tools/local-scripts/src/yearly_downloads.json new file mode 100644 index 000000000..9d0748a20 --- /dev/null +++ b/packages/tools/local-scripts/src/yearly_downloads.json @@ -0,0 +1,12 @@ +{ + "2016": 1627, + "2017": 37490, + "2018": 204227, + "2019": 1240495, + "2020": 1119180, + "2021": 2195646, + "2022": 4723577, + "2023": 6079864, + "2024": 9095014, + "2025": 876894 +} diff --git a/website/src/components/Chart/MonhtlyNpmjsDownloadsChart.tsx b/website/src/components/Chart/MonhtlyNpmjsDownloadsChart.tsx new file mode 100644 index 000000000..1ab639256 --- /dev/null +++ b/website/src/components/Chart/MonhtlyNpmjsDownloadsChart.tsx @@ -0,0 +1,54 @@ +import { + CategoryScale, + Chart as ChartJS, + Legend, + LineElement, + LinearScale, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import React from 'react'; +import { Line } from 'react-chartjs-2'; + +import { monthlyDownloads } from '@verdaccio/local-scripts'; + +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); + +const data = { + labels: monthlyDownloads.map((entry) => entry.start), + datasets: [ + { + label: 'Npmjs Monthly Downloads', + data: monthlyDownloads.map((entry) => entry.downloads), + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + fill: false, + tension: 0.1, + }, + ], +}; + +const options = { + responsive: true, + scales: { + x: { + title: { + display: true, + text: 'Month', + }, + }, + y: { + title: { + display: true, + text: 'Downloads', + }, + }, + }, +}; + +const NpmjsMonthlyDownloadsChart = () => { + return ; +}; + +export default NpmjsMonthlyDownloadsChart; diff --git a/website/src/components/Chart/YearlyNpmjsDownloadsChart.tsx b/website/src/components/Chart/YearlyNpmjsDownloadsChart.tsx new file mode 100644 index 000000000..12d8e6065 --- /dev/null +++ b/website/src/components/Chart/YearlyNpmjsDownloadsChart.tsx @@ -0,0 +1,52 @@ +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + Title, + Tooltip, +} from 'chart.js'; +import React from 'react'; +import { Bar } from 'react-chartjs-2'; + +import { yearlyDownloads } from '@verdaccio/local-scripts'; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + +const data = { + labels: Object.keys(yearlyDownloads), + datasets: [ + { + label: 'Npmjs Yearly Downloads', + data: Object.values(yearlyDownloads), + backgroundColor: 'rgba(75, 192, 192, 0.5)', + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 1, + }, + ], +}; + +const options = { + responsive: true, + scales: { + x: { + title: { + display: true, + text: 'Year', + }, + }, + y: { + title: { + display: true, + text: 'Downloads', + }, + }, + }, +}; + +const NpmjsYearlyDownloadsChart = () => { + return ; +}; + +export default NpmjsYearlyDownloadsChart; diff --git a/website/src/components/Downloads.tsx b/website/src/components/Downloads.tsx index 68a0ceddf..4b6eca0b3 100644 --- a/website/src/components/Downloads.tsx +++ b/website/src/components/Downloads.tsx @@ -8,8 +8,10 @@ import React from 'react'; import DockerPullChart from './Chart/DockerPullChart'; import DockerTotalPull from './Chart/DockerTotalPull'; +import NpmjsMonthlyDownloadsChart from './Chart/MonhtlyNpmjsDownloadsChart'; import NpmjsVersionsChart from './Chart/NpmjsVersionsChart'; import VersionDownloadsChart from './Chart/VersionDownloadsChart'; +import NpmjsYearlyDownloadsChart from './Chart/YearlyNpmjsDownloadsChart'; const theme = createTheme({ palette: { @@ -57,6 +59,12 @@ const Downloads: React.FC<{}> = (): React.ReactElement => {
+
+ +
+
+ +