Skip to content
27 changes: 27 additions & 0 deletions apps/frontend/src/models/CardType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,30 @@ export const CardType = {
WAKATIME: "wakatime",
} as const;
export type CardType = (typeof CardType)[keyof typeof CardType];

export const CardCategory = {
/**
* Cards describing a single repo or gist.
* They accept `show_owner` and use the `_repocard` theme variants.
*/
REPO: "repo",
/**
* Cards describing a user: stats, top languages and WakaTime.
*/
USER: "user",
} as const;
export type CardCategory = (typeof CardCategory)[keyof typeof CardCategory];

/**
* Category each card belongs to.
*
* Exhaustive by construction: a new {@link CardType} will not compile until it
* is categorised here, rather than silently falling into one of the branches.
*/
export const CATEGORY_BY_CARD_TYPE: Record<CardType, CardCategory> = {
[CardType.STATS]: CardCategory.USER,
[CardType.TOP_LANGS]: CardCategory.USER,
[CardType.WAKATIME]: CardCategory.USER,
[CardType.PIN]: CardCategory.REPO,
[CardType.GIST]: CardCategory.REPO,
};
41 changes: 24 additions & 17 deletions apps/frontend/src/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ThemeName } from "@stats-organization/github-readme-stats-core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { JSX } from "react";
import { useDispatch } from "react-redux";
Expand All @@ -8,7 +9,11 @@ import { authenticate } from "../../api/user";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
import { DEMO_USER } from "../../constants";
import { CardType } from "../../models/CardType";
import {
CATEGORY_BY_CARD_TYPE,
CardCategory,
CardType,
} from "../../models/CardType";
import { STAGE_LABELS } from "../../models/Stage";
import type { StageIndex } from "../../models/Stage";
import { useTheme } from "../../redux/selectors/themeSelectors";
Expand All @@ -34,6 +39,13 @@ interface HomeScreenProps {
setStage: (stageIndex: StageIndex) => void;
}

function getDefaultTheme(isDark: boolean, category: CardCategory): ThemeName {
if (category === CardCategory.REPO) {
return isDark ? "dark_github_repocard" : "light_github_repocard";
}
return isDark ? "dark_github" : "light_github";
}

export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
const [isLoading, setIsLoading] = useState(false);

Expand Down Expand Up @@ -66,7 +78,10 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
}

const { isDark } = useTheme();
const [theme, setTheme] = useState(isDark ? "dark" : "default");
const cardCategory = CATEGORY_BY_CARD_TYPE[selectedCard];
const [theme, setTheme] = useState(() =>
getDefaultTheme(isDark, cardCategory),
);

const handleCardTypeChange = (cardType: CardType) => {
if (cardType === CardType.TOP_LANGS) {
Expand All @@ -83,12 +98,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
}));
}

if (theme === "default" || theme === "default_repocard") {
if (cardType === CardType.PIN || cardType === CardType.GIST) {
setTheme("default_repocard");
} else {
setTheme("default");
}
if (theme === getDefaultTheme(isDark, cardCategory)) {
setTheme(getDefaultTheme(isDark, CATEGORY_BY_CARD_TYPE[cardType]));
}

setSelectedCard(cardType);
Expand All @@ -107,19 +118,14 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {

// Preview builder for the customize stage, dark-themed to match the surroundings.
const customizeCardBuilder = useMemo(
() => (isDark ? cardBuilder.theme("github_dark") : cardBuilder),
[cardBuilder, isDark],
() => cardBuilder.theme(getDefaultTheme(isDark, cardCategory)),
[cardBuilder, isDark, cardCategory],
);

// for stage four
const isRepoCard =
selectedCard === CardType.PIN || selectedCard === CardType.GIST;
const defaultTheme = isRepoCard ? "default_repocard" : "default";
const isDefaultTheme = theme === defaultTheme;

const themeBuilder = useMemo(
() => (isDefaultTheme ? cardBuilder : cardBuilder.theme(theme)),
[cardBuilder, isDefaultTheme, theme],
() => cardBuilder.theme(theme),
[cardBuilder, theme],
);

// for stage five
Expand Down Expand Up @@ -252,6 +258,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
<ThemeStage
card={cardBuilder}
theme={theme}
category={cardCategory}
onThemeChange={(theme) => {
setTheme(theme);
setStage(4);
Expand Down
8 changes: 6 additions & 2 deletions apps/frontend/src/pages/Home/stages/Customize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
DEMO_USER,
DEMO_WAKATIME_USER,
} from "../../../constants";
import { CardType } from "../../../models/CardType";
import {
CATEGORY_BY_CARD_TYPE,
CardCategory,
CardType,
} from "../../../models/CardType";
import type { CardUrlBuilder } from "../../../models/CardUrl";
import type { StageIndex } from "../../../models/Stage";
import { useIsAuthenticated } from "../../../redux/selectors/userSelectors";
Expand Down Expand Up @@ -367,7 +371,7 @@ export function CustomizeStage({
}}
/>
)}
{(cardType === CardType.PIN || cardType === CardType.GIST) && (
{CATEGORY_BY_CARD_TYPE[cardType] === CardCategory.REPO && (
<CheckboxSection
title="Show Owner?"
text="Shows the repo owner's name next to the repo name."
Expand Down
40 changes: 34 additions & 6 deletions apps/frontend/src/pages/Home/stages/Theme.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ThemeName } from "@stats-organization/github-readme-stats-core";
import { themes } from "@stats-organization/github-readme-stats-core";
import type { JSX } from "react";

Expand All @@ -6,10 +7,14 @@ import {
getCardThemeBackdrop,
getThemeSortRank,
} from "../../../components/Card/themeBackdrop";
import { CardCategory } from "../../../models/CardType";
import type { CardUrlBuilder } from "../../../models/CardUrl";
import { useTheme } from "../../../redux/selectors/themeSelectors";

const excludedThemes = [
const excludedThemes: Array<ThemeName> = [
"default",
"default_repocard",
"github_dark",
"merko",
"blue-green",
"gotham",
Expand All @@ -18,29 +23,52 @@ const excludedThemes = [
"holi",
];

const allThemeNames = Object.keys(themes) as Array<ThemeName>;

// 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));
const forPicker = (names: ReadonlyArray<ThemeName>) =>
names
.filter((name) => !excludedThemes.includes(name))
.sort((a, b) => getThemeSortRank(a) - getThemeSortRank(b));

// Some themes come in pairs: `X_repocard` for the repo and gist cards, `X` for
// the stats, top languages and WakaTime cards. Each side offers only its own
// variant, so the theme Home.tsx starts on is always one of the entries here.
//
// Keep in sync with `generateTable` in `packages/core/scripts/generate-theme-readme.js`,
// which splits the theme README tables by the same rule.
const repoCardThemes = forPicker(
allThemeNames.filter(
(name) => !allThemeNames.includes(`${name}_repocard` as ThemeName),
),
);

const nonRepoCardThemes = forPicker(
allThemeNames.filter((name) => !name.endsWith("_repocard")),
);

interface ThemeStageProps {
card: CardUrlBuilder;
theme: string;
onThemeChange: (theme: string) => void;
category: CardCategory;
onThemeChange: (theme: ThemeName) => void;
}

export function ThemeStage({
theme,
card,
category,
onThemeChange,
}: ThemeStageProps): JSX.Element {
const { isDark } = useTheme();
const themeList =
category === CardCategory.REPO ? repoCardThemes : nonRepoCardThemes;

return (
<>
<div className="flex flex-wrap">
{themeList.map((myTheme) => {
const themeColors = themes[myTheme as keyof typeof themes];
const themeColors = themes[myTheme];
return (
<button
className="p-2 lg:p-4"
Expand Down
20 changes: 17 additions & 3 deletions packages/core/scripts/generate-theme-readme.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,25 @@ const createTableItem = ({ link, label, isRepoCard }) => {
return `\`${label}\` ![${link}][${link}${isRepoCard ? "_repo" : ""}]`;
};

const allThemeNames = Object.keys(themes);

// Some themes come in pairs: `X_repocard` for the repo and gist cards, `X` for
// the stats, top languages and WakaTime cards. Each table lists only its own
// variant.
//
// Keep in sync with the theme lists in `apps/frontend/src/pages/Home/stages/Theme.tsx`,
// which splits the picker by the same rule.
const repoCardThemes = allThemeNames.filter(
(name) => !allThemeNames.includes(`${name}_repocard`),
);

const nonRepoCardThemes = allThemeNames.filter(
(name) => !name.endsWith("_repocard"),
);

const generateTable = ({ isRepoCard }) => {
const rows = [];
const themesFiltered = Object.keys(themes).filter(
(name) => name !== (isRepoCard ? "default" : "default_repocard"),
);
const themesFiltered = isRepoCard ? repoCardThemes : nonRepoCardThemes;

for (let i = 0; i < themesFiltered.length; i += 3) {
const one = themesFiltered[i];
Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/cards/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { themes } from "../themes/index.js";

type ThemeNames = keyof typeof themes;
import type { ThemeName } from "../themes/index.js";

export interface CommonOptions {
title_color: string;
icon_color: string;
text_color: string;
bg_color: string;
theme: ThemeNames;
theme: ThemeName;
border_radius: number;
border_color: string;
locale: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/common/color.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { themes } from "../themes/index.js";
import type { ThemeName } from "../themes/index.js";

/** Matches a 3-, 4-, 6-, or 8-digit hex color with no leading `#`. */
const HEX_COLOR =
Expand Down Expand Up @@ -174,7 +175,7 @@ const getCardColors = ({
const isThemeProvided = theme !== undefined && theme in themes;

const selectedTheme = isThemeProvided
? themes[theme as keyof typeof themes]
? themes[theme as ThemeName]
: defaultTheme;

const defaultBorderColor =
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { default as wakatime } from "./api/wakatime.js";
export { getConfig, loadConfigFromEnv } from "./common/config.js";

export { themes } from "./themes/index.js";
export type { ThemeName } from "./themes/index.js";
33 changes: 33 additions & 0 deletions packages/core/src/themes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,34 @@ export const themes = {
text_color: "434d58",
bg_color: "fffefe",
},
light_github: {
title_color: "0969da",
icon_color: "0969da",
text_color: "59636e",
bg_color: "ffffff",
border_color: "d1d9e0",
},
dark_github: {
title_color: "4493f8",
icon_color: "4493f8",
text_color: "9198a1",
bg_color: "0d1117",
border_color: "3d444d",
},
light_github_repocard: {
title_color: "0969da",
icon_color: "59636e",
text_color: "59636e",
bg_color: "ffffff",
border_color: "d1d9e0",
},
dark_github_repocard: {
title_color: "4493f8",
icon_color: "9198a1",
text_color: "9198a1",
bg_color: "0d1117",
border_color: "3d444d",
},
transparent: {
title_color: "006AFF",
icon_color: "0579C3",
Expand Down Expand Up @@ -474,3 +502,8 @@ export const themes = {
bg_color: "35,4158d0,c850c0,ffcc70",
},
} as const satisfies Record<string, Theme>;

/**
* Name of one of the {@link themes}.
*/
export type ThemeName = keyof typeof themes;