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();