Include contributions from private repositories for more complete and
accurate stats.
@@ -45,12 +51,13 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
-
+
Explore options using sample data. Insert your own username in the
last step.
diff --git a/apps/frontend/src/pages/Home/stages/SelectCard.tsx b/apps/frontend/src/pages/Home/stages/SelectCard.tsx
index ed59b09e4477d..bc2afb3723257 100644
--- a/apps/frontend/src/pages/Home/stages/SelectCard.tsx
+++ b/apps/frontend/src/pages/Home/stages/SelectCard.tsx
@@ -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 (
diff --git a/apps/frontend/src/pages/Home/stages/Theme.tsx b/apps/frontend/src/pages/Home/stages/Theme.tsx
index 23797cf3f1cfb..fc5c1668b5bfb 100644
--- a/apps/frontend/src/pages/Home/stages/Theme.tsx
+++ b/apps/frontend/src/pages/Home/stages/Theme.tsx
@@ -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 (
<>
- {themeList.map((myTheme) => (
-
- ))}
+ {themeList.map((myTheme) => {
+ const themeColors = themes[myTheme as keyof typeof themes];
+ return (
+
+ );
+ })}
- For more theme options check the{" "}
+ {"For more theme options check the "}
customization documentation
- {" "}
- after you copied your card URL in step 5.
+
+ {" after you copied your card URL in step 5."}
>
);
diff --git a/apps/frontend/src/redux/selectors/themeSelectors.ts b/apps/frontend/src/redux/selectors/themeSelectors.ts
new file mode 100644
index 0000000000000..ec18dd59e36e1
--- /dev/null
+++ b/apps/frontend/src/redux/selectors/themeSelectors.ts
@@ -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 };
+}
diff --git a/apps/frontend/src/redux/slices/theme.ts b/apps/frontend/src/redux/slices/theme.ts
new file mode 100644
index 0000000000000..4f78d7a93ee1e
--- /dev/null
+++ b/apps/frontend/src/redux/slices/theme.ts
@@ -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
= [
+ { 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) => {
+ 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;
diff --git a/apps/frontend/src/redux/store.ts b/apps/frontend/src/redux/store.ts
index 73870b8e2c9d3..97fc03a06cd7b 100644
--- a/apps/frontend/src/redux/store.ts
+++ b/apps/frontend/src/redux/store.ts
@@ -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();
diff --git a/eslint.config.js b/eslint.config.js
index 270d6cbb80d67..c1b759dbeb113 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -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",
diff --git a/packages/core/src/api/gist.js b/packages/core/src/api/gist.js
index d03071248dcde..0cb9610262ca8 100644
--- a/packages/core/src/api/gist.js
+++ b/packages/core/src/api/gist.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import { renderGistCard } from "../cards/gist.js";
import {
MissingParamError,
diff --git a/packages/core/src/api/index.js b/packages/core/src/api/index.js
index 5709e30a08930..4751560082eab 100644
--- a/packages/core/src/api/index.js
+++ b/packages/core/src/api/index.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import { renderStatsCard } from "../cards/stats.js";
import {
MissingParamError,
diff --git a/packages/core/src/api/pin.js b/packages/core/src/api/pin.js
index 7d6ab9504221e..118aa70cc2204 100644
--- a/packages/core/src/api/pin.js
+++ b/packages/core/src/api/pin.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import { renderRepoCard } from "../cards/repo.js";
import {
MissingParamError,
diff --git a/packages/core/src/api/top-langs.js b/packages/core/src/api/top-langs.js
index 9d5c9670e0164..46f6633c013e6 100644
--- a/packages/core/src/api/top-langs.js
+++ b/packages/core/src/api/top-langs.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import { renderTopLanguages } from "../cards/top-languages.js";
import {
MissingParamError,
diff --git a/packages/core/src/api/wakatime.js b/packages/core/src/api/wakatime.js
index eaf02daf5c38a..19307f104aaf8 100644
--- a/packages/core/src/api/wakatime.js
+++ b/packages/core/src/api/wakatime.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import { renderWakatimeCard } from "../cards/wakatime.js";
import {
MissingParamError,
diff --git a/packages/core/src/cards/gist.js b/packages/core/src/cards/gist.js
index 81d5c2446f455..fd16f4ad0783c 100644
--- a/packages/core/src/cards/gist.js
+++ b/packages/core/src/cards/gist.js
@@ -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";
diff --git a/packages/core/src/cards/repo.js b/packages/core/src/cards/repo.js
index 38b7bd6192b0a..a352f357c1ea5 100644
--- a/packages/core/src/cards/repo.js
+++ b/packages/core/src/cards/repo.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";
diff --git a/packages/core/src/cards/stats.js b/packages/core/src/cards/stats.js
index 940271a381dc1..cbaf2b61f3e43 100644
--- a/packages/core/src/cards/stats.js
+++ b/packages/core/src/cards/stats.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";
diff --git a/packages/core/src/cards/top-languages.js b/packages/core/src/cards/top-languages.js
index 8c2d2f1841e09..ad28510074008 100644
--- a/packages/core/src/cards/top-languages.js
+++ b/packages/core/src/cards/top-languages.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";
diff --git a/packages/core/src/cards/wakatime.js b/packages/core/src/cards/wakatime.js
index f7eeeee395cc2..a717e753587fc 100644
--- a/packages/core/src/cards/wakatime.js
+++ b/packages/core/src/cards/wakatime.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";
diff --git a/packages/core/src/common/Card.js b/packages/core/src/common/Card.ts
similarity index 59%
rename from packages/core/src/common/Card.js
rename to packages/core/src/common/Card.ts
index 6345b662c639b..7ae629f83c8cf 100644
--- a/packages/core/src/common/Card.js
+++ b/packages/core/src/common/Card.ts
@@ -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;
+ /** 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 = `
- ${this.titlePrefixIcon}
+ ${String(this.titlePrefixIcon)}
`;
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 {
- ${gradients.map((grad, index) => {
- let offset = (index * 100) / (gradients.length - 1);
- return ``;
- })}
+ ${gradients
+ .map((grad, index) => {
+ const offset = (index * 100) / (gradients.length - 1);
+ return ``;
+ })
+ .join(",")}
`
@@ -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 `