Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc9c926800 | ||
|
|
986eaad1a8 | ||
|
|
5403eefbd3 | ||
|
|
12b27566be | ||
|
|
40097713b9 | ||
|
|
2168c1f91e | ||
|
|
fa82216dfe | ||
|
|
9cf716c85f | ||
|
|
ef3f207dbf | ||
|
|
35fbec8004 | ||
|
|
3ef27f872c | ||
|
|
9f4ad921aa | ||
|
|
5557e9c772 | ||
|
|
570fc4aaed | ||
|
|
b6f4183ddd | ||
|
|
2ac454a177 | ||
|
|
b837c50885 | ||
|
|
73189400ee | ||
|
|
8a239313d3 | ||
|
|
a3dbb37dd2 | ||
|
|
9c4e631bc7 | ||
|
|
6ebd9a082f | ||
|
|
9236acd99d | ||
|
|
12b4e979a8 | ||
|
|
5c4ea2a797 | ||
|
|
7d666e2510 | ||
|
|
96a18562f0 | ||
|
|
b64d601d47 | ||
|
|
26aec2a4af | ||
|
|
ad23ab39ed | ||
|
|
d982bef3c5 | ||
|
|
2463f0900d | ||
|
|
23ba727b9a | ||
|
|
6c625fb76b | ||
|
|
0aafbbbdb5 | ||
|
|
8e6e0fac47 |
@@ -14,11 +14,11 @@ runs:
|
||||
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
|
||||
- name: Setup Node.js (via input)
|
||||
if: ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: "pnpm"
|
||||
@@ -26,7 +26,7 @@ runs:
|
||||
|
||||
- name: Setup Node.js (via .nvmrc)
|
||||
if: ${{ !inputs.node-version }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# * * * * *
|
||||
- cron: "45 * * * *"
|
||||
- cron: "15,45 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"yzhang.markdown-all-in-one",
|
||||
"prettier.prettier-vscode",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"expiration": 39600,
|
||||
"expiration": 255600,
|
||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||
"passQuery": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"expiration": 39600,
|
||||
"expiration": 255600,
|
||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||
"passQuery": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"expiration": 39600,
|
||||
"expiration": 255600,
|
||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||
"passQuery": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"expiration": 39600,
|
||||
"expiration": 255600,
|
||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||
"passQuery": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"expiration": 39600,
|
||||
"expiration": 255600,
|
||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||
"passQuery": true
|
||||
}
|
||||
|
||||
@@ -66,6 +66,6 @@ export async function repeatRecentRequests() {
|
||||
if (urls.length === 0) {
|
||||
console.log("No recent requests found.");
|
||||
} else {
|
||||
await makeRequests(urls, 5);
|
||||
await makeRequests(urls, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const STATS_MOCK_RESPONSE = {
|
||||
followers: { totalCount: 0 },
|
||||
repositories: {
|
||||
totalCount: 1,
|
||||
nodes: [{ name: REPO, stargazers: { totalCount: 1 } }],
|
||||
nodes: [{ name: REPO, stargazerCount: 1 }],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
endCursor: "cursor",
|
||||
@@ -110,9 +110,7 @@ const REPO_MOCK_RESPONSE = {
|
||||
isPrivate: false,
|
||||
isArchived: false,
|
||||
isTemplate: false,
|
||||
stargazers: {
|
||||
totalCount: 1,
|
||||
},
|
||||
stargazerCount: 1,
|
||||
description: "Simple cra test repo.",
|
||||
primaryLanguage: {
|
||||
color: "#f1e05a",
|
||||
|
||||
@@ -443,7 +443,7 @@ exports[`Test /api contract > should match the public many-params response snaps
|
||||
|
||||
</g>
|
||||
</svg>",
|
||||
"graphqlRequest": "{"query":"\\n query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection (from: $startTime) {\\n totalCommitContributions,\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n \\n repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {\\n totalCount\\n nodes {\\n name\\n stargazers {\\n totalCount\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n\\n }\\n }\\n","variables":{"login":"anuraghazra","first":100,"after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
|
||||
"graphqlRequest": "{"query":"\\n query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection (from: $startTime) {\\n totalCommitContributions,\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n \\n repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {\\n totalCount\\n nodes {\\n name\\n stargazerCount\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n\\n }\\n }\\n","variables":{"login":"anuraghazra","first":100,"after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
|
||||
"headers": [
|
||||
[
|
||||
"Cache-Control",
|
||||
|
||||
@@ -341,7 +341,7 @@ exports[`Test /api/pin contract > should match the public many-params response s
|
||||
|
||||
</g>
|
||||
</svg>",
|
||||
"graphqlRequest": "{"query":"\\n fragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazers {\\n totalCount\\n }\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n }\\n query getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","repo":"convoychat"}}",
|
||||
"graphqlRequest": "{"query":"\\n fragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazerCount\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n }\\n query getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","repo":"convoychat"}}",
|
||||
"headers": [
|
||||
[
|
||||
"Cache-Control",
|
||||
|
||||
@@ -26,9 +26,9 @@ export const data_stats = {
|
||||
repositories: {
|
||||
totalCount: 3,
|
||||
nodes: [
|
||||
{ name: "repo-keep-1", stargazers: { totalCount: 1500 } },
|
||||
{ name: "repo-exclude-me", stargazers: { totalCount: 9999 } },
|
||||
{ name: "repo-keep-2", stargazers: { totalCount: 2600 } },
|
||||
{ name: "repo-keep-1", stargazerCount: 1500 },
|
||||
{ name: "repo-exclude-me", stargazerCount: 9999 },
|
||||
{ name: "repo-keep-2", stargazerCount: 2600 },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
@@ -73,9 +73,7 @@ export const data_user = {
|
||||
username: "anuraghazra",
|
||||
name: "convoychat",
|
||||
nameWithOwner: "anuraghazra/convoychat",
|
||||
stargazers: {
|
||||
totalCount: 38000,
|
||||
},
|
||||
stargazerCount: 38000,
|
||||
description:
|
||||
"Help us take over the world with a deeply customizable React, TypeScript and GraphQL chat app that has enough text to wrap across multiple lines in the repository card.",
|
||||
primaryLanguage: {
|
||||
|
||||
@@ -12,8 +12,8 @@ test("load initial page correctly", async ({ page }) => {
|
||||
// Logo exists
|
||||
await expect(page.locator('img[alt="logo"]')).toBeVisible();
|
||||
|
||||
// Star on GitHub button
|
||||
const starButton = page.getByRole("button", { name: /star on/i });
|
||||
// Star on GitHub link
|
||||
const starButton = page.getByRole("link", { name: /star on/i });
|
||||
await expect(starButton).toBeVisible();
|
||||
|
||||
const githubLink = page.locator(
|
||||
@@ -39,6 +39,45 @@ test("load initial page correctly", async ({ page }) => {
|
||||
await expect(guestBtn).toBeEnabled();
|
||||
});
|
||||
|
||||
test("theme picker switches and persists the theme", async ({ page }) => {
|
||||
await page.goto("");
|
||||
|
||||
const html = page.locator("html");
|
||||
const trigger = page.getByRole("button", { name: /choose theme/i });
|
||||
const options = page.getByRole("menuitemradio");
|
||||
|
||||
// The menu is closed initially.
|
||||
await expect(trigger).toBeVisible();
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(options).toHaveCount(0);
|
||||
|
||||
// Opening it reveals the options (portaled to body, above the stepper).
|
||||
await trigger.click();
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||
|
||||
// Read the themes off the options instead of hardcoding their names.
|
||||
const firstTheme = await options.first().getAttribute("data-theme-value");
|
||||
const lastTheme = await options.last().getAttribute("data-theme-value");
|
||||
|
||||
// Selecting an option applies the theme, persists it, and closes the menu.
|
||||
await options.last().click();
|
||||
await expect(html).toHaveAttribute("data-theme", lastTheme ?? "");
|
||||
await expect(options).toHaveCount(0);
|
||||
await expect(
|
||||
page.evaluate(() => localStorage.getItem("theme")),
|
||||
).resolves.toBe(lastTheme);
|
||||
|
||||
// The choice survives a reload and is marked as selected on reopen.
|
||||
await page.reload();
|
||||
await expect(html).toHaveAttribute("data-theme", lastTheme ?? "");
|
||||
await trigger.click();
|
||||
await expect(options.last()).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
// Switching to another option works too.
|
||||
await options.first().click();
|
||||
await expect(html).toHaveAttribute("data-theme", firstTheme ?? "");
|
||||
});
|
||||
|
||||
test("navigates between steps", async ({ page }) => {
|
||||
await page.goto("");
|
||||
|
||||
|
||||
@@ -37,6 +37,24 @@
|
||||
<title>GitHub Stats Extended</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Apply the persisted theme from localStorage before first paint. This is
|
||||
// what makes a saved theme survive a reload when it differs from the OS
|
||||
// preference (DaisyUI's `--prefersdark` only covers the OS default, and
|
||||
// nothing in React sets `data-theme` on mount).
|
||||
// Also suppress color transitions until after the first paint so elements
|
||||
// don't animate from their unstyled state on load.
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem("theme");
|
||||
const matchesDarkTheme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
var theme = stored || (matchesDarkTheme ? "dark" : "light");
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { clsx } from "clsx";
|
||||
import type { JSX } from "react";
|
||||
import type { CSSProperties, JSX } from "react";
|
||||
|
||||
import { CardImage } from "./CardImage";
|
||||
import { LIGHT_CARD_BG } from "./themeBackdrop";
|
||||
|
||||
interface CardProps {
|
||||
title: string;
|
||||
@@ -11,6 +12,10 @@ interface CardProps {
|
||||
selected?: boolean;
|
||||
compact?: boolean;
|
||||
fixedSize?: boolean;
|
||||
/** CSS background for the card surface (e.g. a theme's own background). */
|
||||
backgroundColor?: string;
|
||||
/** Title color, used when the card sits on a custom background. */
|
||||
titleColor?: string;
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
@@ -21,16 +26,46 @@ export const Card = ({
|
||||
selected = false,
|
||||
compact = false,
|
||||
fixedSize = false,
|
||||
backgroundColor,
|
||||
titleColor,
|
||||
}: CardProps): JSX.Element => {
|
||||
const customBackground = backgroundColor
|
||||
? selected
|
||||
? `color-mix(in oklab, var(--color-primary) ${backgroundColor === LIGHT_CARD_BG ? "10%" : "25%"}, ${backgroundColor})`
|
||||
: backgroundColor
|
||||
: undefined;
|
||||
|
||||
const cardStyle = customBackground
|
||||
? ({ "--card-bg": customBackground } as CSSProperties)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("p-6 rounded border-2", {
|
||||
className={clsx("p-6 rounded border-4", {
|
||||
"h-[370px] w-[510px]": fixedSize,
|
||||
"border-blue-500 bg-blue-50": selected,
|
||||
"border-gray-200 bg-white hover:bg-gray-50": !selected,
|
||||
"border-primary": selected,
|
||||
"border-base-300": !selected,
|
||||
"bg-[var(--card-bg)]": !!customBackground,
|
||||
// Token backgrounds only apply when no custom background is given.
|
||||
"bg-primary/10": selected && !customBackground,
|
||||
"bg-base-100 hover:bg-base-200": !selected && !customBackground,
|
||||
"hover:bg-base-300": !selected && !!customBackground,
|
||||
})}
|
||||
style={cardStyle}
|
||||
data-theme={
|
||||
stage == 3
|
||||
? backgroundColor === LIGHT_CARD_BG || !backgroundColor
|
||||
? "light"
|
||||
: "dark"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<h2 className="text-xl font-medium title-font text-gray-900">{title}</h2>
|
||||
<h2
|
||||
className="text-xl font-medium title-font text-base-content"
|
||||
style={titleColor ? { color: titleColor } : undefined}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-base leading-relaxed mt-2 mb-4">{description}</p>
|
||||
<CardImage
|
||||
imageSrc={imageSrc}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
|
||||
FETCH_MULTI_PAGE_STARS: "10",
|
||||
PAT_1: userToken as string, // even if it's null, core's retryer.js sees there is 1 PAT and sets `RETRIES` accordingly
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
|
||||
loadConfigFromEnv(config);
|
||||
|
||||
setLoaded(false);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { themes } from "@stats-organization/github-readme-stats-core";
|
||||
|
||||
// We use the same background colors here which GitHub uses.
|
||||
export const LIGHT_CARD_BG = "#ffffff";
|
||||
const DARK_CARD_BG = "#0d1117";
|
||||
|
||||
// Themes that look good in either mode (their text/icon colors read on both a
|
||||
// light and a dark backdrop), so their backdrop follows the app theme. Note we
|
||||
// only list themes that are genuinely mode-agnostic here: other transparent
|
||||
// themes (e.g. shadow_*) have dark text and must stay on a light backdrop.
|
||||
const ADAPTIVE_THEMES = ["transparent", "ambient_gradient"];
|
||||
|
||||
/** Expand a shorthand `#abc` hex to `#aabbcc`. */
|
||||
function expandHex(hex: string): string {
|
||||
if (hex.length === 3) {
|
||||
return hex
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.join("");
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
/** Whether a hex color (3-, 6- or 8-digit) is dark by perceived luminance. */
|
||||
function isDarkHex(hex: string): boolean {
|
||||
const normalized = expandHex(hex);
|
||||
if (normalized.length < 6) {
|
||||
return false;
|
||||
}
|
||||
const r = parseInt(normalized.slice(0, 2), 16);
|
||||
const g = parseInt(normalized.slice(2, 4), 16);
|
||||
const b = parseInt(normalized.slice(4, 6), 16);
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
return luminance < 0.5;
|
||||
}
|
||||
|
||||
/** Whether a theme's `bg_color` (hex or `angle,c1,c2,…` gradient) is dark. */
|
||||
function isDarkBg(bgColor: string): boolean {
|
||||
const parts = bgColor.split(",");
|
||||
// For gradients use the first color stop (index 0 is the angle).
|
||||
return isDarkHex((parts.length > 1 ? parts[1] : parts[0]) ?? "");
|
||||
}
|
||||
|
||||
/** A theme whose backdrop should follow the app mode rather than its own bg. */
|
||||
function isAdaptiveTheme(themeName: string): boolean {
|
||||
return ADAPTIVE_THEMES.includes(themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the backdrop a card actually gets is dark, for the given app mode.
|
||||
* Adaptive themes follow the app mode; the rest follow their own background.
|
||||
*/
|
||||
function isCardBackdropDark(themeName: string, appIsDark: boolean): boolean {
|
||||
const theme = themes[themeName as keyof typeof themes];
|
||||
if (isAdaptiveTheme(themeName)) {
|
||||
return appIsDark;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!theme) {
|
||||
return appIsDark;
|
||||
}
|
||||
return isDarkBg(theme.bg_color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort rank for the theme grid: light themes first (0), adaptive themes in the
|
||||
* middle (1), dark themes last (2). Adaptive cards sit between the groups so
|
||||
* they blend with whichever side matches their backdrop in the current mode.
|
||||
*/
|
||||
export function getThemeSortRank(themeName: string): number {
|
||||
if (isAdaptiveTheme(themeName)) {
|
||||
return 1;
|
||||
}
|
||||
const theme = themes[themeName as keyof typeof themes];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!theme) {
|
||||
return 0;
|
||||
}
|
||||
return isDarkBg(theme.bg_color) ? 2 : 0;
|
||||
}
|
||||
|
||||
/** The backdrop color a card rendered with the given theme should sit on. */
|
||||
export function getCardThemeBackdrop(
|
||||
themeName: string,
|
||||
appIsDark: boolean,
|
||||
): string {
|
||||
return isCardBackdropDark(themeName, appIsDark)
|
||||
? DARK_CARD_BG
|
||||
: LIGHT_CARD_BG;
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
import { clsx } from "clsx";
|
||||
import type { HTMLProps, JSX, ReactNode } from "react";
|
||||
|
||||
interface ButtonProps extends HTMLProps<HTMLButtonElement> {
|
||||
type ButtonVariant = "primary" | "soft" | "error";
|
||||
type ButtonSize = "sm" | "md" | "lg";
|
||||
|
||||
interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, "size"> {
|
||||
children: ReactNode;
|
||||
/** DaisyUI color variant. Omit for the default neutral button. */
|
||||
variant?: ButtonVariant | undefined;
|
||||
size?: ButtonSize;
|
||||
}
|
||||
|
||||
export function Button(props: ButtonProps): JSX.Element {
|
||||
const { className, children, ...rest } = props;
|
||||
const { className, children, variant, size = "md", ...rest } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"border-0 py-2 px-6 inline-flex focus:outline-none rounded-[0.25rem] text-lg",
|
||||
"btn text-lg",
|
||||
{
|
||||
"btn-primary": variant === "primary",
|
||||
"btn-soft": variant === "soft",
|
||||
"btn-error": variant === "error",
|
||||
"btn-sm": size === "sm",
|
||||
"btn-lg": size === "lg",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function Checkbox({
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
className="checkbox bg-white text-[#18181b] mr-2"
|
||||
className="checkbox checkbox-primary mr-2"
|
||||
onChange={() => {
|
||||
onCheckedChange(!checked);
|
||||
}}
|
||||
|
||||
@@ -27,10 +27,7 @@ export function Select({
|
||||
}: SelectProps): JSX.Element {
|
||||
return (
|
||||
<select
|
||||
className={clsx(
|
||||
"text-gray-700 text-base bg-white select select-sm w-40 rounded-sm mt-4",
|
||||
className,
|
||||
)}
|
||||
className={clsx("text-base select select-sm w-40 mt-4", className)}
|
||||
value={selectedOption.value}
|
||||
onChange={(e) => {
|
||||
onOptionChange(options[e.target.selectedIndex] as SelectOption);
|
||||
@@ -43,7 +40,7 @@ export function Select({
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
className={clsx({
|
||||
"bg-blue-200": option.value === selectedOption.value,
|
||||
"bg-primary/20": option.value === selectedOption.value,
|
||||
})}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
FaCheck as CheckIcon,
|
||||
FaMoon as MoonIcon,
|
||||
FaSun as SunIcon,
|
||||
} from "react-icons/fa";
|
||||
|
||||
import { useTheme } from "../../redux/selectors/themeSelectors";
|
||||
import { THEMES } from "../../redux/slices/theme";
|
||||
|
||||
interface ThemeIconProps {
|
||||
isDark: boolean;
|
||||
className: string;
|
||||
}
|
||||
|
||||
/** Sun for light themes, moon for dark ones. */
|
||||
function ThemeIcon({ isDark, className }: ThemeIconProps): JSX.Element {
|
||||
return isDark ? (
|
||||
<MoonIcon className={className} />
|
||||
) : (
|
||||
<SunIcon className={className} />
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemePicker(): JSX.Element {
|
||||
const { theme, isDark, setTheme } = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, right: 0 });
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (!open && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setPosition({
|
||||
top: rect.bottom + 8,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}
|
||||
setOpen((value) => !value);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (
|
||||
!menuRef.current?.contains(target) &&
|
||||
!buttonRef.current?.contains(target)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("resize", close);
|
||||
// Close on scroll so the fixed menu can't detach from its button.
|
||||
window.addEventListener("scroll", close, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("resize", close);
|
||||
window.removeEventListener("scroll", close, true);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
className="btn btn-ghost btn-circle"
|
||||
aria-label="Choose theme"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
title="Choose theme"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ThemeIcon isDark={isDark} className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{open &&
|
||||
createPortal(
|
||||
<ul
|
||||
ref={menuRef}
|
||||
role="menu"
|
||||
className="menu bg-base-200 text-base-content rounded-box fixed z-[9999] w-48 p-2 shadow-lg"
|
||||
style={{ top: position.top, right: position.right }}
|
||||
>
|
||||
{THEMES.map((option) => (
|
||||
<li key={option.name}>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={option.name === theme}
|
||||
data-theme-value={option.name}
|
||||
className="flex items-center justify-between"
|
||||
onClick={() => {
|
||||
setTheme(option.name);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<ThemeIcon isDark={option.isDark} className="w-4 h-4" />
|
||||
{option.label}
|
||||
</span>
|
||||
{option.name === theme && <CheckIcon className="w-3 h-3" />}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export function NumericSection({
|
||||
<p>{description}</p>
|
||||
<input
|
||||
type="number"
|
||||
className="border border-gray-300 bg-white rounded px-2 py-1 mt-2 w-1/4"
|
||||
className="border border-base-content/20 rounded px-2 py-1 mt-2 w-1/4 bg-base-100"
|
||||
value={internalValue ?? ""}
|
||||
onChange={(e) => {
|
||||
setInternalValue(e.target.value);
|
||||
|
||||
@@ -24,7 +24,7 @@ function ProgressSection({
|
||||
<button
|
||||
className={clsx(
|
||||
"w-1/4 flex flex-col mx-2 p-2 cursor-pointer",
|
||||
passed ? "border-blue-500" : "border-gray-500",
|
||||
passed ? "border-primary" : "border-base-content/60",
|
||||
isActive ? "border-t-[14px] -mt-[5px]" : "border-t-4",
|
||||
)}
|
||||
type="button"
|
||||
@@ -32,14 +32,16 @@ function ProgressSection({
|
||||
>
|
||||
<div
|
||||
className={clsx("text-lg font-bold", {
|
||||
"text-blue-500": passed,
|
||||
"text-gray-500": !passed,
|
||||
"text-primary": passed,
|
||||
"text-base-content/60": !passed,
|
||||
"-mt-[4px]": isActive,
|
||||
})}
|
||||
>
|
||||
{`Step ${num + 1}`}
|
||||
</div>
|
||||
<div className={passed ? "text-gray-700" : "text-gray-500"}>{item}</div>
|
||||
<div className={passed ? "text-base-content/80" : "text-base-content/60"}>
|
||||
{item}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -59,16 +61,23 @@ export function ProgressBar({
|
||||
const rightDisabled = currItemIndex === items.length - 1;
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center sticky top-0 bg-gray-200 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
|
||||
<LeftArrowIcon
|
||||
className={clsx("w-8 h-8", {
|
||||
"text-gray-400 cursor-not-allowed": leftDisabled,
|
||||
"text-gray-700 cursor-pointer": !leftDisabled,
|
||||
})}
|
||||
<div className="w-full flex items-center sticky top-0 bg-base-300 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Previous step"
|
||||
disabled={leftDisabled}
|
||||
className="btn btn-ghost btn-circle"
|
||||
onClick={() => {
|
||||
onItemClick(Math.max(currItemIndex - 1, 0));
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<LeftArrowIcon
|
||||
className={clsx("w-8 h-8", {
|
||||
"text-base-content/30": leftDisabled,
|
||||
"text-base-content/70": !leftDisabled,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<div className="px-2 flex-grow flex flex-row">
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
@@ -85,15 +94,22 @@ export function ProgressBar({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<RightArrowIcon
|
||||
className={clsx("w-8 h-8", {
|
||||
"text-gray-400 cursor-not-allowed": rightDisabled,
|
||||
"text-gray-700 cursor-pointer": !rightDisabled,
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Next step"
|
||||
disabled={rightDisabled}
|
||||
className="btn btn-ghost btn-circle"
|
||||
onClick={() => {
|
||||
onItemClick(Math.min(currItemIndex + 1, items.length - 1));
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<RightArrowIcon
|
||||
className={clsx("w-8 h-8", {
|
||||
"text-base-content/30": rightDisabled,
|
||||
"text-base-content/70": !rightDisabled,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ export function Section({ title, children }: SectionProps): JSX.Element {
|
||||
return (
|
||||
<div className="flex relative pb-12">
|
||||
<div className="h-full w-10 absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-full w-1 bg-gray-200 pointer-events-none" />
|
||||
<div className="h-full w-1 bg-base-300 pointer-events-none" />
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-blue-500 inline-flex items-center justify-center text-white relative z-10">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary inline-flex items-center justify-center text-primary-content relative z-10">
|
||||
<LightningIcon className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-grow pl-4">
|
||||
<h2 className="font-medium title-font text-sm text-gray-900 mb-1 tracking-wider">
|
||||
<h2 className="font-medium title-font text-sm text-base-content mb-1 tracking-wider">
|
||||
{title}
|
||||
</h2>
|
||||
{children}
|
||||
|
||||
@@ -55,8 +55,8 @@ export function TextSection({
|
||||
<input
|
||||
type="text"
|
||||
className={clsx(
|
||||
"border border-gray-300 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
|
||||
{ "cursor-not-allowed bg-gray-100": disabled, "bg-white": !disabled },
|
||||
"border border-base-content/20 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
|
||||
disabled ? "cursor-not-allowed bg-base-200" : "bg-base-100",
|
||||
)}
|
||||
value={internalValue}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const USE_LOGGER = true as boolean;
|
||||
|
||||
export const CLIENT_ID = "Ov23lilAc5biyyRY0K1u";
|
||||
export const CLIENT_ID = "Ov23liZSweT9LJrck9i8";
|
||||
|
||||
export const HOST = window.location.host;
|
||||
|
||||
|
||||
+103
-2
@@ -1,14 +1,115 @@
|
||||
/* ./src/index.css */
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@plugin "daisyui" {
|
||||
themes:
|
||||
light --default,
|
||||
dark --prefersdark;
|
||||
}
|
||||
|
||||
/*
|
||||
Override primary color with default blue from github-stats-extended.
|
||||
Override background color in dark mode with GitHub's dark mode background color.
|
||||
*/
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
--color-primary: #2f80ed;
|
||||
--color-error: var(--color-red-700);
|
||||
}
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dark";
|
||||
--color-primary: #2f80ed;
|
||||
--color-error: var(--color-red-900);
|
||||
--color-base-100: #0d1117;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Snappy duration for interactive elements (buttons, hovers). */
|
||||
--transition-fast: 100ms;
|
||||
/* Slower, deliberate fade for large surfaces during a theme switch. */
|
||||
--transition-surface: 300ms;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Ubuntu, Sans-Serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--color-base-100);
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Smoothly animate color changes when switching themes / on hover. */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
transition-property: background-color, border-color, color, fill, stroke;
|
||||
/*
|
||||
* An ease-out curve: animations start at full speed so quick interactions
|
||||
* like button hovers feel responsive instead of laggy.
|
||||
*/
|
||||
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
transition-duration: var(--transition-fast);
|
||||
}
|
||||
|
||||
/*
|
||||
* Large surface backgrounds fade more slowly so a theme switch reads as a
|
||||
* deliberate fade rather than an instant flip. Durations reference the custom
|
||||
* properties above so the reduced-motion override below zeroes them out
|
||||
* regardless of selector specificity.
|
||||
*/
|
||||
body,
|
||||
.bg-base-100,
|
||||
.bg-base-200,
|
||||
.bg-base-300 {
|
||||
transition-duration: var(--transition-surface);
|
||||
}
|
||||
|
||||
/*
|
||||
* DaisyUI's `.btn` ships its own 200ms transition which felt slow. The global
|
||||
* rule above is unlayered, so it would otherwise clobber the button's property
|
||||
* list and timing function. Restore both here (so fill, text and border animate
|
||||
* together on outline buttons) and only speed up the duration.
|
||||
*/
|
||||
.btn {
|
||||
transition-property: color, background-color, border-color, box-shadow;
|
||||
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
transition-duration: var(--transition-fast);
|
||||
}
|
||||
|
||||
/*
|
||||
* DaisyUI's default hover is very subtle; make it noticeable. Primary always darkens,
|
||||
* while soft lightens in dark mode and darkens in light mode, so the change shows.
|
||||
*/
|
||||
.btn-primary:hover {
|
||||
background-color: color-mix(in oklab, var(--color-primary), #000 30%);
|
||||
}
|
||||
.btn-soft {
|
||||
background-color: color-mix(in oklab, var(--color-base-100), #888 35%);
|
||||
}
|
||||
.btn-soft:hover {
|
||||
background-color: color-mix(in oklab, var(--color-base-100), #888 60%);
|
||||
}
|
||||
.btn-error {
|
||||
color: var(--color-neutral-content);
|
||||
}
|
||||
.btn-error:hover {
|
||||
color: var(--color-neutral-content);
|
||||
background-color: color-mix(in oklab, var(--color-error), #f00 40%);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: 0;
|
||||
border: var(--color-primary) solid 1px;
|
||||
}
|
||||
|
||||
/* Respect users who prefer reduced motion. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--transition-fast: 0ms;
|
||||
--transition-surface: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,75 +4,45 @@
|
||||
"repositories": {
|
||||
"totalCount": 139,
|
||||
"nodes": [
|
||||
{ "name": "classicLogo", "stargazers": { "totalCount": 1 } },
|
||||
{ "name": "creativechat", "stargazers": { "totalCount": 1 } },
|
||||
{ "name": "verlet_tests", "stargazers": { "totalCount": 1 } },
|
||||
{
|
||||
"name": "blade-checkout-ui-demo",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{
|
||||
"name": "tslint-plugin-analytics",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "stargazer", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "react-smooth-range-input",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{
|
||||
"name": "csb-git-integration-bug",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "covid-nepal-web", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "shrijan00003.github.io",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "TheBigCB.com", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "html2commonmark", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "test-myaction", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "react-emoji-render", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "sajilo-editor", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "coder-aayush.github.io",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "codesandbox-client", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "product-sans", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "NodejsBlogSystem", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "native-javascript", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "p5.js", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "gatsbytutorials.com", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "hello-world-docker-action",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "glugmvit.github.io", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "react-map-interaction",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "react-external-image", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "gatsby-boilerplate", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "YouBackup", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "npm-expansions", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "Toy-Neural-Network-JS",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "LSystemCreator", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "Happy-2019", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "jquery-navScroll", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "strml.net", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "Logo", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "website", "stargazers": { "totalCount": 0 } },
|
||||
{
|
||||
"name": "anuraghazra.github.io-old",
|
||||
"stargazers": { "totalCount": 0 }
|
||||
},
|
||||
{ "name": "Quintus", "stargazers": { "totalCount": 0 } },
|
||||
{ "name": "Electron_experiments", "stargazers": { "totalCount": 0 } }
|
||||
{ "name": "classicLogo", "stargazerCount": 1 },
|
||||
{ "name": "creativechat", "stargazerCount": 1 },
|
||||
{ "name": "verlet_tests", "stargazerCount": 1 },
|
||||
{ "name": "blade-checkout-ui-demo", "stargazerCount": 0 },
|
||||
{ "name": "tslint-plugin-analytics", "stargazerCount": 0 },
|
||||
{ "name": "stargazer", "stargazerCount": 0 },
|
||||
{ "name": "react-smooth-range-input", "stargazerCount": 0 },
|
||||
{ "name": "csb-git-integration-bug", "stargazerCount": 0 },
|
||||
{ "name": "covid-nepal-web", "stargazerCount": 0 },
|
||||
{ "name": "shrijan00003.github.io", "stargazerCount": 0 },
|
||||
{ "name": "TheBigCB.com", "stargazerCount": 0 },
|
||||
{ "name": "html2commonmark", "stargazerCount": 0 },
|
||||
{ "name": "test-myaction", "stargazerCount": 0 },
|
||||
{ "name": "react-emoji-render", "stargazerCount": 0 },
|
||||
{ "name": "sajilo-editor", "stargazerCount": 0 },
|
||||
{ "name": "coder-aayush.github.io", "stargazerCount": 0 },
|
||||
{ "name": "codesandbox-client", "stargazerCount": 0 },
|
||||
{ "name": "product-sans", "stargazerCount": 0 },
|
||||
{ "name": "NodejsBlogSystem", "stargazerCount": 0 },
|
||||
{ "name": "native-javascript", "stargazerCount": 0 },
|
||||
{ "name": "p5.js", "stargazerCount": 0 },
|
||||
{ "name": "gatsbytutorials.com", "stargazerCount": 0 },
|
||||
{ "name": "hello-world-docker-action", "stargazerCount": 0 },
|
||||
{ "name": "glugmvit.github.io", "stargazerCount": 0 },
|
||||
{ "name": "react-map-interaction", "stargazerCount": 0 },
|
||||
{ "name": "react-external-image", "stargazerCount": 0 },
|
||||
{ "name": "gatsby-boilerplate", "stargazerCount": 0 },
|
||||
{ "name": "YouBackup", "stargazerCount": 0 },
|
||||
{ "name": "npm-expansions", "stargazerCount": 0 },
|
||||
{ "name": "Toy-Neural-Network-JS", "stargazerCount": 0 },
|
||||
{ "name": "LSystemCreator", "stargazerCount": 0 },
|
||||
{ "name": "Happy-2019", "stargazerCount": 0 },
|
||||
{ "name": "jquery-navScroll", "stargazerCount": 0 },
|
||||
{ "name": "strml.net", "stargazerCount": 0 },
|
||||
{ "name": "Logo", "stargazerCount": 0 },
|
||||
{ "name": "website", "stargazerCount": 0 },
|
||||
{ "name": "anuraghazra.github.io-old", "stargazerCount": 0 },
|
||||
{ "name": "Quintus", "stargazerCount": 0 },
|
||||
{ "name": "Electron_experiments", "stargazerCount": 0 }
|
||||
],
|
||||
"pageInfo": {
|
||||
"hasNextPage": false,
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
"isPrivate": false,
|
||||
"isArchived": false,
|
||||
"isTemplate": false,
|
||||
"stargazers": {
|
||||
"totalCount": 76994
|
||||
},
|
||||
"stargazerCount": 76994,
|
||||
"description": ":zap: Dynamically generated stats for your github readmes",
|
||||
"primaryLanguage": {
|
||||
"color": "#f1e05a",
|
||||
|
||||
@@ -36,606 +36,109 @@
|
||||
"repositories": {
|
||||
"totalCount": 139,
|
||||
"nodes": [
|
||||
{
|
||||
"name": "github-readme-stats",
|
||||
"stargazers": {
|
||||
"totalCount": 77025
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Verly.js",
|
||||
"stargazers": {
|
||||
"totalCount": 696
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "anuraghazra.github.io",
|
||||
"stargazers": {
|
||||
"totalCount": 608
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "anuraghazra",
|
||||
"stargazers": {
|
||||
"totalCount": 376
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type-trident",
|
||||
"stargazers": {
|
||||
"totalCount": 359
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "BugVilla",
|
||||
"stargazers": {
|
||||
"totalCount": 232
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "convoychat",
|
||||
"stargazers": {
|
||||
"totalCount": 214
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CanvasFun",
|
||||
"stargazers": {
|
||||
"totalCount": 211
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EvolutionAquerium",
|
||||
"stargazers": {
|
||||
"totalCount": 192
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "typelevel-parser",
|
||||
"stargazers": {
|
||||
"totalCount": 147
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "VerlyRangeSlider",
|
||||
"stargazers": {
|
||||
"totalCount": 135
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vscode-strip-ts-copy",
|
||||
"stargazers": {
|
||||
"totalCount": 72
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "react-folder-tree",
|
||||
"stargazers": {
|
||||
"totalCount": 54
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Atomic.js",
|
||||
"stargazers": {
|
||||
"totalCount": 53
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ToyLang",
|
||||
"stargazers": {
|
||||
"totalCount": 51
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "design-patterns-everyday",
|
||||
"stargazers": {
|
||||
"totalCount": 46
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gatsby",
|
||||
"stargazers": {
|
||||
"totalCount": 36
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Slime",
|
||||
"stargazers": {
|
||||
"totalCount": 35
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Candy.js",
|
||||
"stargazers": {
|
||||
"totalCount": 34
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ShaderExpo",
|
||||
"stargazers": {
|
||||
"totalCount": 33
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Nothing",
|
||||
"stargazers": {
|
||||
"totalCount": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "peerlist-profile-action",
|
||||
"stargazers": {
|
||||
"totalCount": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "facebook-reaction-animation",
|
||||
"stargazers": {
|
||||
"totalCount": 29
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "react-stripe-dropdown",
|
||||
"stargazers": {
|
||||
"totalCount": 27
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "QuickerPoll",
|
||||
"stargazers": {
|
||||
"totalCount": 27
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "playground-format-on-save",
|
||||
"stargazers": {
|
||||
"totalCount": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ParticleBrush",
|
||||
"stargazers": {
|
||||
"totalCount": 23
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "graphql-oauth",
|
||||
"stargazers": {
|
||||
"totalCount": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parasites",
|
||||
"stargazers": {
|
||||
"totalCount": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "twitter-banner-jokes",
|
||||
"stargazers": {
|
||||
"totalCount": 18
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "verly-cpp",
|
||||
"stargazers": {
|
||||
"totalCount": 16
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gatsby-github-issues-blog",
|
||||
"stargazers": {
|
||||
"totalCount": 16
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "NikeHyperAce",
|
||||
"stargazers": {
|
||||
"totalCount": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rust-particles",
|
||||
"stargazers": {
|
||||
"totalCount": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "VerletDrawing",
|
||||
"stargazers": {
|
||||
"totalCount": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "graphql-jwt-auth-test",
|
||||
"stargazers": {
|
||||
"totalCount": 9
|
||||
}
|
||||
},
|
||||
{ "name": "github-readme-stats", "stargazerCount": 77025 },
|
||||
{ "name": "Verly.js", "stargazerCount": 696 },
|
||||
{ "name": "anuraghazra.github.io", "stargazerCount": 608 },
|
||||
{ "name": "anuraghazra", "stargazerCount": 376 },
|
||||
{ "name": "type-trident", "stargazerCount": 359 },
|
||||
{ "name": "BugVilla", "stargazerCount": 232 },
|
||||
{ "name": "convoychat", "stargazerCount": 214 },
|
||||
{ "name": "CanvasFun", "stargazerCount": 211 },
|
||||
{ "name": "EvolutionAquerium", "stargazerCount": 192 },
|
||||
{ "name": "typelevel-parser", "stargazerCount": 147 },
|
||||
{ "name": "VerlyRangeSlider", "stargazerCount": 135 },
|
||||
{ "name": "vscode-strip-ts-copy", "stargazerCount": 72 },
|
||||
{ "name": "react-folder-tree", "stargazerCount": 54 },
|
||||
{ "name": "Atomic.js", "stargazerCount": 53 },
|
||||
{ "name": "ToyLang", "stargazerCount": 51 },
|
||||
{ "name": "design-patterns-everyday", "stargazerCount": 46 },
|
||||
{ "name": "gatsby", "stargazerCount": 36 },
|
||||
{ "name": "Slime", "stargazerCount": 35 },
|
||||
{ "name": "Candy.js", "stargazerCount": 34 },
|
||||
{ "name": "ShaderExpo", "stargazerCount": 33 },
|
||||
{ "name": "Nothing", "stargazerCount": 32 },
|
||||
{ "name": "peerlist-profile-action", "stargazerCount": 30 },
|
||||
{ "name": "facebook-reaction-animation", "stargazerCount": 29 },
|
||||
{ "name": "react-stripe-dropdown", "stargazerCount": 27 },
|
||||
{ "name": "QuickerPoll", "stargazerCount": 27 },
|
||||
{ "name": "playground-format-on-save", "stargazerCount": 24 },
|
||||
{ "name": "ParticleBrush", "stargazerCount": 23 },
|
||||
{ "name": "graphql-oauth", "stargazerCount": 21 },
|
||||
{ "name": "parasites", "stargazerCount": 21 },
|
||||
{ "name": "twitter-banner-jokes", "stargazerCount": 18 },
|
||||
{ "name": "verly-cpp", "stargazerCount": 16 },
|
||||
{ "name": "gatsby-github-issues-blog", "stargazerCount": 16 },
|
||||
{ "name": "NikeHyperAce", "stargazerCount": 12 },
|
||||
{ "name": "rust-particles", "stargazerCount": 11 },
|
||||
{ "name": "VerletDrawing", "stargazerCount": 10 },
|
||||
{ "name": "graphql-jwt-auth-test", "stargazerCount": 9 },
|
||||
{
|
||||
"name": "anuraghazra.github.io-old-react",
|
||||
"stargazers": {
|
||||
"totalCount": 9
|
||||
}
|
||||
"stargazerCount": 9
|
||||
},
|
||||
{
|
||||
"name": "react18-ssr-test",
|
||||
"stargazers": {
|
||||
"totalCount": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GyroDodge",
|
||||
"stargazers": {
|
||||
"totalCount": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "go-phyllotaxis",
|
||||
"stargazers": {
|
||||
"totalCount": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "graphql-oauth-client",
|
||||
"stargazers": {
|
||||
"totalCount": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "code-runner-action",
|
||||
"stargazers": {
|
||||
"totalCount": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GithubActionsPlayground",
|
||||
"stargazers": {
|
||||
"totalCount": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Loader.js",
|
||||
"stargazers": {
|
||||
"totalCount": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ts-essentials",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "chakra-ui",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "text-to-handwriting",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "build-your-own-x",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nodejs-dynamic-restapi",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "DevYouTubeList",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlockingBlackHole",
|
||||
"stargazers": {
|
||||
"totalCount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "webgpu-boids",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test-githubblock",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "renderless-components",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reakit",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "micro-open-graph",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rest-and-graphql",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "abell-issues-blog",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "VanillaMVC",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "covid19-sim",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "YoutubeDownloader",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "verlet.js",
|
||||
"stargazers": {
|
||||
"totalCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test-github-ts",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tw-dynamic-interpolation",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ts-string-interpolator",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AnuReact",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CanvasBalls",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "circleci-test",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gatsby-starter-netlify-cms",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WebGL.js",
|
||||
"stargazers": {
|
||||
"totalCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test-fork-workflow",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test-codesandbox-projects",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "codesandbox-template-astro",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apollo",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "api-docs",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Buddies-movie",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2k76",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gatsby-remark-embedder",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "weird-hello-worlds",
|
||||
"stargazers": {
|
||||
"totalCount": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rn-tabview-test-repro",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tailwindcss-jit",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "DefinitelyTyped",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server-components-demo",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "volant-vo",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "react-spectrum",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "webhooks-test",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "react-showdown",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "transcode",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "awake-yet-action",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bradgarropy.com",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gatsby-plugin-social-banners",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Tour360",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "blizard.js",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "keep-a-changelog",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vulnerabilities",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "harleydavidson",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "awesome-webgl",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "NewtonsInterpolation",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "now-github-starter",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "simplerockets",
|
||||
"stargazers": {
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
{ "name": "react18-ssr-test", "stargazerCount": 8 },
|
||||
{ "name": "GyroDodge", "stargazerCount": 8 },
|
||||
{ "name": "go-phyllotaxis", "stargazerCount": 6 },
|
||||
{ "name": "graphql-oauth-client", "stargazerCount": 6 },
|
||||
{ "name": "code-runner-action", "stargazerCount": 6 },
|
||||
{ "name": "GithubActionsPlayground", "stargazerCount": 6 },
|
||||
{ "name": "Loader.js", "stargazerCount": 6 },
|
||||
{ "name": "ts-essentials", "stargazerCount": 5 },
|
||||
{ "name": "chakra-ui", "stargazerCount": 5 },
|
||||
{ "name": "text-to-handwriting", "stargazerCount": 5 },
|
||||
{ "name": "build-your-own-x", "stargazerCount": 5 },
|
||||
{ "name": "nodejs-dynamic-restapi", "stargazerCount": 5 },
|
||||
{ "name": "DevYouTubeList", "stargazerCount": 5 },
|
||||
{ "name": "FlockingBlackHole", "stargazerCount": 5 },
|
||||
{ "name": "webgpu-boids", "stargazerCount": 4 },
|
||||
{ "name": "test-githubblock", "stargazerCount": 4 },
|
||||
{ "name": "renderless-components", "stargazerCount": 4 },
|
||||
{ "name": "reakit", "stargazerCount": 4 },
|
||||
{ "name": "micro-open-graph", "stargazerCount": 4 },
|
||||
{ "name": "rest-and-graphql", "stargazerCount": 4 },
|
||||
{ "name": "abell-issues-blog", "stargazerCount": 4 },
|
||||
{ "name": "VanillaMVC", "stargazerCount": 4 },
|
||||
{ "name": "covid19-sim", "stargazerCount": 4 },
|
||||
{ "name": "YoutubeDownloader", "stargazerCount": 4 },
|
||||
{ "name": "verlet.js", "stargazerCount": 4 },
|
||||
{ "name": "test-github-ts", "stargazerCount": 3 },
|
||||
{ "name": "tw-dynamic-interpolation", "stargazerCount": 3 },
|
||||
{ "name": "ts-string-interpolator", "stargazerCount": 3 },
|
||||
{ "name": "AnuReact", "stargazerCount": 3 },
|
||||
{ "name": "CanvasBalls", "stargazerCount": 3 },
|
||||
{ "name": "circleci-test", "stargazerCount": 3 },
|
||||
{ "name": "gatsby-starter-netlify-cms", "stargazerCount": 3 },
|
||||
{ "name": "WebGL.js", "stargazerCount": 3 },
|
||||
{ "name": "test-fork-workflow", "stargazerCount": 2 },
|
||||
{ "name": "test-codesandbox-projects", "stargazerCount": 2 },
|
||||
{ "name": "codesandbox-template-astro", "stargazerCount": 2 },
|
||||
{ "name": "apollo", "stargazerCount": 2 },
|
||||
{ "name": "api-docs", "stargazerCount": 2 },
|
||||
{ "name": "Buddies-movie", "stargazerCount": 2 },
|
||||
{ "name": "2k76", "stargazerCount": 2 },
|
||||
{ "name": "gatsby-remark-embedder", "stargazerCount": 2 },
|
||||
{ "name": "weird-hello-worlds", "stargazerCount": 2 },
|
||||
{ "name": "rn-tabview-test-repro", "stargazerCount": 1 },
|
||||
{ "name": "tailwindcss-jit", "stargazerCount": 1 },
|
||||
{ "name": "DefinitelyTyped", "stargazerCount": 1 },
|
||||
{ "name": "server-components-demo", "stargazerCount": 1 },
|
||||
{ "name": "volant-vo", "stargazerCount": 1 },
|
||||
{ "name": "react-spectrum", "stargazerCount": 1 },
|
||||
{ "name": "webhooks-test", "stargazerCount": 1 },
|
||||
{ "name": "react-showdown", "stargazerCount": 1 },
|
||||
{ "name": "transcode", "stargazerCount": 1 },
|
||||
{ "name": "awake-yet-action", "stargazerCount": 1 },
|
||||
{ "name": "bradgarropy.com", "stargazerCount": 1 },
|
||||
{ "name": "gatsby-plugin-social-banners", "stargazerCount": 1 },
|
||||
{ "name": "Tour360", "stargazerCount": 1 },
|
||||
{ "name": "blizard.js", "stargazerCount": 1 },
|
||||
{ "name": "keep-a-changelog", "stargazerCount": 1 },
|
||||
{ "name": "vulnerabilities", "stargazerCount": 1 },
|
||||
{ "name": "harleydavidson", "stargazerCount": 1 },
|
||||
{ "name": "awesome-webgl", "stargazerCount": 1 },
|
||||
{ "name": "NewtonsInterpolation", "stargazerCount": 1 },
|
||||
{ "name": "now-github-starter", "stargazerCount": 1 },
|
||||
{ "name": "simplerockets", "stargazerCount": 1 }
|
||||
],
|
||||
"pageInfo": {
|
||||
"hasNextPage": true,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ToastContainer, toast } from "react-toastify";
|
||||
import { getUserMetadata } from "../../api/user";
|
||||
import { clearAxiosCache } from "../../axios-override";
|
||||
import type { StageIndex } from "../../models/Stage";
|
||||
import { useTheme } from "../../redux/selectors/themeSelectors";
|
||||
import {
|
||||
useIsAuthenticated,
|
||||
useUserKey,
|
||||
@@ -56,6 +57,7 @@ export function AppTrends() {
|
||||
const userKey = useUserKey();
|
||||
const userToken = useUserToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const [stage, setStage] = useState<StageIndex>(isAuthenticated ? 1 : 0);
|
||||
|
||||
@@ -116,9 +118,9 @@ export function AppTrends() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header currStageIndex={stage} onStageIndexChange={setStage} />
|
||||
<section className="bg-white text-gray-700 flex-grow">
|
||||
<section className="bg-base-100 text-base-content flex-grow">
|
||||
<HomeScreen stage={stage} setStage={setStage} />
|
||||
<ToastContainer />
|
||||
<ToastContainer theme={isDark ? "dark" : "light"} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { JSX } from "react";
|
||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||
|
||||
import appIcon from "../../assets/appLogo64.png";
|
||||
import { ThemePicker } from "../../components/Generic/ThemePicker";
|
||||
import { ProgressBar } from "../../components/Home/Progress";
|
||||
import { STAGE_LABELS } from "../../models/Stage";
|
||||
import type { StageIndex } from "../../models/Stage";
|
||||
@@ -19,31 +20,34 @@ export function Header({
|
||||
}: HeaderProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-100 bg-gray-800 shadow-md z-50">
|
||||
<div
|
||||
className="text-neutral-content shadow-md z-50"
|
||||
style={{
|
||||
backgroundColor:
|
||||
"color-mix(in oklab, var(--color-primary), #000 50%)",
|
||||
}}
|
||||
>
|
||||
<div className="px-5 py-2 flex flex-wrap">
|
||||
{/* Logo */}
|
||||
<a
|
||||
href="/"
|
||||
className="flex items-center title-font font-medium text-gray-50 mb-0 md:mr-8"
|
||||
className="flex items-center title-font font-medium text-neutral-content mb-0 md:mr-8"
|
||||
>
|
||||
<img src={appIcon} alt="logo" className="w-6 h-6" />
|
||||
<span className="ml-2 text-xl">GitHub Stats Extended</span>
|
||||
</a>
|
||||
{/* Star on GitHub */}
|
||||
<div className="flex ml-auto items-center text-base justify-center">
|
||||
{/* Star on GitHub + theme toggle */}
|
||||
<div className="flex ml-auto items-center gap-2 text-base justify-center">
|
||||
<a
|
||||
href="https://github.com/stats-organization/github-stats-extended"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-[0.25rem] shadow bg-neutral-content hover:opacity-90 text-neutral px-3 py-1 flex items-center"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-[0.25rem] shadow bg-gray-200 hover:bg-gray-300 text-black px-3 py-1 flex items-center"
|
||||
>
|
||||
Star on
|
||||
<GithubIcon className="ml-1.5 w-5 h-5" />
|
||||
</button>
|
||||
Star on
|
||||
<GithubIcon className="ml-1.5 w-5 h-5" />
|
||||
</a>
|
||||
<ThemePicker />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { CardType } from "../../models/CardType";
|
||||
import { STAGE_LABELS } from "../../models/Stage";
|
||||
import type { StageIndex } from "../../models/Stage";
|
||||
import { useTheme } from "../../redux/selectors/themeSelectors";
|
||||
import {
|
||||
useIsAuthenticated,
|
||||
usePrivateAccess,
|
||||
@@ -83,7 +84,9 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
||||
const [enableAnimations, setEnableAnimations] = useState(true);
|
||||
const [usePercent, setUsePercent] = useState(false);
|
||||
|
||||
const [theme, setTheme] = useState("default");
|
||||
const { isDark } = useTheme();
|
||||
const themeParam = isDark ? "&theme=github_dark" : "";
|
||||
const [theme, setTheme] = useState(isDark ? "dark" : "default");
|
||||
|
||||
const handleCardTypeChange = (cardType: CardType) => {
|
||||
if (cardType === CardType.TOP_LANGS) {
|
||||
@@ -230,11 +233,14 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={contentSectionRef} className="h-full px-2 lg:px-8 text-gray-600">
|
||||
<div
|
||||
ref={contentSectionRef}
|
||||
className="h-full px-2 lg:px-8 text-base-content/70"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="m-4 rounded-sm">
|
||||
<div className="lg:p-4">
|
||||
<h1 className="text-2xl text-gray-600 font-semibold">
|
||||
<h1 className="text-2xl text-base-content/70 font-semibold">
|
||||
{STAGE_LABELS[stage].title}
|
||||
</h1>
|
||||
<div>
|
||||
@@ -245,7 +251,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
||||
<a
|
||||
href={`https://github.com/${userId}`}
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:underline font-semibold"
|
||||
className="text-primary hover:underline font-semibold"
|
||||
>
|
||||
{userId}
|
||||
</a>
|
||||
@@ -328,7 +334,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
||||
setWakatimeUser={setWakatimeUser}
|
||||
usePercent={usePercent}
|
||||
setUsePercent={setUsePercent}
|
||||
fullSuffix={fullSuffix}
|
||||
fullSuffix={fullSuffix + themeParam}
|
||||
setStage={setStage}
|
||||
/>
|
||||
)}
|
||||
@@ -382,6 +388,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
||||
return "";
|
||||
}
|
||||
})()}
|
||||
theme={theme}
|
||||
themeSuffix={themeSuffix}
|
||||
guestHint={
|
||||
isAuthenticated
|
||||
|
||||
@@ -109,7 +109,7 @@ export function CustomizeStage({
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-wrap">
|
||||
<div className="h-auto lg:w-2/5 md:w-1/2 p-10 rounded-sm bg-gray-200">
|
||||
<div className="h-auto lg:w-2/5 md:w-1/2 p-10 rounded-sm bg-base-200">
|
||||
{(cardType === CardType.STATS || cardType === CardType.TOP_LANGS) && (
|
||||
<TextSection
|
||||
title="Username"
|
||||
@@ -126,7 +126,7 @@ export function CustomizeStage({
|
||||
e.preventDefault();
|
||||
setStage(0);
|
||||
}}
|
||||
className="underline text-blue-900"
|
||||
className="underline text-primary"
|
||||
>
|
||||
log in
|
||||
</a>{" "}
|
||||
@@ -170,7 +170,7 @@ export function CustomizeStage({
|
||||
e.preventDefault();
|
||||
setStage(0);
|
||||
}}
|
||||
className="underline text-blue-900"
|
||||
className="underline text-primary"
|
||||
>
|
||||
log in
|
||||
</a>{" "}
|
||||
@@ -214,7 +214,7 @@ export function CustomizeStage({
|
||||
e.preventDefault();
|
||||
setStage(0);
|
||||
}}
|
||||
className="underline text-blue-900"
|
||||
className="underline text-primary"
|
||||
>
|
||||
log in
|
||||
</a>{" "}
|
||||
@@ -251,7 +251,7 @@ export function CustomizeStage({
|
||||
<a
|
||||
href="https://wakatime.com/"
|
||||
target="_blank"
|
||||
className="underline text-blue-900"
|
||||
className="underline text-primary"
|
||||
>
|
||||
WakaTime
|
||||
</a>{" "}
|
||||
@@ -411,7 +411,7 @@ export function CustomizeStage({
|
||||
<a
|
||||
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md"
|
||||
target="_blank"
|
||||
className="underline text-blue-900"
|
||||
className="underline text-primary"
|
||||
>
|
||||
customization documentation
|
||||
</a>{" "}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { clsx } from "clsx";
|
||||
import type { JSX } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { saveSvgAsPng } from "save-svg-as-png";
|
||||
|
||||
import { CardImage } from "../../../components/Card/CardImage";
|
||||
import { getCardThemeBackdrop } from "../../../components/Card/themeBackdrop";
|
||||
import { Button } from "../../../components/Generic/Button";
|
||||
import { HOST } from "../../../constants";
|
||||
import { useTheme } from "../../../redux/selectors/themeSelectors";
|
||||
|
||||
interface DisplayStageProps {
|
||||
filename: string;
|
||||
link: string;
|
||||
theme: string;
|
||||
themeSuffix: string;
|
||||
guestHint: string | null;
|
||||
}
|
||||
@@ -17,9 +19,12 @@ interface DisplayStageProps {
|
||||
export function DisplayStage({
|
||||
filename,
|
||||
link,
|
||||
theme,
|
||||
themeSuffix,
|
||||
guestHint,
|
||||
}: DisplayStageProps): JSX.Element {
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const downloadPNG = () => {
|
||||
saveSvgAsPng(
|
||||
document.getElementById("svgWrapper")?.shadowRoot?.firstElementChild
|
||||
@@ -61,7 +66,7 @@ export function DisplayStage({
|
||||
return (
|
||||
<div className="w-full flex flex-wrap">
|
||||
<div className="h-auto lg:w-2/5 md:w-1/2">
|
||||
<div className="p-10 rounded-sm bg-gray-200">
|
||||
<div className="p-10 rounded-sm bg-base-200">
|
||||
<div className="flex flex-col items-center">
|
||||
{[
|
||||
{
|
||||
@@ -82,10 +87,8 @@ export function DisplayStage({
|
||||
].map((item) => (
|
||||
<Button
|
||||
key={item.title}
|
||||
className={clsx("m-4 w-60 flex justify-center", {
|
||||
"bg-blue-500 hover:bg-blue-600 text-white": item.highlight,
|
||||
"bg-white hover:bg-gray-100 text-black": !item.highlight,
|
||||
})}
|
||||
variant={item.highlight ? "primary" : "soft"}
|
||||
className="m-4 w-60 flex justify-center"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{item.title}
|
||||
@@ -96,10 +99,14 @@ export function DisplayStage({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0">
|
||||
<div className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32">
|
||||
<div
|
||||
className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32 rounded p-4"
|
||||
style={{ background: getCardThemeBackdrop(theme, isDark) }}
|
||||
>
|
||||
<CardImage
|
||||
imageSrc={`${themeSuffix}&disable_animations=true`}
|
||||
stage={4}
|
||||
className="flex justify-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function LoginAccountDeleteModal(
|
||||
<div className="fixed left-0 top-0 w-full h-full">
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<div
|
||||
className="w-96 p-4 bg-white rounded-sm border-2 border-gray-200 text-gray-700"
|
||||
className="w-96 p-4 bg-base-100 rounded-sm border-2 border-base-300 text-base-content"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<p className="mb-1 text-2xl">Delete Account</p>
|
||||
@@ -57,16 +57,10 @@ export function LoginAccountDeleteModal(
|
||||
</p>
|
||||
<br />
|
||||
<div className="flex flex-wrap">
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white rounded-[0.25rem]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gray-200 hover:bg-gray-300 ml-auto rounded-[0.25rem] text-red-600 border-2"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Button variant="error" className="ml-auto" onClick={onConfirm}>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -56,14 +56,17 @@ export function LoginAccountManagement(): JSX.Element {
|
||||
<a
|
||||
href={`https://${HOST}/api/downgrade?user_key=${userKey as string}`}
|
||||
>
|
||||
<Button className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100">
|
||||
<Button
|
||||
variant="soft"
|
||||
className="h-12 flex justify-center items-center w-[320px]"
|
||||
>
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
Downgrade to Public Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/70 flex-1">
|
||||
Switch to public access if you prefer not to share private
|
||||
contributions.
|
||||
</p>
|
||||
@@ -71,14 +74,17 @@ export function LoginAccountManagement(): JSX.Element {
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[320px] text-white bg-blue-500 hover:bg-blue-600">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="h-12 flex justify-center items-center w-[320px]"
|
||||
>
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">
|
||||
Upgrade to Private Access
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/70 flex-1">
|
||||
Upgrade to include contributions in private repositories for more
|
||||
complete and accurate stats.
|
||||
</p>
|
||||
@@ -89,12 +95,15 @@ export function LoginAccountManagement(): JSX.Element {
|
||||
{/* Delete Account Button */}
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
variant="soft"
|
||||
className="h-12 flex justify-center items-center w-[320px]"
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
<span className="xl:text-lg text-red-600">Delete Account</span>
|
||||
<span className="xl:text-lg text-error text-shadow-none">
|
||||
Delete Account
|
||||
</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/70 flex-1">
|
||||
This will delete your GitHub-Stats-Extended account and then redirect
|
||||
you to a GitHub screen where you can revoke your access token.
|
||||
</p>
|
||||
@@ -103,12 +112,13 @@ export function LoginAccountManagement(): JSX.Element {
|
||||
{/* Logout Button */}
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
variant="soft"
|
||||
className="h-12 flex justify-center items-center w-[320px]"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<span className="xl:text-lg">Log Out</span>
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/70 flex-1">
|
||||
Log out from GitHub-Stats-Extended.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function LoginBox(props: LoginBoxProps): JSX.Element {
|
||||
<div className="h-full flex flex-wrap">
|
||||
<div className={clsx("md:flex", { "opacity-25": isOpaque })}>
|
||||
<div className="lg:block lg:w-3/5 lg:p-8">
|
||||
<div className="bg-gray-200 rounded-sm w-full h-full m-auto p-8 shadow lg:h-auto">
|
||||
<div className="bg-base-300 rounded-sm w-full h-full m-auto p-8 shadow lg:h-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DEMO_USER,
|
||||
DEMO_WAKATIME_USER,
|
||||
} from "../../../../constants";
|
||||
import { useTheme } from "../../../../redux/selectors/themeSelectors";
|
||||
|
||||
const cards: Array<{ demoImageSrc: string }> = [
|
||||
{
|
||||
@@ -39,6 +40,10 @@ function getCardXPosition(cardIndex: number): number {
|
||||
}
|
||||
|
||||
export function LoginBoxDemoCards(): JSX.Element {
|
||||
const { isDark } = useTheme();
|
||||
// Show dark-themed demo cards in dark mode so they fit the surroundings.
|
||||
const themeParam = isDark ? "&theme=github_dark" : "";
|
||||
|
||||
return (
|
||||
<div className="w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8 relative overflow-hidden">
|
||||
<div className="relative w-full h-full">
|
||||
@@ -52,7 +57,11 @@ export function LoginBoxDemoCards(): JSX.Element {
|
||||
marginBottom: "1%",
|
||||
}}
|
||||
>
|
||||
<CardImage imageSrc={card.demoImageSrc} compact={false} stage={0} />
|
||||
<CardImage
|
||||
imageSrc={`${card.demoImageSrc}${themeParam}`}
|
||||
compact={false}
|
||||
stage={0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -20,24 +20,30 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
|
||||
<LoginBox>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<a href={GITHUB_PUBLIC_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[260px] text-white bg-blue-500 hover:bg-blue-600">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="h-12 flex justify-center items-center w-[260px]"
|
||||
>
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">GitHub Public Access</span>
|
||||
GitHub Public Access
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/80 flex-1">
|
||||
Generate stats based on your contributions in public repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
||||
<Button className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100">
|
||||
<Button
|
||||
variant="soft"
|
||||
className="h-12 flex justify-center items-center w-[260px]"
|
||||
>
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
<span className="ml-2 xl:text-lg">GitHub Private Access</span>
|
||||
GitHub Private Access
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/80 flex-1">
|
||||
Include contributions from private repositories for more complete and
|
||||
accurate stats.
|
||||
</p>
|
||||
@@ -45,12 +51,13 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100"
|
||||
variant="soft"
|
||||
className="h-12 flex justify-center items-center w-[260px]"
|
||||
onClick={onContinueAsGuest}
|
||||
>
|
||||
<span className="ml-2 xl:text-lg">Continue as Guest</span>
|
||||
Continue as Guest
|
||||
</Button>
|
||||
<p className="text-sm text-gray-600 flex-1">
|
||||
<p className="text-sm text-base-content/80 flex-1">
|
||||
Explore options using sample data. Insert your own username in the
|
||||
last step.
|
||||
</p>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DEMO_WAKATIME_USER,
|
||||
} from "../../../constants";
|
||||
import { CardType } from "../../../models/CardType";
|
||||
import { useTheme } from "../../../redux/selectors/themeSelectors";
|
||||
import { useUserId } from "../../../redux/selectors/userSelectors";
|
||||
|
||||
interface SelectCardStageProps {
|
||||
@@ -21,6 +22,9 @@ export function SelectCardStage({
|
||||
onCardTypeChange,
|
||||
}: SelectCardStageProps): JSX.Element {
|
||||
const userId = useUserId(DEMO_USER);
|
||||
const { isDark } = useTheme();
|
||||
// Show dark-themed demo cards in dark mode so they fit the surroundings.
|
||||
const themeParam = isDark ? "&theme=github_dark" : "";
|
||||
|
||||
const options = useMemo<
|
||||
Array<{
|
||||
@@ -34,37 +38,37 @@ export function SelectCardStage({
|
||||
{
|
||||
title: "GitHub Stats Card",
|
||||
description: "your overall GitHub statistics",
|
||||
demoImageSrc: `?username=${userId}&include_all_commits=true`,
|
||||
demoImageSrc: `?username=${userId}&include_all_commits=true${themeParam}`,
|
||||
cardType: CardType.STATS,
|
||||
},
|
||||
{
|
||||
title: "Top Languages Card",
|
||||
description: "your most frequently used languages",
|
||||
demoImageSrc: `/top-langs?username=${userId}&langs_count=4`,
|
||||
demoImageSrc: `/top-langs?username=${userId}&langs_count=4${themeParam}`,
|
||||
cardType: CardType.TOP_LANGS,
|
||||
},
|
||||
{
|
||||
title: "GitHub Extra Pin",
|
||||
description:
|
||||
"pin more than 6 repositories in your profile using a GitHub profile readme",
|
||||
demoImageSrc: `/pin?repo=${DEMO_REPO}`,
|
||||
demoImageSrc: `/pin?repo=${DEMO_REPO}${themeParam}`,
|
||||
cardType: CardType.PIN,
|
||||
},
|
||||
{
|
||||
title: "GitHub Gist Pin",
|
||||
description:
|
||||
"pin gists in your GitHub profile using a GitHub profile readme",
|
||||
demoImageSrc: `/gist?id=${DEMO_GIST}`,
|
||||
demoImageSrc: `/gist?id=${DEMO_GIST}${themeParam}`,
|
||||
cardType: CardType.GIST,
|
||||
},
|
||||
{
|
||||
title: "WakaTime Stats Card",
|
||||
description: "your coding activity from WakaTime",
|
||||
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450`,
|
||||
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450${themeParam}`,
|
||||
cardType: CardType.WAKATIME,
|
||||
},
|
||||
],
|
||||
[userId],
|
||||
[userId, themeParam],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,11 @@ import { themes } from "@stats-organization/github-readme-stats-core";
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { Card } from "../../../components/Card/Card";
|
||||
import {
|
||||
getCardThemeBackdrop,
|
||||
getThemeSortRank,
|
||||
} from "../../../components/Card/themeBackdrop";
|
||||
import { useTheme } from "../../../redux/selectors/themeSelectors";
|
||||
|
||||
const excludedThemes = [
|
||||
"merko",
|
||||
@@ -12,9 +17,10 @@ const excludedThemes = [
|
||||
"holi",
|
||||
];
|
||||
|
||||
const themeList = Object.keys(themes).filter(
|
||||
(myTheme) => !excludedThemes.includes(myTheme),
|
||||
);
|
||||
// Light themes first, adaptive themes in the middle, dark themes last.
|
||||
const themeList = Object.keys(themes)
|
||||
.filter((myTheme) => !excludedThemes.includes(myTheme))
|
||||
.sort((a, b) => getThemeSortRank(a) - getThemeSortRank(b));
|
||||
|
||||
interface ThemeStageProps {
|
||||
fullSuffix: string;
|
||||
@@ -27,38 +33,49 @@ export function ThemeStage({
|
||||
fullSuffix,
|
||||
onThemeChange,
|
||||
}: ThemeStageProps): JSX.Element {
|
||||
const { isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap">
|
||||
{themeList.map((myTheme) => (
|
||||
<button
|
||||
className="p-2 lg:p-4"
|
||||
key={myTheme}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onThemeChange(myTheme);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
title={myTheme}
|
||||
description=""
|
||||
imageSrc={`${fullSuffix}&theme=${myTheme}`}
|
||||
selected={theme === myTheme}
|
||||
stage={3}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{themeList.map((myTheme) => {
|
||||
const themeColors = themes[myTheme as keyof typeof themes];
|
||||
return (
|
||||
<button
|
||||
className="p-2 lg:p-4"
|
||||
key={myTheme}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onThemeChange(myTheme);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
title={myTheme}
|
||||
description=""
|
||||
imageSrc={`${fullSuffix}&theme=${myTheme}`}
|
||||
selected={theme === myTheme}
|
||||
stage={3}
|
||||
backgroundColor={getCardThemeBackdrop(myTheme, isDark)}
|
||||
titleColor={`#${
|
||||
myTheme === "ambient_gradient" && !isDark
|
||||
? (themes["ambient_gradient"].bg_color.split(",")[1] ?? "")
|
||||
: themeColors.title_color
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="pl-10 pr-10">
|
||||
For more theme options check the{" "}
|
||||
{"For more theme options check the "}
|
||||
<a
|
||||
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md#themes"
|
||||
target="_blank"
|
||||
className="underline text-blue-900"
|
||||
className="underline text-primary"
|
||||
>
|
||||
customization documentation
|
||||
</a>{" "}
|
||||
after you copied your card URL in step 5.
|
||||
</a>
|
||||
{" after you copied your card URL in step 5."}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { isDarkTheme, setTheme as setThemeAction } from "../slices/theme";
|
||||
import type { StoreState } from "../store";
|
||||
|
||||
/**
|
||||
* Reads the active DaisyUI theme from the Redux store and exposes a setter
|
||||
* that persists the choice and updates the `data-theme` attribute.
|
||||
*/
|
||||
export function useTheme(): {
|
||||
theme: string;
|
||||
isDark: boolean;
|
||||
setTheme: (theme: string) => void;
|
||||
} {
|
||||
const dispatch = useDispatch();
|
||||
const theme = useSelector((state: StoreState) => state.theme.theme);
|
||||
|
||||
const setTheme = useCallback(
|
||||
(next: string) => {
|
||||
dispatch(setThemeAction(next));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return { theme, isDark: isDarkTheme(theme), setTheme };
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface ThemeOption {
|
||||
/** DaisyUI theme name, used as the `data-theme` value. */
|
||||
name: string;
|
||||
label: string;
|
||||
/** Whether the theme has a dark background (drives toast styling, etc.). */
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Themes exposed in the theme picker.
|
||||
* Keep in sync with the `themes:` list in `index.css`.
|
||||
*/
|
||||
export const THEMES: ReadonlyArray<ThemeOption> = [
|
||||
{ name: "light", label: "Light", isDark: false },
|
||||
{ name: "dark", label: "Dark", isDark: true },
|
||||
];
|
||||
|
||||
const DEFAULT_LIGHT_THEME = "light";
|
||||
const DEFAULT_DARK_THEME = "dark";
|
||||
|
||||
function isValidTheme(name: string | null): name is string {
|
||||
return !!name && THEMES.some((theme) => theme.name === name);
|
||||
}
|
||||
|
||||
export function isDarkTheme(name: string): boolean {
|
||||
return THEMES.find((theme) => theme.name === name)?.isDark ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Exported so the inferred store type can name it (see the note in `user.ts`).
|
||||
*/
|
||||
export interface ThemeState {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
function getInitialTheme(): string {
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (isValidTheme(stored)) {
|
||||
return stored;
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? DEFAULT_DARK_THEME
|
||||
: DEFAULT_LIGHT_THEME;
|
||||
}
|
||||
|
||||
const initialState: ThemeState = {
|
||||
theme: getInitialTheme(),
|
||||
};
|
||||
|
||||
const themeSlice = createSlice({
|
||||
name: "theme",
|
||||
initialState,
|
||||
reducers: {
|
||||
setTheme: (state, action: PayloadAction<string>) => {
|
||||
const theme = action.payload;
|
||||
localStorage.setItem("theme", theme);
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
state.theme = theme;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setTheme } = themeSlice.actions;
|
||||
|
||||
export default themeSlice.reducer;
|
||||
@@ -3,11 +3,13 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||
import { USE_LOGGER } from "../constants";
|
||||
|
||||
import { loggerMiddleware } from "./logger";
|
||||
import theme from "./slices/theme";
|
||||
import user from "./slices/user";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
user,
|
||||
theme,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
const middleware = getDefaultMiddleware();
|
||||
|
||||
@@ -150,6 +150,10 @@ export default defineConfig(
|
||||
},
|
||||
],
|
||||
|
||||
// Keep `@param props.x` doc names in sync with the destructured
|
||||
// parameters so they cannot silently drift on rename.
|
||||
"jsdoc/check-param-names": "error",
|
||||
|
||||
// We don't need this we have typescript
|
||||
"jsdoc/require-returns": "off",
|
||||
"jsdoc/require-returns-description": "off",
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
"@eslint/compat": "2.1.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@playwright/test": "1.60.0",
|
||||
"@types/node": "24.13.1",
|
||||
"@types/node": "24.13.2",
|
||||
"@vitest/coverage-v8": "catalog:default",
|
||||
"eslint": "10.4.1",
|
||||
"eslint-import-resolver-typescript": "4.4.5",
|
||||
@@ -20,7 +20,7 @@
|
||||
"knip": "6.16.1",
|
||||
"lint-staged": "17.0.7",
|
||||
"prettier": "3.8.4",
|
||||
"turbo": "2.9.17",
|
||||
"turbo": "2.9.18",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"vitest": "catalog:default"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@stats-organization/github-readme-stats-core",
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.4",
|
||||
"type": "module",
|
||||
"homepage": "https://github-stats-extended.vercel.app/frontend",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderGistCard } from "../cards/gist.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderStatsCard } from "../cards/stats.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderRepoCard } from "../cards/repo.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderTopLanguages } from "../cards/top-languages.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderWakatimeCard } from "../cards/wakatime.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { default as Card } from "../common/Card.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { fallbackColor, getCardColors } from "../common/color.js";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
// @ts-check
|
||||
|
||||
import { encodeHTML } from "./html.js";
|
||||
import { flexLayout } from "./render.js";
|
||||
|
||||
interface CardColors {
|
||||
/** Card title color. */
|
||||
titleColor?: string;
|
||||
/** Card text color. */
|
||||
textColor?: string;
|
||||
/** Card icon color. */
|
||||
iconColor?: string;
|
||||
/** Card background color. */
|
||||
bgColor?: string | Array<string>;
|
||||
/** Card border color. */
|
||||
borderColor?: string;
|
||||
}
|
||||
|
||||
class Card {
|
||||
width: number;
|
||||
height: number;
|
||||
hideBorder: boolean;
|
||||
hideTitle: boolean;
|
||||
border_radius: number;
|
||||
colors: CardColors;
|
||||
title: string;
|
||||
css: string;
|
||||
paddingX: number;
|
||||
paddingY: number;
|
||||
titlePrefixIcon: string | undefined;
|
||||
animations: boolean;
|
||||
a11yTitle: string;
|
||||
a11yDesc: string;
|
||||
|
||||
/**
|
||||
* Creates a new card instance.
|
||||
*
|
||||
* @param {object} args Card arguments.
|
||||
* @param {number=} args.width Card width.
|
||||
* @param {number=} args.height Card height.
|
||||
* @param {number=} args.border_radius Card border radius.
|
||||
* @param {string=} args.customTitle Card custom title.
|
||||
* @param {string=} args.defaultTitle Card default title.
|
||||
* @param {string=} args.titlePrefixIcon Card title prefix icon.
|
||||
* @param {object} [args.colors={}] Card colors arguments.
|
||||
* @param {string=} args.colors.titleColor Card title color.
|
||||
* @param {string=} args.colors.textColor Card text color.
|
||||
* @param {string=} args.colors.iconColor Card icon color.
|
||||
* @param {string|string[]=} args.colors.bgColor Card background color.
|
||||
* @param {string=} args.colors.borderColor Card border color.
|
||||
* @param props Card arguments.
|
||||
* @param props.width Card width.
|
||||
* @param props.height Card height.
|
||||
* @param props.border_radius Card border radius.
|
||||
* @param props.colors Card colors arguments.
|
||||
* @param props.customTitle Card custom title.
|
||||
* @param props.defaultTitle Card default title.
|
||||
* @param props.titlePrefixIcon Card title prefix icon.
|
||||
*/
|
||||
constructor({
|
||||
width = 100,
|
||||
@@ -29,6 +50,14 @@ class Card {
|
||||
customTitle,
|
||||
defaultTitle = "",
|
||||
titlePrefixIcon,
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
border_radius?: number;
|
||||
colors?: CardColors;
|
||||
customTitle?: string;
|
||||
defaultTitle?: string;
|
||||
titlePrefixIcon?: string;
|
||||
}) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
@@ -40,10 +69,9 @@ class Card {
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
this.colors = colors;
|
||||
this.title =
|
||||
customTitle === undefined
|
||||
? encodeHTML(defaultTitle)
|
||||
: encodeHTML(customTitle);
|
||||
this.title = encodeHTML(
|
||||
customTitle === undefined ? defaultTitle : customTitle,
|
||||
);
|
||||
|
||||
this.css = "";
|
||||
|
||||
@@ -55,45 +83,44 @@ class Card {
|
||||
this.a11yDesc = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
disableAnimations() {
|
||||
disableAnimations(): void {
|
||||
this.animations = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} props The props object.
|
||||
* @param {string} props.title Accessibility title.
|
||||
* @param {string} props.desc Accessibility description.
|
||||
* @returns {void}
|
||||
* @param props The props object.
|
||||
* @param props.title Accessibility title.
|
||||
* @param props.desc Accessibility description.
|
||||
*/
|
||||
setAccessibilityLabel({ title, desc }) {
|
||||
setAccessibilityLabel({
|
||||
title,
|
||||
desc,
|
||||
}: {
|
||||
title: string;
|
||||
desc: string;
|
||||
}): void {
|
||||
this.a11yTitle = title;
|
||||
this.a11yDesc = desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value The CSS to add to the card.
|
||||
* @returns {void}
|
||||
* @param value The CSS to add to the card.
|
||||
*/
|
||||
setCSS(value) {
|
||||
setCSS(value: string): void {
|
||||
this.css = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value Whether to hide the border or not.
|
||||
* @returns {void}
|
||||
* @param value Whether to hide the border or not.
|
||||
*/
|
||||
setHideBorder(value) {
|
||||
setHideBorder(value: boolean): void {
|
||||
this.hideBorder = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value Whether to hide the title or not.
|
||||
* @returns {void}
|
||||
* @param value Whether to hide the title or not.
|
||||
*/
|
||||
setHideTitle(value) {
|
||||
setHideTitle(value: boolean): void {
|
||||
this.hideTitle = value;
|
||||
if (value) {
|
||||
this.height -= 30;
|
||||
@@ -101,17 +128,16 @@ class Card {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text The title to set.
|
||||
* @returns {void}
|
||||
* @param text The title to set.
|
||||
*/
|
||||
setTitle(text) {
|
||||
setTitle(text: string): void {
|
||||
this.title = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The rendered card title.
|
||||
* @returns The rendered card title.
|
||||
*/
|
||||
renderTitle() {
|
||||
renderTitle(): string {
|
||||
const titleText = `
|
||||
<text
|
||||
x="0"
|
||||
@@ -131,7 +157,7 @@ class Card {
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
${this.titlePrefixIcon}
|
||||
${String(this.titlePrefixIcon)}
|
||||
</svg>
|
||||
`;
|
||||
return `
|
||||
@@ -148,9 +174,9 @@ class Card {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The rendered card gradient.
|
||||
* @returns The rendered card gradient.
|
||||
*/
|
||||
renderGradient() {
|
||||
renderGradient(): string {
|
||||
if (typeof this.colors.bgColor !== "object") {
|
||||
return "";
|
||||
}
|
||||
@@ -161,13 +187,15 @@ class Card {
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient"
|
||||
gradientTransform="rotate(${this.colors.bgColor[0]})"
|
||||
gradientTransform="rotate(${String(this.colors.bgColor[0])})"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
${gradients.map((grad, index) => {
|
||||
let offset = (index * 100) / (gradients.length - 1);
|
||||
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
||||
})}
|
||||
${gradients
|
||||
.map((grad, index) => {
|
||||
const offset = (index * 100) / (gradients.length - 1);
|
||||
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
||||
})
|
||||
.join(",")}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
`
|
||||
@@ -177,9 +205,9 @@ class Card {
|
||||
/**
|
||||
* Retrieves css animations for a card.
|
||||
*
|
||||
* @returns {string} Animation css.
|
||||
* @returns Animation css.
|
||||
*/
|
||||
getAnimations = () => {
|
||||
getAnimations = (): string => {
|
||||
return `
|
||||
/* Animations */
|
||||
@keyframes scaleInAnimation {
|
||||
@@ -202,10 +230,10 @@ class Card {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} body The inner body of the card.
|
||||
* @returns {string} The rendered card.
|
||||
* @param body The inner body of the card.
|
||||
* @returns The rendered card.
|
||||
*/
|
||||
render(body) {
|
||||
render(body: string): string {
|
||||
return `
|
||||
<svg
|
||||
width="${this.width}"
|
||||
@@ -221,7 +249,7 @@ class Card {
|
||||
<style>
|
||||
.header {
|
||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
fill: ${this.colors.titleColor};
|
||||
fill: ${String(this.colors.titleColor)};
|
||||
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
@@ -232,9 +260,9 @@ class Card {
|
||||
|
||||
${this.getAnimations()}
|
||||
${
|
||||
this.animations === false
|
||||
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
||||
: ""
|
||||
this.animations
|
||||
? ""
|
||||
: `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -246,12 +274,12 @@ class Card {
|
||||
y="0.5"
|
||||
rx="${this.border_radius}"
|
||||
height="99%"
|
||||
stroke="${this.colors.borderColor}"
|
||||
stroke="${String(this.colors.borderColor)}"
|
||||
width="${this.width - 1}"
|
||||
fill="${
|
||||
typeof this.colors.bgColor === "object"
|
||||
? "url(#gradient)"
|
||||
: this.colors.bgColor
|
||||
: String(this.colors.bgColor)
|
||||
}"
|
||||
stroke-opacity="${this.hideBorder ? 0 : 1}"
|
||||
/>
|
||||
@@ -1,42 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
const FALLBACK_LOCALE = "en";
|
||||
|
||||
/**
|
||||
* I18n translation class.
|
||||
*/
|
||||
class I18n {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Object} options Options.
|
||||
* @param {string=} options.locale Locale.
|
||||
* @param {any} options.translations Translations.
|
||||
*/
|
||||
constructor({ locale, translations }) {
|
||||
this.locale = locale || FALLBACK_LOCALE;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation.
|
||||
*
|
||||
* @param {string} str String to translate.
|
||||
* @returns {string} Translated string.
|
||||
*/
|
||||
t(str) {
|
||||
if (!this.translations[str]) {
|
||||
throw new Error(`${str} Translation string not found`);
|
||||
}
|
||||
|
||||
if (!this.translations[str][this.locale]) {
|
||||
throw new Error(
|
||||
`'${str}' translation not found for locale '${this.locale}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.translations[str][this.locale];
|
||||
}
|
||||
}
|
||||
|
||||
export { I18n };
|
||||
@@ -0,0 +1,49 @@
|
||||
const FALLBACK_LOCALE = "en";
|
||||
|
||||
/**
|
||||
* I18n translation class.
|
||||
*/
|
||||
class I18n<Translations extends Record<string, Record<string, string>>> {
|
||||
locale: string;
|
||||
translations: Translations;
|
||||
|
||||
/**
|
||||
* @param props Constructor arguments.
|
||||
* @param props.locale Locale.
|
||||
* @param props.translations Translations.
|
||||
*/
|
||||
constructor({
|
||||
locale,
|
||||
translations,
|
||||
}: {
|
||||
locale?: string;
|
||||
translations: Translations;
|
||||
}) {
|
||||
this.locale = locale || FALLBACK_LOCALE;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation.
|
||||
*
|
||||
* @param str String to translate.
|
||||
* @returns Translated string.
|
||||
*/
|
||||
t(str: keyof Translations & string): string {
|
||||
const translation = this.translations[str];
|
||||
if (!translation) {
|
||||
throw new Error(`${str} Translation string not found`);
|
||||
}
|
||||
|
||||
const localized = translation[this.locale];
|
||||
if (!localized) {
|
||||
throw new Error(
|
||||
`'${str}' translation not found for locale '${this.locale}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return localized;
|
||||
}
|
||||
}
|
||||
|
||||
export { I18n };
|
||||
@@ -1,14 +1,12 @@
|
||||
// @ts-check
|
||||
|
||||
import { themes } from "../themes/index.js";
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid hex color.
|
||||
*
|
||||
* @param {string} hexColor String to check.
|
||||
* @returns {boolean} True if the given string is a valid hex color.
|
||||
* @param hexColor String to check.
|
||||
* @returns True if the given string is a valid hex color.
|
||||
*/
|
||||
const isValidHexColor = (hexColor) => {
|
||||
const isValidHexColor = (hexColor: string): boolean => {
|
||||
return new RegExp(
|
||||
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
|
||||
).test(hexColor);
|
||||
@@ -17,10 +15,10 @@ const isValidHexColor = (hexColor) => {
|
||||
/**
|
||||
* Check if the given string is a valid gradient.
|
||||
*
|
||||
* @param {string[]} colors Array of colors.
|
||||
* @returns {boolean} True if the given string is a valid gradient.
|
||||
* @param colors Array of colors.
|
||||
* @returns True if the given string is a valid gradient.
|
||||
*/
|
||||
const isValidGradient = (colors) => {
|
||||
const isValidGradient = (colors: Array<string>): boolean => {
|
||||
return (
|
||||
colors.length > 2 &&
|
||||
colors.slice(1).every((color) => isValidHexColor(color))
|
||||
@@ -30,48 +28,50 @@ const isValidGradient = (colors) => {
|
||||
/**
|
||||
* Retrieves a gradient if color has more than one valid hex codes else a single color.
|
||||
*
|
||||
* @param {string} color The color to parse.
|
||||
* @param {string | string[]} fallbackColor The fallback color.
|
||||
* @returns {string | string[]} The gradient or color.
|
||||
* @param color The color to parse.
|
||||
* @param fallbackColor The fallback color.
|
||||
* @returns The gradient or color.
|
||||
*/
|
||||
const fallbackColor = (color, fallbackColor) => {
|
||||
let gradient = null;
|
||||
|
||||
let colors = color ? color.split(",") : [];
|
||||
const fallbackColor = (
|
||||
color: string | undefined,
|
||||
fallbackColor: string | Array<string>,
|
||||
): string | Array<string> => {
|
||||
const colors = color ? color.split(",") : [];
|
||||
if (colors.length > 1 && isValidGradient(colors)) {
|
||||
gradient = colors;
|
||||
return colors;
|
||||
}
|
||||
|
||||
return (
|
||||
(gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
|
||||
fallbackColor
|
||||
);
|
||||
if (color !== undefined && isValidHexColor(color)) {
|
||||
return `#${color}`;
|
||||
}
|
||||
|
||||
return fallbackColor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Object containing card colors.
|
||||
* @typedef {{
|
||||
* titleColor: string;
|
||||
* iconColor: string;
|
||||
* textColor: string;
|
||||
* bgColor: string | string[];
|
||||
* borderColor: string;
|
||||
* ringColor: string;
|
||||
* }} CardColors
|
||||
*/
|
||||
interface CardColors {
|
||||
titleColor: string;
|
||||
iconColor: string;
|
||||
textColor: string;
|
||||
bgColor: string | Array<string>;
|
||||
borderColor: string;
|
||||
ringColor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns theme based colors with proper overrides and defaults.
|
||||
*
|
||||
* @param {Object} args Function arguments.
|
||||
* @param {string=} args.title_color Card title color.
|
||||
* @param {string=} args.text_color Card text color.
|
||||
* @param {string=} args.icon_color Card icon color.
|
||||
* @param {string=} args.bg_color Card background color.
|
||||
* @param {string=} args.border_color Card border color.
|
||||
* @param {string=} args.ring_color Card ring color.
|
||||
* @param {string=} args.theme Card theme.
|
||||
* @returns {CardColors} Card colors.
|
||||
* @param props Function arguments.
|
||||
* @param props.title_color Card title color.
|
||||
* @param props.text_color Card text color.
|
||||
* @param props.icon_color Card icon color.
|
||||
* @param props.bg_color Card background color.
|
||||
* @param props.border_color Card border color.
|
||||
* @param props.ring_color Card ring color.
|
||||
* @param props.theme Card theme.
|
||||
* @returns Card colors.
|
||||
*/
|
||||
const getCardColors = ({
|
||||
title_color,
|
||||
@@ -81,19 +81,26 @@ const getCardColors = ({
|
||||
border_color,
|
||||
ring_color,
|
||||
theme,
|
||||
}) => {
|
||||
const defaultTheme = themes["default"];
|
||||
const isThemeProvided =
|
||||
theme !== null && theme !== undefined && theme in themes;
|
||||
}: {
|
||||
title_color?: string;
|
||||
text_color?: string;
|
||||
icon_color?: string;
|
||||
bg_color?: string;
|
||||
border_color?: string;
|
||||
ring_color?: string;
|
||||
theme?: string;
|
||||
}): CardColors => {
|
||||
const defaultTheme = themes.default;
|
||||
const isThemeProvided = theme !== undefined && theme in themes;
|
||||
|
||||
// @ts-ignore
|
||||
const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme;
|
||||
const selectedTheme = isThemeProvided
|
||||
? themes[theme as keyof typeof themes]
|
||||
: defaultTheme;
|
||||
|
||||
const defaultBorderColor =
|
||||
"border_color" in selectedTheme
|
||||
? selectedTheme.border_color
|
||||
: // @ts-ignore
|
||||
defaultTheme.border_color;
|
||||
: defaultTheme.border_color;
|
||||
|
||||
// get the color provided by the user else the theme color
|
||||
// finally if both colors are invalid fallback to default theme
|
||||
@@ -104,11 +111,8 @@ const getCardColors = ({
|
||||
|
||||
// get the color provided by the user else the theme color
|
||||
// finally if both colors are invalid we use the titleColor
|
||||
const ringColor = fallbackColor(
|
||||
// @ts-ignore
|
||||
ring_color || selectedTheme.ring_color,
|
||||
titleColor,
|
||||
);
|
||||
// NOTE: no built-in theme defines `ring_color`, so it falls back to the title color.
|
||||
const ringColor = fallbackColor(ring_color, titleColor);
|
||||
const iconColor = fallbackColor(
|
||||
icon_color || selectedTheme.icon_color,
|
||||
"#" + defaultTheme.icon_color,
|
||||
@@ -1,78 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {string | undefined} value Comma-separated string.
|
||||
* @returns {string[] | undefined} Parsed string values.
|
||||
*/
|
||||
const parseCsv = (value) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return value.split(",");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Record<string, string | undefined>} env Environment variables to inspect.
|
||||
* @returns {{name: string, value: string}[]} Personal access tokens found in the environment.
|
||||
*/
|
||||
const parsePATsFromEnv = (env) => {
|
||||
return Object.keys(env)
|
||||
.filter((key) => /PAT_\d*$/.exec(key))
|
||||
.map((name) => ({
|
||||
name,
|
||||
value: env[name],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Record<string, string | undefined>} `process.env` if available, otherwise `{}`.
|
||||
*/
|
||||
const getDefaultEnv = () => {
|
||||
if (typeof process !== "undefined" && process?.env) {
|
||||
return process.env;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Partial<ReturnType<typeof loadConfigFromEnv>>} config (Partial) config values to normalize.
|
||||
* @returns {{
|
||||
* whitelist: string[] | undefined,
|
||||
* gistWhitelist: string[] | undefined,
|
||||
* excludeRepositories: string[],
|
||||
* fetchMultiPageStars: string | undefined,
|
||||
* pats: {name: string, value: string}[],
|
||||
* }} Normalized config object with defaults applied.
|
||||
*/
|
||||
const normalizeConfig = (config = {}) => {
|
||||
return {
|
||||
whitelist: config.whitelist,
|
||||
gistWhitelist: config.gistWhitelist,
|
||||
excludeRepositories: config.excludeRepositories || [],
|
||||
fetchMultiPageStars: config.fetchMultiPageStars,
|
||||
pats: config.pats || [],
|
||||
};
|
||||
};
|
||||
|
||||
let currentConfig;
|
||||
|
||||
/**
|
||||
* @param {Record<string, string | undefined>} env Environment variables used to build the runtime config.
|
||||
*/
|
||||
export const loadConfigFromEnv = (env = getDefaultEnv()) => {
|
||||
const whitelist = parseCsv(env.WHITELIST);
|
||||
const gistWhitelist = parseCsv(env.GIST_WHITELIST);
|
||||
const excludeRepositories = parseCsv(env.EXCLUDE_REPO) || [];
|
||||
|
||||
currentConfig = normalizeConfig({
|
||||
whitelist,
|
||||
gistWhitelist,
|
||||
excludeRepositories,
|
||||
fetchMultiPageStars: env.FETCH_MULTI_PAGE_STARS,
|
||||
pats: parsePATsFromEnv(env),
|
||||
});
|
||||
};
|
||||
|
||||
loadConfigFromEnv();
|
||||
|
||||
export const getConfig = () => currentConfig;
|
||||
@@ -0,0 +1,83 @@
|
||||
type Env = Record<string, string | undefined>;
|
||||
|
||||
interface PersonalAccessToken {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
whitelist: Array<string> | undefined;
|
||||
gistWhitelist: Array<string> | undefined;
|
||||
excludeRepositories: Array<string>;
|
||||
fetchMultiPageStars: string | undefined;
|
||||
pats: Array<PersonalAccessToken>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value Comma-separated string.
|
||||
* @returns Parsed string values.
|
||||
*/
|
||||
const parseCsv = (value: string | undefined): Array<string> | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return value.split(",");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param env Environment variables to inspect.
|
||||
* @returns Personal access tokens found in the environment.
|
||||
*/
|
||||
const parsePATsFromEnv = (env: Env): Array<PersonalAccessToken> => {
|
||||
return Object.keys(env)
|
||||
.filter((key) => /PAT_\d*$/.exec(key))
|
||||
.map((name) => ({
|
||||
name,
|
||||
value: env[name] ?? "",
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns `process.env` if available, otherwise `{}`.
|
||||
*/
|
||||
const getDefaultEnv = (): Env => {
|
||||
const processEnv = (globalThis as { process?: { env?: Env } }).process?.env;
|
||||
return processEnv ?? {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param config (Partial) config values to normalize.
|
||||
* @returns Normalized config object with defaults applied.
|
||||
*/
|
||||
const normalizeConfig = (config: Partial<Config> = {}): Config => {
|
||||
return {
|
||||
whitelist: config.whitelist,
|
||||
gistWhitelist: config.gistWhitelist,
|
||||
excludeRepositories: config.excludeRepositories ?? [],
|
||||
fetchMultiPageStars: config.fetchMultiPageStars,
|
||||
pats: config.pats ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
let currentConfig: Config;
|
||||
|
||||
/**
|
||||
* @param env Environment variables used to build the runtime config.
|
||||
*/
|
||||
export const loadConfigFromEnv = (env: Env = getDefaultEnv()): void => {
|
||||
const whitelist = parseCsv(env["WHITELIST"]);
|
||||
const gistWhitelist = parseCsv(env["GIST_WHITELIST"]);
|
||||
const excludeRepositories = parseCsv(env["EXCLUDE_REPO"]) ?? [];
|
||||
|
||||
currentConfig = normalizeConfig({
|
||||
whitelist,
|
||||
gistWhitelist,
|
||||
excludeRepositories,
|
||||
fetchMultiPageStars: env["FETCH_MULTI_PAGE_STARS"],
|
||||
pats: parsePATsFromEnv(env),
|
||||
});
|
||||
};
|
||||
|
||||
loadConfigFromEnv();
|
||||
|
||||
export const getConfig = (): Config => currentConfig;
|
||||
@@ -1,9 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Valid owner affiliations for GitHub API queries.
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"];
|
||||
|
||||
export { OWNER_AFFILIATIONS };
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Valid owner affiliations for GitHub API queries.
|
||||
*/
|
||||
const OWNER_AFFILIATIONS: Array<string> = [
|
||||
"OWNER",
|
||||
"COLLABORATOR",
|
||||
"ORGANIZATION_MEMBER",
|
||||
];
|
||||
|
||||
export { OWNER_AFFILIATIONS };
|
||||
@@ -1,14 +1,12 @@
|
||||
// @ts-check
|
||||
|
||||
import { OWNER_AFFILIATIONS } from "./constants.js";
|
||||
|
||||
/**
|
||||
* @type {string} A general message to ask user to try again later.
|
||||
* A general message to ask user to try again later.
|
||||
*/
|
||||
const TRY_AGAIN_LATER = "Please try again later";
|
||||
|
||||
/**
|
||||
* @type {Object<string, string>} A map of error types to secondary error messages.
|
||||
* A map of error types to secondary error messages.
|
||||
*/
|
||||
const SECONDARY_ERROR_MESSAGES = {
|
||||
MAX_RETRY:
|
||||
@@ -28,16 +26,21 @@ const SECONDARY_ERROR_MESSAGES = {
|
||||
* Custom error class to handle custom GRS errors.
|
||||
*/
|
||||
class CustomError extends Error {
|
||||
type: string;
|
||||
secondaryMessage: string;
|
||||
|
||||
/**
|
||||
* Custom error constructor.
|
||||
*
|
||||
* @param {string} message Error message.
|
||||
* @param {string} type Error type.
|
||||
* @param message Error message.
|
||||
* @param type Error type.
|
||||
*/
|
||||
constructor(message, type) {
|
||||
constructor(message: string, type: string) {
|
||||
super(message);
|
||||
this.type = type;
|
||||
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
|
||||
type PartialRecord = Partial<Record<string, string>>;
|
||||
this.secondaryMessage =
|
||||
(SECONDARY_ERROR_MESSAGES as PartialRecord)[type] ?? type;
|
||||
}
|
||||
|
||||
static MAX_RETRY = "MAX_RETRY";
|
||||
@@ -53,13 +56,16 @@ class CustomError extends Error {
|
||||
* Missing query parameter class.
|
||||
*/
|
||||
class MissingParamError extends Error {
|
||||
missedParams: Array<string>;
|
||||
secondaryMessage: string | undefined;
|
||||
|
||||
/**
|
||||
* Missing query parameter error constructor.
|
||||
*
|
||||
* @param {string[]} missedParams An array of missing parameters names.
|
||||
* @param {string=} secondaryMessage Optional secondary message to display.
|
||||
* @param missedParams An array of missing parameters names.
|
||||
* @param secondaryMessage Optional secondary message to display.
|
||||
*/
|
||||
constructor(missedParams, secondaryMessage) {
|
||||
constructor(missedParams: Array<string>, secondaryMessage?: string) {
|
||||
const msg = `Missing params ${missedParams
|
||||
.map((p) => `"${p}"`)
|
||||
.join(", ")} make sure you pass the parameters in URL`;
|
||||
@@ -72,10 +78,10 @@ class MissingParamError extends Error {
|
||||
/**
|
||||
* Retrieve secondary message from an error object.
|
||||
*
|
||||
* @param {Error} err The error object.
|
||||
* @returns {string|undefined} The secondary message if available, otherwise undefined.
|
||||
* @param err The error object.
|
||||
* @returns The secondary message if available, otherwise undefined.
|
||||
*/
|
||||
const retrieveSecondaryMessage = (err) => {
|
||||
const retrieveSecondaryMessage = (err: Error): string | undefined => {
|
||||
return "secondaryMessage" in err && typeof err.secondaryMessage === "string"
|
||||
? err.secondaryMessage
|
||||
: undefined;
|
||||
@@ -1,16 +1,14 @@
|
||||
// @ts-check
|
||||
|
||||
import { encodeHTML } from "./html.js";
|
||||
import { splitWrappedText } from "./render.js";
|
||||
|
||||
/**
|
||||
* Retrieves num with suffix k(thousands) precise to given decimal places.
|
||||
*
|
||||
* @param {number} num The number to format.
|
||||
* @param {number=} precision The number of decimal places to include.
|
||||
* @returns {string|number} The formatted number.
|
||||
* @param num The number to format.
|
||||
* @param precision The number of decimal places to include.
|
||||
* @returns The formatted number.
|
||||
*/
|
||||
const kFormatter = (num, precision) => {
|
||||
const kFormatter = (num: number, precision?: number): string | number => {
|
||||
const abs = Math.abs(num);
|
||||
const sign = Math.sign(num);
|
||||
|
||||
@@ -22,17 +20,17 @@ const kFormatter = (num, precision) => {
|
||||
return sign * abs;
|
||||
}
|
||||
|
||||
return sign * parseFloat((abs / 1000).toFixed(1)) + "k";
|
||||
return `${sign * parseFloat((abs / 1000).toFixed(1))}k`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert bytes to a human-readable string representation.
|
||||
*
|
||||
* @param {number} bytes The number of bytes to convert.
|
||||
* @returns {string} The human-readable representation of bytes.
|
||||
* @param bytes The number of bytes to convert.
|
||||
* @returns The human-readable representation of bytes.
|
||||
* @throws {Error} If bytes is negative or too large.
|
||||
*/
|
||||
const formatBytes = (bytes) => {
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes < 0) {
|
||||
throw new Error("Bytes must be a non-negative number");
|
||||
}
|
||||
@@ -45,23 +43,29 @@ const formatBytes = (bytes) => {
|
||||
const base = 1024;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(base));
|
||||
|
||||
if (i >= sizes.length) {
|
||||
const unit = sizes[i];
|
||||
if (unit === undefined) {
|
||||
throw new Error("Bytes is too large to convert to a human-readable string");
|
||||
}
|
||||
|
||||
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${sizes[i]}`;
|
||||
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${unit}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Split text over multiple lines based on the card width.
|
||||
*
|
||||
* @param {string} text Text to split.
|
||||
* @param {number} width Available wrap width in px.
|
||||
* @param {number} fontSize Font size in px.
|
||||
* @param {number} maxLines Maximum number of lines.
|
||||
* @returns {string[]} Array of lines.
|
||||
* @param text Text to split.
|
||||
* @param width Available wrap width in px.
|
||||
* @param fontSize Font size in px.
|
||||
* @param maxLines Maximum number of lines.
|
||||
* @returns Array of lines.
|
||||
*/
|
||||
const wrapTextMultiline = (text, width, fontSize, maxLines = 3) => {
|
||||
const wrapTextMultiline = (
|
||||
text: string,
|
||||
width: number,
|
||||
fontSize: number,
|
||||
maxLines = 3,
|
||||
): Array<string> => {
|
||||
const wrapped = splitWrappedText(text, fontSize, width);
|
||||
const lines = wrapped
|
||||
.map((line) => encodeHTML(line.trim()))
|
||||
@@ -69,7 +73,11 @@ const wrapTextMultiline = (text, width, fontSize, maxLines = 3) => {
|
||||
|
||||
// Add "..." to the last line if the text exceeds maxLines
|
||||
if (wrapped.length > maxLines) {
|
||||
lines[maxLines - 1] += "...";
|
||||
const lastIndex = maxLines - 1;
|
||||
const lastLine = lines[lastIndex];
|
||||
if (lastLine !== undefined) {
|
||||
lines[lastIndex] = `${lastLine}...`;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty lines if text fits in less than maxLines lines
|
||||
@@ -1,18 +1,13 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Encode string as HTML.
|
||||
*
|
||||
* @see https://stackoverflow.com/a/48073476/10629172
|
||||
*
|
||||
* @param {string} str String to encode.
|
||||
* @returns {string} Encoded string.
|
||||
*/
|
||||
const encodeHTML = (str) => {
|
||||
const encodeHTML = (str: string): string => {
|
||||
return (
|
||||
str
|
||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i: string) => {
|
||||
return `&#${i.charCodeAt(0)};`;
|
||||
})
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/\u0008/gim, "")
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
/*
|
||||
The icons in this file are based on https://github.com/primer/octicons which is released under the MIT license:
|
||||
|
||||
@@ -46,12 +44,16 @@ const icons = {
|
||||
/**
|
||||
* Get rank icon
|
||||
*
|
||||
* @param {string} rankIcon - The rank icon type.
|
||||
* @param {string} rankLevel - The rank level.
|
||||
* @param {number} percentile - The rank percentile.
|
||||
* @returns {string} - The SVG code of the rank icon
|
||||
* @param rankIcon - The rank icon type.
|
||||
* @param rankLevel - The rank level.
|
||||
* @param percentile - The rank percentile.
|
||||
* @returns The SVG code of the rank icon
|
||||
*/
|
||||
const rankIcon = (rankIcon, rankLevel, percentile) => {
|
||||
const rankIcon = (
|
||||
rankIcon: string,
|
||||
rankLevel: string,
|
||||
percentile: number,
|
||||
): string => {
|
||||
switch (rankIcon) {
|
||||
case "github":
|
||||
return `
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import toEmoji from "emoji-name-map";
|
||||
|
||||
import { OWNER_AFFILIATIONS } from "./constants.js";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { getCardColors } from "./color.js";
|
||||
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
|
||||
import { encodeHTML } from "./html.js";
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
// @ts-check
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "./config.js";
|
||||
import { CustomError } from "./error.js";
|
||||
import { logger } from "./log.js";
|
||||
|
||||
/**
|
||||
* Subset of the response payload the retryer inspects to detect
|
||||
* rate-limiting and credential failures.
|
||||
*/
|
||||
interface ResponseData {
|
||||
errors?: Array<{ type?: string; message?: string }>;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random integer from 0 (inclusive) to `max` (exclusive).
|
||||
*
|
||||
* The value is generated using `Math.random()` and uniformly distributed
|
||||
* across the range.
|
||||
*
|
||||
* @param {number} max The upper bound (exclusive). Must be a positive number.
|
||||
* @param max The upper bound (exclusive). Must be a positive number.
|
||||
*
|
||||
* @returns {number} A random integer `n` such that `0 <= n < max`.
|
||||
* @returns A random integer `n` such that `0 <= n < max`.
|
||||
*/
|
||||
function getRandomInt(max) {
|
||||
function getRandomInt(max: number): number {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import("axios").AxiosResponse} AxiosResponse Axios response.
|
||||
* @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.
|
||||
*/
|
||||
type FetcherResponse = AxiosResponse<ResponseData>;
|
||||
|
||||
type FetcherFunction = (
|
||||
variables: Record<string, unknown>,
|
||||
token: string,
|
||||
retriesForTests?: number,
|
||||
) => Promise<FetcherResponse>;
|
||||
|
||||
/**
|
||||
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
|
||||
*
|
||||
* @param {FetcherFunction} fetcher The fetcher function.
|
||||
* @param {any} variables Object with arguments to pass to the fetcher function.
|
||||
* @param {string | null} pat Optional PAT override.
|
||||
* @returns {Promise<any>} The response from the fetcher function.
|
||||
* @param fetcher The fetcher function.
|
||||
* @param variables Object with arguments to pass to the fetcher function.
|
||||
* @param pat Optional PAT override.
|
||||
* @returns The response from the fetcher function.
|
||||
*/
|
||||
const retryer = async (fetcher, variables, pat = null) => {
|
||||
let PATs;
|
||||
if (pat) {
|
||||
PATs = [{ name: "user PAT from database", value: pat }];
|
||||
} else {
|
||||
PATs = getConfig().pats;
|
||||
}
|
||||
const retryer = async (
|
||||
fetcher: FetcherFunction,
|
||||
variables: Record<string, unknown>,
|
||||
pat: string | null = null,
|
||||
): Promise<FetcherResponse> => {
|
||||
const PATs = pat
|
||||
? [{ name: "user PAT from database", value: pat }]
|
||||
: getConfig().pats;
|
||||
|
||||
if (!PATs.length) {
|
||||
throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS);
|
||||
@@ -46,11 +59,13 @@ const retryer = async (fetcher, variables, pat = null) => {
|
||||
|
||||
for (let retries = 0; retries < PATs.length; retries++) {
|
||||
const currentPAT = PATs[(startPAT + retries) % PATs.length];
|
||||
if (!currentPAT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await fetcher(
|
||||
const response = await fetcher(
|
||||
variables,
|
||||
// @ts-ignore
|
||||
currentPAT.value,
|
||||
// used in tests for faking rate limit
|
||||
retries,
|
||||
@@ -58,11 +73,11 @@ const retryer = async (fetcher, variables, pat = null) => {
|
||||
|
||||
// react on both type and message-based rate-limit signals.
|
||||
// https://github.com/anuraghazra/github-readme-stats/issues/4425
|
||||
const errors = response?.data?.errors;
|
||||
const errors = response.data.errors;
|
||||
const errorType = errors?.[0]?.type;
|
||||
const errorMsg = errors?.[0]?.message || "";
|
||||
const errorMsg = errors?.[0]?.message ?? "";
|
||||
const isRateLimited =
|
||||
(errors && errorType === "RATE_LIMITED") ||
|
||||
(!!errors && errorType === "RATE_LIMITED") ||
|
||||
/rate limit/i.test(errorMsg);
|
||||
|
||||
if (isRateLimited) {
|
||||
@@ -71,20 +86,18 @@ const retryer = async (fetcher, variables, pat = null) => {
|
||||
return response;
|
||||
}
|
||||
} catch (err) {
|
||||
/** @type {any} */
|
||||
const e = err;
|
||||
const e = err as { response?: FetcherResponse };
|
||||
|
||||
// network/unexpected error → let caller treat as failure
|
||||
if (!e?.response) {
|
||||
throw e;
|
||||
if (!e.response) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
// also checking for bad credentials if any tokens gets invalidated
|
||||
const isBadCredential =
|
||||
e?.response?.data?.message === "Bad credentials";
|
||||
const message = e.response.data.message;
|
||||
const isBadCredential = message === "Bad credentials";
|
||||
const isAccountSuspended =
|
||||
e?.response?.data?.message === "Sorry. Your account was suspended.";
|
||||
message === "Sorry. Your account was suspended.";
|
||||
|
||||
if (isBadCredential || isAccountSuspended) {
|
||||
logger.log(`${currentPAT.name} Failed due to bad credentials`);
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { MissingParamError } from "../common/error.js";
|
||||
import { request } from "../common/http.js";
|
||||
import { retryer } from "../common/retryer.js";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { MissingParamError } from "../common/error.js";
|
||||
import { request } from "../common/http.js";
|
||||
import { retryer } from "../common/retryer.js";
|
||||
@@ -23,9 +21,7 @@ const fetcher = (variables, token) => {
|
||||
isPrivate
|
||||
isArchived
|
||||
isTemplate
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
stargazerCount
|
||||
description
|
||||
primaryLanguage {
|
||||
color
|
||||
@@ -130,7 +126,7 @@ const fetchRepo = async (
|
||||
return {
|
||||
...repoUserStats,
|
||||
...data.user.repository,
|
||||
starCount: data.user.repository.stargazers.totalCount,
|
||||
starCount: data.user.repository.stargazerCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,7 +151,7 @@ const fetchRepo = async (
|
||||
return {
|
||||
...repoUserStats,
|
||||
...data.organization.repository,
|
||||
starCount: data.organization.repository.stargazers.totalCount,
|
||||
starCount: data.organization.repository.stargazerCount,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import axios from "axios";
|
||||
import githubUsernameRegex from "github-username-regex";
|
||||
|
||||
@@ -18,9 +16,7 @@ const GRAPHQL_REPOS_FIELD = `
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
stargazerCount
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
@@ -159,7 +155,7 @@ const statsFetcher = async ({
|
||||
// Disable multi page fetching on public Vercel instance due to rate limits.
|
||||
fetchedPages++;
|
||||
const repoNodesWithStars = repoNodes.filter(
|
||||
(node) => node.stargazers.totalCount !== 0,
|
||||
(node) => node.stargazerCount !== 0,
|
||||
);
|
||||
|
||||
hasNextPage =
|
||||
@@ -458,7 +454,7 @@ const fetchStats = async (
|
||||
return !repoToHide.has(data.name);
|
||||
})
|
||||
.reduce((prev, curr) => {
|
||||
return prev + curr.stargazers.totalCount;
|
||||
return prev + curr.stargazerCount;
|
||||
}, 0);
|
||||
|
||||
stats.rank = calculateRank({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { getConfig } from "../common/config.js";
|
||||
import { CustomError, MissingParamError } from "../common/error.js";
|
||||
import { wrapTextMultiline } from "../common/fmt.js";
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export interface RepositoryData {
|
||||
isPrivate: boolean;
|
||||
isArchived: boolean;
|
||||
isTemplate: boolean;
|
||||
stargazers: { totalCount: number };
|
||||
stargazerCount: number;
|
||||
description: string;
|
||||
primaryLanguage: {
|
||||
color: string;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { CustomError, MissingParamError } from "../common/error.js";
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
// @ts-check
|
||||
|
||||
import { encodeHTML } from "./common/html.js";
|
||||
|
||||
/**
|
||||
* Retrieves stat card labels in the available locales.
|
||||
*
|
||||
* @param {object} props Function arguments.
|
||||
* @param {string} props.name The name of the locale.
|
||||
* @param {string} props.apostrophe Whether to use apostrophe or not.
|
||||
* @returns {object} The locales object.
|
||||
* @param props Function arguments.
|
||||
* @param props.name The name of the locale.
|
||||
* @param props.apostrophe Whether to use apostrophe or not.
|
||||
* @returns The locales object.
|
||||
*
|
||||
* @see https://www.andiamo.co.uk/resources/iso-language-codes/ for language codes.
|
||||
*/
|
||||
const statCardLocales = ({ name, apostrophe }) => {
|
||||
const statCardLocales = ({
|
||||
name,
|
||||
apostrophe,
|
||||
}: {
|
||||
name: string;
|
||||
apostrophe: string;
|
||||
}) => {
|
||||
const encodedName = encodeHTML(name);
|
||||
return {
|
||||
"statcard.title": {
|
||||
@@ -1140,10 +1144,10 @@ const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
|
||||
/**
|
||||
* Checks whether the locale is available or not.
|
||||
*
|
||||
* @param {string} locale The locale to check.
|
||||
* @returns {boolean} Boolean specifying whether the locale is available or not.
|
||||
* @param locale The locale to check.
|
||||
* @returns Boolean specifying whether the locale is available or not.
|
||||
*/
|
||||
const isLocaleAvailable = (locale) => {
|
||||
const isLocaleAvailable = (locale: string): boolean => {
|
||||
return availableLocales.includes(locale.toLowerCase());
|
||||
};
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* `@uppercod/css-to-object` ships type definitions but does not expose them
|
||||
* through its package.json `exports` map, so they cannot be resolved under
|
||||
* `nodenext`. Declare the minimal surface used in tests here.
|
||||
*/
|
||||
declare module "@uppercod/css-to-object" {
|
||||
export type CssObject = Record<
|
||||
string,
|
||||
Record<string, Record<string, string>>
|
||||
>;
|
||||
export function cssToObject(css: string): CssObject;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import * as matchers from "@testing-library/jest-dom/matchers";
|
||||
import { expect } from "vitest";
|
||||
|
||||
expect.extend(matchers);
|
||||
|
||||
process.env.PAT_1 = "dummyPAT1";
|
||||
process.env.PAT_2 = "dummyPAT2";
|
||||
@@ -0,0 +1,4 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
process.env["PAT_1"] = "dummyPAT1";
|
||||
process.env["PAT_2"] = "dummyPAT2";
|
||||
@@ -1,12 +1,16 @@
|
||||
import { queryByTestId } from "@testing-library/dom";
|
||||
import { cssToObject } from "@uppercod/css-to-object";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { Card } from "../src/common/Card.js";
|
||||
import { getCardColors } from "../src/common/color.js";
|
||||
import { icons } from "../src/common/icons.js";
|
||||
|
||||
describe("Card", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("should hide border", () => {
|
||||
const card = new Card({});
|
||||
card.setHideBorder(true);
|
||||
@@ -68,21 +72,21 @@ describe("Card", () => {
|
||||
});
|
||||
|
||||
it("title should have prefix icon", () => {
|
||||
const card = new Card({ title: "ok", titlePrefixIcon: icons.contribs });
|
||||
const card = new Card({ titlePrefixIcon: icons.contribs });
|
||||
|
||||
document.body.innerHTML = card.render(``);
|
||||
expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("title should not have prefix icon", () => {
|
||||
const card = new Card({ title: "ok" });
|
||||
const card = new Card({});
|
||||
|
||||
document.body.innerHTML = card.render(``);
|
||||
expect(document.getElementsByClassName("icon")[0]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should have proper height, width", () => {
|
||||
const card = new Card({ height: 200, width: 200, title: "ok" });
|
||||
const card = new Card({ height: 200, width: 200 });
|
||||
document.body.innerHTML = card.render(``);
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
@@ -95,7 +99,7 @@ describe("Card", () => {
|
||||
});
|
||||
|
||||
it("should have less height after title is hidden", () => {
|
||||
const card = new Card({ height: 200, title: "ok" });
|
||||
const card = new Card({ height: 200 });
|
||||
card.setHideTitle(true);
|
||||
|
||||
document.body.innerHTML = card.render(``);
|
||||
@@ -147,10 +151,10 @@ describe("Card", () => {
|
||||
document.body.innerHTML = card.render(``);
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
const headerClassStyles = stylesObject[":host"][".header "];
|
||||
const stylesObject = cssToObject(styleTag?.innerHTML ?? "");
|
||||
const headerClassStyles = stylesObject[":host"]?.[".header "];
|
||||
|
||||
expect(headerClassStyles["fill"].trim()).toBe("#f00");
|
||||
expect(headerClassStyles?.["fill"]?.trim()).toBe("#f00");
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#fff",
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getCardColors } from "../src/common/color";
|
||||
import { getCardColors } from "../src/common/color.js";
|
||||
|
||||
describe("Test color.js", () => {
|
||||
it("getCardColors: should return expected values", () => {
|
||||
let colors = getCardColors({
|
||||
const colors = getCardColors({
|
||||
title_color: "f00",
|
||||
text_color: "0f0",
|
||||
ring_color: "0000ff",
|
||||
@@ -24,7 +24,7 @@ describe("Test color.js", () => {
|
||||
});
|
||||
|
||||
it("getCardColors: should fallback to default colors if color is invalid", () => {
|
||||
let colors = getCardColors({
|
||||
const colors = getCardColors({
|
||||
title_color: "invalidcolor",
|
||||
text_color: "0f0",
|
||||
icon_color: "00f",
|
||||
@@ -43,7 +43,7 @@ describe("Test color.js", () => {
|
||||
});
|
||||
|
||||
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
|
||||
let colors = getCardColors({
|
||||
const colors = getCardColors({
|
||||
theme: "dark",
|
||||
});
|
||||
expect(colors).toStrictEqual({
|
||||
@@ -57,7 +57,7 @@ describe("Test color.js", () => {
|
||||
});
|
||||
|
||||
it("getCardColors: should return ring color equal to title color if not ring color is defined", () => {
|
||||
let colors = getCardColors({
|
||||
const colors = getCardColors({
|
||||
title_color: "f00",
|
||||
text_color: "0f0",
|
||||
icon_color: "00f",
|
||||
@@ -76,7 +76,7 @@ describe("Test color.js", () => {
|
||||
});
|
||||
|
||||
it("getCardColors: should fallback to default theme if theme is invalid", () => {
|
||||
let colors = getCardColors({
|
||||
const colors = getCardColors({
|
||||
theme: "invalidTheme",
|
||||
});
|
||||
expect(colors).toStrictEqual({
|
||||
@@ -7,7 +7,7 @@ import { fetchRepo } from "../src/fetchers/repo.js";
|
||||
const data_repo = {
|
||||
repository: {
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
stargazerCount: 38000,
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
primaryLanguage: {
|
||||
color: "#2b7489",
|
||||
@@ -46,7 +46,7 @@ describe("Test fetchRepo", () => {
|
||||
|
||||
expect(repo).toStrictEqual({
|
||||
...data_repo.repository,
|
||||
starCount: data_repo.repository.stargazers.totalCount,
|
||||
starCount: data_repo.repository.stargazerCount,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("Test fetchRepo", () => {
|
||||
let repo = await fetchRepo("anuraghazra", "convoychat");
|
||||
expect(repo).toStrictEqual({
|
||||
...data_repo.repository,
|
||||
starCount: data_repo.repository.stargazers.totalCount,
|
||||
starCount: data_repo.repository.stargazerCount,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ const data_stats = {
|
||||
repositories: {
|
||||
totalCount: 5,
|
||||
nodes: [
|
||||
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-3", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-1", stargazerCount: 100 },
|
||||
{ name: "test-repo-2", stargazerCount: 100 },
|
||||
{ name: "test-repo-3", stargazerCount: 100 },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
@@ -64,8 +64,8 @@ const data_repo = {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{ name: "test-repo-4", stargazers: { totalCount: 50 } },
|
||||
{ name: "test-repo-5", stargazers: { totalCount: 50 } },
|
||||
{ name: "test-repo-4", stargazerCount: 50 },
|
||||
{ name: "test-repo-5", stargazerCount: 50 },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
@@ -81,11 +81,11 @@ const data_repo_zero_stars = {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-3", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-4", stargazers: { totalCount: 0 } },
|
||||
{ name: "test-repo-5", stargazers: { totalCount: 0 } },
|
||||
{ name: "test-repo-1", stargazerCount: 100 },
|
||||
{ name: "test-repo-2", stargazerCount: 100 },
|
||||
{ name: "test-repo-3", stargazerCount: 100 },
|
||||
{ name: "test-repo-4", stargazerCount: 0 },
|
||||
{ name: "test-repo-5", stargazerCount: 0 },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("Test fmt.js", () => {
|
||||
|
||||
it("wrapTextMultiline: should not wrap small texts", () => {
|
||||
{
|
||||
let multiLineText = wrapTextMultiline(
|
||||
const multiLineText = wrapTextMultiline(
|
||||
"Small text should not wrap",
|
||||
130,
|
||||
11,
|
||||
@@ -83,7 +83,7 @@ describe("Test fmt.js", () => {
|
||||
});
|
||||
|
||||
it("wrapTextMultiline: should wrap large texts", () => {
|
||||
let multiLineText = wrapTextMultiline(
|
||||
const multiLineText = wrapTextMultiline(
|
||||
"Hello world long long long text",
|
||||
130,
|
||||
11,
|
||||
@@ -93,7 +93,7 @@ describe("Test fmt.js", () => {
|
||||
});
|
||||
|
||||
it("wrapTextMultiline: should wrap large texts and limit max lines", () => {
|
||||
let multiLineText = wrapTextMultiline(
|
||||
const multiLineText = wrapTextMultiline(
|
||||
"Hello world long long long text",
|
||||
53,
|
||||
11,
|
||||
@@ -103,13 +103,13 @@ describe("Test fmt.js", () => {
|
||||
});
|
||||
|
||||
it("wrapTextMultiline: should handle chinese characters", () => {
|
||||
let multiLineText = wrapTextMultiline(
|
||||
const multiLineText = wrapTextMultiline(
|
||||
"专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!",
|
||||
130,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
expect(multiLineText.length).toEqual(3);
|
||||
expect(multiLineText[0].length).toEqual(11 * 8); // &#xxxxx; x 8
|
||||
expect(multiLineText).toHaveLength(3);
|
||||
expect(multiLineText[0]).toHaveLength(11 * 8); // &#xxxxx; x 8
|
||||
});
|
||||
});
|
||||
@@ -17,9 +17,10 @@ describe("I18n", () => {
|
||||
locale: "en",
|
||||
translations: statCardLocales({ name: "Anurag Hazra", apostrophe: "s" }),
|
||||
});
|
||||
expect(() => i18n.t("statcard.title1")).toThrow(
|
||||
"statcard.title1 Translation string not found",
|
||||
);
|
||||
expect(
|
||||
// @ts-expect-error using a non-existing key should be reported by ts
|
||||
() => i18n.t("statcard.title1"),
|
||||
).toThrow("statcard.title1 Translation string not found");
|
||||
});
|
||||
|
||||
it("should throw error if translation not found for locale", () => {
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import { queryByTestId } from "@testing-library/dom";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-check
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { retryer } from "../src/common/retryer.js";
|
||||
|
||||
type Fetcher = Parameters<typeof retryer>[0];
|
||||
|
||||
vi.mock(import("../src/common/log.js"), async () => {
|
||||
const { createLoggerMock } = await import("./utils.js");
|
||||
return createLoggerMock();
|
||||
@@ -13,14 +13,14 @@ const fetcher = vi.fn().mockResolvedValue({ data: "ok" });
|
||||
|
||||
const fetcherFail = vi.fn().mockResolvedValue({
|
||||
data: { errors: [{ type: "RATE_LIMITED" }] },
|
||||
});
|
||||
}) as unknown as Fetcher;
|
||||
|
||||
const fetcherFailOnSecondTry = vi.fn((_vars, _token, retries) => {
|
||||
if (retries < 1) {
|
||||
return Promise.resolve({ data: { errors: [{ type: "RATE_LIMITED" }] } });
|
||||
}
|
||||
return Promise.resolve({ data: "ok" });
|
||||
});
|
||||
}) as unknown as Fetcher;
|
||||
|
||||
const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
|
||||
(_vars, _token, retries) => {
|
||||
@@ -38,29 +38,29 @@ const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
|
||||
}
|
||||
return Promise.resolve({ data: "ok" });
|
||||
},
|
||||
);
|
||||
) as unknown as Fetcher;
|
||||
|
||||
const customFetcher = vi.fn((variables, token) => {
|
||||
const customFetcher = vi.fn((_variables: unknown, token: string) => {
|
||||
return Promise.resolve({ data: { token } });
|
||||
});
|
||||
}) as unknown as Fetcher;
|
||||
|
||||
describe("Test Retryer", () => {
|
||||
it("retryer should return value and have zero retries on first try", async () => {
|
||||
let res = await retryer(fetcher, {});
|
||||
const res = await retryer(fetcher, {});
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
});
|
||||
|
||||
it("retryer should return value and have 2 retries", async () => {
|
||||
let res = await retryer(fetcherFailOnSecondTry, {});
|
||||
const res = await retryer(fetcherFailOnSecondTry, {});
|
||||
|
||||
expect(fetcherFailOnSecondTry).toHaveBeenCalledTimes(2);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
});
|
||||
|
||||
it("retryer should return value and have 2 retries with message based rate limit error", async () => {
|
||||
let res = await retryer(fetcherFailWithMessageBasedRateLimitErr, {});
|
||||
const res = await retryer(fetcherFailWithMessageBasedRateLimitErr, {});
|
||||
|
||||
expect(fetcherFailWithMessageBasedRateLimitErr).toHaveBeenCalledTimes(2);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
@@ -16,8 +16,7 @@ import type * as loggerModule from "../src/common/log.js";
|
||||
* For example, with `precision = 3`, values must be within `0.001`.
|
||||
*
|
||||
* @param expected The expected numeric value to compare against.
|
||||
*
|
||||
* @param
|
||||
* @param precision
|
||||
* The number of decimal places of tolerance. Higher values mean stricter
|
||||
* comparison. Internally converted to epsilon = 10^-precision.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"customConditions": []
|
||||
"customConditions": [],
|
||||
|
||||
// Typecheck is performed in a separate task
|
||||
"noCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,7 @@
|
||||
"extends": ["../../tsconfig.base.json"],
|
||||
"include": ["src", "tests", "vitest.config.ts"],
|
||||
"compilerOptions": {
|
||||
/**
|
||||
* This workspace contains a lot of type errors.
|
||||
* We will add typecheck later:
|
||||
* @see https://github.com/stats-organization/github-stats-extended/issues/140
|
||||
*
|
||||
* At the moment we just need to output `js` and `.d.ts` files
|
||||
*/
|
||||
"noCheck": true,
|
||||
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
|
||||
@@ -4,6 +4,6 @@ export default defineProject({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
include: ["./tests/*.test.{ts,js}"],
|
||||
setupFiles: ["./tests/_setup.js"],
|
||||
setupFiles: ["./tests/_setup.ts"],
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+54
-54
@@ -36,8 +36,8 @@ importers:
|
||||
specifier: 1.60.0
|
||||
version: 1.60.0
|
||||
'@types/node':
|
||||
specifier: 24.13.1
|
||||
version: 24.13.1
|
||||
specifier: 24.13.2
|
||||
version: 24.13.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: catalog:default
|
||||
version: 4.1.8(vitest@4.1.8)
|
||||
@@ -75,8 +75,8 @@ importers:
|
||||
specifier: 3.8.4
|
||||
version: 3.8.4
|
||||
turbo:
|
||||
specifier: 2.9.17
|
||||
version: 2.9.17
|
||||
specifier: 2.9.18
|
||||
version: 2.9.18
|
||||
typescript:
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
@@ -85,7 +85,7 @@ importers:
|
||||
version: 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
||||
vitest:
|
||||
specifier: catalog:default
|
||||
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
|
||||
apps/backend:
|
||||
dependencies:
|
||||
@@ -110,7 +110,7 @@ importers:
|
||||
version: 29.1.1
|
||||
vitest:
|
||||
specifier: catalog:default
|
||||
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
|
||||
apps/frontend:
|
||||
dependencies:
|
||||
@@ -125,7 +125,7 @@ importers:
|
||||
version: link:../../packages/core
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
version: 4.3.0(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
axios:
|
||||
specifier: catalog:default
|
||||
version: 1.17.0
|
||||
@@ -174,7 +174,7 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.17)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: 6.0.2
|
||||
version: 6.0.2(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
version: 6.0.2(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
clsx:
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
@@ -183,10 +183,10 @@ importers:
|
||||
version: 4.3.0
|
||||
vite:
|
||||
specifier: catalog:default
|
||||
version: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
|
||||
version: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
vitest:
|
||||
specifier: catalog:default
|
||||
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
@@ -220,7 +220,7 @@ importers:
|
||||
version: 29.1.1
|
||||
vitest:
|
||||
specifier: catalog:default
|
||||
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -938,33 +938,33 @@ packages:
|
||||
resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
|
||||
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
|
||||
|
||||
'@turbo/darwin-64@2.9.17':
|
||||
resolution: {integrity: sha512-io5jn5RDeU+9YV78rWhwG++HD/OZ/Lxg1sg93+jDGKQNP3UDxY6RX2dmarbCILhNxNuAM8FH3WgGMY9E96Mf8w==}
|
||||
'@turbo/darwin-64@2.9.18':
|
||||
resolution: {integrity: sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@turbo/darwin-arm64@2.9.17':
|
||||
resolution: {integrity: sha512-83YZTYmN2sxFWf2LTMOwqbOvR3qZMa/TSFwnB6BHVBbIWyoPPe+TAdSTd8KevEx8ml8KkycJ/9A70DFVReyUww==}
|
||||
'@turbo/darwin-arm64@2.9.18':
|
||||
resolution: {integrity: sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@turbo/linux-64@2.9.17':
|
||||
resolution: {integrity: sha512-teKfwJg0zSC+C2ZSOsX3VnAJGVgcN+pgKNmnGWzcpXQ9eIkAQtYP+getrQ2f1Tw/ePudnreQhq8tVP8S73Vy6Q==}
|
||||
'@turbo/linux-64@2.9.18':
|
||||
resolution: {integrity: sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@turbo/linux-arm64@2.9.17':
|
||||
resolution: {integrity: sha512-mqO36x2CNtJ9CCbEf5xOqH662tVSc1wB0mxh7dopBpgXg0fEifdzwX0IEAUW1WUAaNH986L7iEUUgw3HNqUWgg==}
|
||||
'@turbo/linux-arm64@2.9.18':
|
||||
resolution: {integrity: sha512-Va1kXI04naMgYwqv/5Dfa36dTDx8015U7oaQAjrXa45ua9OoFjSV4OmvkML4EmXvUclQHCiBRbY8bvd0jV7eAg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@turbo/windows-64@2.9.17':
|
||||
resolution: {integrity: sha512-jbyoNePufyMoSSrvVr+/mglcjmya/MOgoIrSHPr67iZ1VFgrlMQXHXtptR2lR48gi+86b1XBvsviJZ9A7zrydg==}
|
||||
'@turbo/windows-64@2.9.18':
|
||||
resolution: {integrity: sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@turbo/windows-arm64@2.9.17':
|
||||
resolution: {integrity: sha512-+ql0wYc99Y2AMvyHCcC/P+xtyV4nz522L+C9HDnyi7ryHXBGM+ZjBP28M7SLBGMDImgpN8sk2szpgbvreMeXVA==}
|
||||
'@turbo/windows-arm64@2.9.18':
|
||||
resolution: {integrity: sha512-nUdR8WqoomUys9iIQmG45TMiizJ+5BV8egSeLLZba/AWblyp3fVBcIH1kSE58OtK4g2YzbMJEth6Ttv9w5rqMA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
@@ -992,8 +992,8 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@24.13.1':
|
||||
resolution: {integrity: sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==}
|
||||
'@types/node@24.13.2':
|
||||
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
|
||||
|
||||
'@types/react-dom@19.2.3':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
@@ -2917,8 +2917,8 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
turbo@2.9.17:
|
||||
resolution: {integrity: sha512-91Q3KxfHJn7esFu2Ic6j9pkvQqWjncQCOp7r1gCKChRSb/+T/yIjsavAmbGLmFRKAzSjmWW/FMrcknmJ4hEOPA==}
|
||||
turbo@2.9.18:
|
||||
resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==}
|
||||
hasBin: true
|
||||
|
||||
type-check@0.4.0:
|
||||
@@ -3714,12 +3714,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
|
||||
|
||||
'@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))':
|
||||
'@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.3.0
|
||||
'@tailwindcss/oxide': 4.3.0
|
||||
tailwindcss: 4.3.0
|
||||
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
|
||||
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
@@ -3741,22 +3741,22 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
redent: 3.0.0
|
||||
|
||||
'@turbo/darwin-64@2.9.17':
|
||||
'@turbo/darwin-64@2.9.18':
|
||||
optional: true
|
||||
|
||||
'@turbo/darwin-arm64@2.9.17':
|
||||
'@turbo/darwin-arm64@2.9.18':
|
||||
optional: true
|
||||
|
||||
'@turbo/linux-64@2.9.17':
|
||||
'@turbo/linux-64@2.9.18':
|
||||
optional: true
|
||||
|
||||
'@turbo/linux-arm64@2.9.17':
|
||||
'@turbo/linux-arm64@2.9.18':
|
||||
optional: true
|
||||
|
||||
'@turbo/windows-64@2.9.17':
|
||||
'@turbo/windows-64@2.9.18':
|
||||
optional: true
|
||||
|
||||
'@turbo/windows-arm64@2.9.17':
|
||||
'@turbo/windows-arm64@2.9.18':
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
@@ -3781,7 +3781,7 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@24.13.1':
|
||||
'@types/node@24.13.2':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
@@ -3949,10 +3949,10 @@ snapshots:
|
||||
|
||||
'@uppercod/css-to-object@1.1.1': {}
|
||||
|
||||
'@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))':
|
||||
'@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
|
||||
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
|
||||
'@vitest/coverage-v8@4.1.8(vitest@4.1.8)':
|
||||
dependencies:
|
||||
@@ -3966,7 +3966,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.1.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
vitest: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
|
||||
'@vitest/expect@4.1.8':
|
||||
dependencies:
|
||||
@@ -3977,13 +3977,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))':
|
||||
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.8
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
|
||||
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.8':
|
||||
dependencies:
|
||||
@@ -5922,14 +5922,14 @@ snapshots:
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
turbo@2.9.17:
|
||||
turbo@2.9.18:
|
||||
optionalDependencies:
|
||||
'@turbo/darwin-64': 2.9.17
|
||||
'@turbo/darwin-arm64': 2.9.17
|
||||
'@turbo/linux-64': 2.9.17
|
||||
'@turbo/linux-arm64': 2.9.17
|
||||
'@turbo/windows-64': 2.9.17
|
||||
'@turbo/windows-arm64': 2.9.17
|
||||
'@turbo/darwin-64': 2.9.18
|
||||
'@turbo/darwin-arm64': 2.9.18
|
||||
'@turbo/linux-64': 2.9.18
|
||||
'@turbo/linux-arm64': 2.9.18
|
||||
'@turbo/windows-64': 2.9.18
|
||||
'@turbo/windows-arm64': 2.9.18
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
@@ -6044,7 +6044,7 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0):
|
||||
vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
@@ -6052,15 +6052,15 @@ snapshots:
|
||||
rolldown: 1.0.3
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 24.13.1
|
||||
'@types/node': 24.13.2
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.7.0
|
||||
yaml: 2.9.0
|
||||
|
||||
vitest@4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)):
|
||||
vitest@4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.8
|
||||
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.1.8
|
||||
'@vitest/runner': 4.1.8
|
||||
'@vitest/snapshot': 4.1.8
|
||||
@@ -6077,10 +6077,10 @@ snapshots:
|
||||
tinyexec: 1.2.3
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
|
||||
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.13.1
|
||||
'@types/node': 24.13.2
|
||||
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
|
||||
jsdom: 29.1.1
|
||||
transitivePeerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user