From a438931648e1e4afb723a7075d21ae273a8e6897 Mon Sep 17 00:00:00 2001
From: Marco Pasqualetti
Date: Fri, 3 Jul 2026 18:23:43 +0200
Subject: [PATCH] refactor(frontend): consolidate Home card options into a
single state object
---
apps/frontend/src/models/Stage.ts | 12 +-
apps/frontend/src/pages/Home/Home.tsx | 205 ++++------------
.../src/pages/Home/buildCardUrl.test.ts | 28 +--
apps/frontend/src/pages/Home/buildCardUrl.ts | 72 ++----
apps/frontend/src/pages/Home/cardOptions.ts | 53 ++++
.../src/pages/Home/stages/Customize.tsx | 232 +++++++++---------
6 files changed, 267 insertions(+), 335 deletions(-)
create mode 100644 apps/frontend/src/pages/Home/cardOptions.ts
diff --git a/apps/frontend/src/models/Stage.ts b/apps/frontend/src/models/Stage.ts
index c1a102c1a78d3..f76755ec421c5 100644
--- a/apps/frontend/src/models/Stage.ts
+++ b/apps/frontend/src/models/Stage.ts
@@ -2,24 +2,34 @@ export const STAGE_LABELS = [
{
title: "Login",
shortTitle: "Login",
+ description: "",
},
{
title: "Select a Card",
shortTitle: "Select Card",
+ description: "You will be able to customize your card in future steps.",
},
{
title: "Modify Card Parameters",
shortTitle: "Modify Parameters",
+ description: "",
},
{
title: "Choose a Theme",
shortTitle: "Select Theme",
+ description: "",
},
{
title: "Display your Card",
shortTitle: "Display Card",
+ description:
+ "Display the finished card on GitHub, Twitter/X, LinkedIn, or anywhere else!",
},
-] as const satisfies Array<{ title: string; shortTitle: string }>;
+] as const satisfies Array<{
+ title: string;
+ shortTitle: string;
+ description: string;
+}>;
export type StageIndex = {
[K in keyof typeof STAGE_LABELS]: K extends `${infer N extends number}`
diff --git a/apps/frontend/src/pages/Home/Home.tsx b/apps/frontend/src/pages/Home/Home.tsx
index b475fa2b69c59..7c22adef91d09 100644
--- a/apps/frontend/src/pages/Home/Home.tsx
+++ b/apps/frontend/src/pages/Home/Home.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { JSX } from "react";
import { useDispatch } from "react-redux";
import { BounceLoader } from "react-spinners";
@@ -6,15 +6,8 @@ import { v4 as uuidv4 } from "uuid";
import { authenticate } from "../../api/user";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
-import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
-import {
- DEMO_GIST,
- DEMO_REPO,
- DEMO_USER,
- DEMO_WAKATIME_USER,
- HOST,
-} from "../../constants";
+import { DEMO_USER } from "../../constants";
import { CardType } from "../../models/CardType";
import { STAGE_LABELS } from "../../models/Stage";
import type { StageIndex } from "../../models/Stage";
@@ -27,6 +20,8 @@ import {
import { login } from "../../redux/slices/user";
import { buildCardUrl } from "./buildCardUrl";
+import { getDefaultCardOptions } from "./cardOptions";
+import type { CardOptions } from "./cardOptions";
import { CustomizeStage } from "./stages/Customize";
import { DisplayStage } from "./stages/Display";
import { LoginStage } from "./stages/Login/Login";
@@ -48,57 +43,44 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
const dispatch = useDispatch();
- // for stage two
- const [selectedUserId, setSelectedUserId] = useState(userId);
- const [repo, setRepo] = useState(DEMO_REPO);
- const [gist, setGist] = useState(DEMO_GIST);
- const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER);
-
const [selectedCard, setSelectedCard] = useState(CardType.STATS);
+ // for stages two and three
+ const [cardOptions, setCardOptions] = useState(() =>
+ getDefaultCardOptions(userId),
+ );
+
+ const setCardOption = useCallback<
+ (key: K, value: CardOptions[K]) => void
+ >((key, value) => {
+ setCardOptions((prev) => ({ ...prev, [key]: value }));
+ }, []);
+
// Reset the selected user to the resolved account id when it changes,
// adjusting state during render rather than in an effect:
// https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
const [prevUserId, setPrevUserId] = useState(userId);
if (userId !== prevUserId) {
setPrevUserId(userId);
- setSelectedUserId(userId);
+ setCardOptions((prev) => ({ ...prev, selectedUserId: userId }));
}
- // for stage three
- const [selectedStatsRank, setSelectedStatsRank] =
- useState(STATS_DEFAULT_RANK);
- const [selectedLanguagesLayout, setSelectedLanguagesLayout] = useState(
- LANGUAGES_DEFAULT_LAYOUT,
- );
- const [selectedWakatimeLayout, setSelectedWakatimeLayout] = useState(
- WAKATIME_DEFAULT_LAYOUT,
- );
-
- const [showTitle, setShowTitle] = useState(true);
- const [showOwner, setShowOwner] = useState(false);
- const [descriptionLines, setDescriptionLines] = useState<
- number | undefined
- >();
- const [customTitle, setCustomTitle] = useState("");
- const [langsCount, setLangsCount] = useState();
- const [hideValues, setHideValues] = useState(false);
- const [showAllStats, setShowAllStats] = useState(false);
- const [showIcons, setShowIcons] = useState(false);
- const [includeAllCommits, setIncludeAllCommits] = useState(true);
- const [enableAnimations, setEnableAnimations] = useState(true);
- const [usePercent, setUsePercent] = useState(false);
-
const { isDark } = useTheme();
const [theme, setTheme] = useState(isDark ? "dark" : "default");
const handleCardTypeChange = (cardType: CardType) => {
if (cardType === CardType.TOP_LANGS) {
- setLangsCount(4);
- setSelectedWakatimeLayout(WAKATIME_DEFAULT_LAYOUT);
+ setCardOptions((prev) => ({
+ ...prev,
+ langsCount: 4,
+ selectedWakatimeLayout: WAKATIME_DEFAULT_LAYOUT,
+ }));
} else if (cardType === CardType.WAKATIME) {
- setLangsCount(6);
- setSelectedLanguagesLayout(LANGUAGES_DEFAULT_LAYOUT);
+ setCardOptions((prev) => ({
+ ...prev,
+ langsCount: 6,
+ selectedLanguagesLayout: LANGUAGES_DEFAULT_LAYOUT,
+ }));
}
if (theme === "default" || theme === "default_repocard") {
@@ -119,51 +101,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
// string), but memoizing keeps it safe to pass as a prop if any of them are
// ever wrapped in React.memo.
const cardBuilder = useMemo(
- () =>
- buildCardUrl({
- userId,
- selectedCard,
- selectedUserId,
- repo,
- gist,
- wakatimeUser,
- selectedStatsRank,
- selectedLanguagesLayout,
- selectedWakatimeLayout,
- showTitle,
- showOwner,
- descriptionLines,
- customTitle,
- langsCount,
- hideValues,
- showAllStats,
- showIcons,
- includeAllCommits,
- enableAnimations,
- usePercent,
- }),
- [
- userId,
- selectedCard,
- selectedUserId,
- repo,
- gist,
- wakatimeUser,
- selectedStatsRank,
- selectedLanguagesLayout,
- selectedWakatimeLayout,
- showTitle,
- showOwner,
- descriptionLines,
- customTitle,
- langsCount,
- hideValues,
- showAllStats,
- showIcons,
- includeAllCommits,
- enableAnimations,
- usePercent,
- ],
+ () => buildCardUrl(userId, selectedCard, cardOptions),
+ [userId, selectedCard, cardOptions],
);
// Preview builder for the customize stage, dark-themed to match the surroundings.
@@ -187,10 +126,10 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
const cardDescriptor = useCardDescriptor({
selectedCard,
themeBuilder,
- repo,
+ repo: cardOptions.repo,
userId,
- wakatimeUser,
- gist,
+ wakatimeUser: cardOptions.wakatimeUser,
+ gist: cardOptions.gist,
});
const contentSectionRef = useRef(null);
@@ -208,31 +147,31 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
}, [stage]);
useEffect(() => {
- async function redirectCode() {
- // After requesting GitHub access, GitHub redirects back to your app with a code parameter
- const url = window.location.href;
+ // After requesting GitHub access, GitHub redirects back to the app with a
+ // code parameter appended to the redirect URI (which carries mode=private|public).
+ const url = new URL(window.location.href);
+ const code = url.searchParams.get("code");
+ if (code === null) {
+ return;
+ }
- // If GitHub API returns the code parameter
- if (url.includes("code=")) {
- const tempPrivateAccess = url.includes("private");
- const newUrl = url.split("code=", 2) as [string, string];
- const redirect = `${url.split(HOST)[0] as string}${HOST}/frontend`;
- window.history.pushState({}, "", redirect);
- setIsLoading(true);
- const userKey = uuidv4();
- const newUserId = await authenticate(
- newUrl[1],
- tempPrivateAccess,
- userKey,
- );
+ const tempPrivateAccess = url.searchParams.get("mode") === "private";
+ window.history.pushState({}, "", `${url.origin}/frontend`);
+ async function exchangeCode(code: string) {
+ setIsLoading(true);
+ try {
+ const userKey = uuidv4();
+ const newUserId = await authenticate(code, tempPrivateAccess, userKey);
dispatch(login({ userId: newUserId, userKey }));
-
+ } catch (error) {
+ console.error(error);
+ } finally {
setIsLoading(false);
}
}
- void redirectCode();
+ void exchangeCode(code);
}, [dispatch]);
if (isLoading) {
@@ -283,13 +222,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
) : (
- [
- "",
- "You will be able to customize your card in future steps.",
- "",
- "",
- "Display the finished card on GitHub, Twitter/X, LinkedIn, or anywhere else!",
- ][stage]
+ STAGE_LABELS[stage].description
)}
@@ -309,42 +242,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
{stage === 2 && (
diff --git a/apps/frontend/src/pages/Home/buildCardUrl.test.ts b/apps/frontend/src/pages/Home/buildCardUrl.test.ts
index 2f36b4ad0c979..350eecd12c848 100644
--- a/apps/frontend/src/pages/Home/buildCardUrl.test.ts
+++ b/apps/frontend/src/pages/Home/buildCardUrl.test.ts
@@ -6,11 +6,14 @@ import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home
import { CardType } from "../../models/CardType";
import { buildCardUrl } from "./buildCardUrl";
+import type { CardOptions } from "./cardOptions";
-const baseOptions = {
- selectedCard: CardType.STATS,
+const USER_ID = "john-github";
+
+// Built inline instead of via `getDefaultCardOptions`, which imports
+// `constants.ts` and needs a `window` global these node-based tests lack.
+const baseOptions: CardOptions = {
selectedUserId: "john",
- userId: "john-github",
repo: "repo1",
gist: "gist1",
wakatimeUser: "wakaUser",
@@ -32,13 +35,13 @@ const baseOptions = {
describe("buildCardUrl", () => {
it("builds stats suffix with defaults", () => {
- const result = buildCardUrl(baseOptions);
+ const result = buildCardUrl(USER_ID, CardType.STATS, baseOptions);
expect(result.toString()).toBe("?username=john");
});
it("adds stats options", () => {
- const result = buildCardUrl({
+ const result = buildCardUrl(USER_ID, CardType.STATS, {
...baseOptions,
showIcons: true,
includeAllCommits: true,
@@ -57,9 +60,8 @@ describe("buildCardUrl", () => {
});
it("builds top-langs suffix", () => {
- const result = buildCardUrl({
+ const result = buildCardUrl(USER_ID, CardType.TOP_LANGS, {
...baseOptions,
- selectedCard: CardType.TOP_LANGS,
langsCount: 5,
showTitle: false,
});
@@ -70,9 +72,8 @@ describe("buildCardUrl", () => {
});
it("builds pin suffix using userId not selectedUserId", () => {
- const result = buildCardUrl({
+ const result = buildCardUrl(USER_ID, CardType.PIN, {
...baseOptions,
- selectedCard: CardType.PIN,
showOwner: true,
descriptionLines: 3,
});
@@ -83,9 +84,8 @@ describe("buildCardUrl", () => {
});
it("builds gist suffix", () => {
- const result = buildCardUrl({
+ const result = buildCardUrl(USER_ID, CardType.GIST, {
...baseOptions,
- selectedCard: CardType.GIST,
showOwner: true,
});
@@ -93,9 +93,8 @@ describe("buildCardUrl", () => {
});
it("builds wakatime suffix with percent and custom title", () => {
- const result = buildCardUrl({
+ const result = buildCardUrl(USER_ID, CardType.WAKATIME, {
...baseOptions,
- selectedCard: CardType.WAKATIME,
wakatimeUser: "waka",
usePercent: true,
customTitle: "My Stats",
@@ -108,9 +107,8 @@ describe("buildCardUrl", () => {
});
it("adds non-default layouts", () => {
- const result = buildCardUrl({
+ const result = buildCardUrl(USER_ID, CardType.TOP_LANGS, {
...baseOptions,
- selectedCard: CardType.TOP_LANGS,
selectedLanguagesLayout: { id: 2, value: "compact", label: "Compact" },
});
diff --git a/apps/frontend/src/pages/Home/buildCardUrl.ts b/apps/frontend/src/pages/Home/buildCardUrl.ts
index b129930c71547..abab84e067010 100644
--- a/apps/frontend/src/pages/Home/buildCardUrl.ts
+++ b/apps/frontend/src/pages/Home/buildCardUrl.ts
@@ -1,4 +1,3 @@
-import type { SelectOption } from "../../components/Generic/Select";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
@@ -6,51 +5,34 @@ import { CardType } from "../../models/CardType";
import { cardUrl } from "../../models/CardUrl";
import type { CardUrlBuilder } from "../../models/CardUrl";
-interface Options {
- userId: string;
- selectedUserId: string;
- selectedCard: CardType;
- repo: string;
- gist: string;
- wakatimeUser: string;
- selectedStatsRank: SelectOption;
- selectedLanguagesLayout: SelectOption;
- selectedWakatimeLayout: SelectOption;
- showTitle: boolean;
- showOwner: boolean;
- descriptionLines: number | undefined;
- customTitle: string;
- langsCount: number | undefined;
- hideValues: boolean;
- showAllStats: boolean;
- showIcons: boolean;
- includeAllCommits: boolean;
- enableAnimations: boolean;
- usePercent: boolean;
-}
+import type { CardOptions } from "./cardOptions";
+
+export function buildCardUrl(
+ userId: string,
+ selectedCard: CardType,
+ options: CardOptions,
+): CardUrlBuilder {
+ const {
+ selectedUserId,
+ repo,
+ gist,
+ wakatimeUser,
+ selectedStatsRank,
+ selectedLanguagesLayout,
+ selectedWakatimeLayout,
+ showTitle,
+ showOwner,
+ descriptionLines,
+ customTitle,
+ langsCount,
+ hideValues,
+ showAllStats,
+ showIcons,
+ includeAllCommits,
+ enableAnimations,
+ usePercent,
+ } = options;
-export function buildCardUrl({
- userId,
- selectedCard,
- selectedUserId,
- repo,
- gist,
- wakatimeUser,
- selectedStatsRank,
- selectedLanguagesLayout,
- selectedWakatimeLayout,
- showTitle,
- showOwner,
- descriptionLines,
- customTitle,
- langsCount,
- hideValues,
- showAllStats,
- showIcons,
- includeAllCommits,
- enableAnimations,
- usePercent,
-}: Options): CardUrlBuilder {
switch (selectedCard) {
case CardType.STATS: {
let url = cardUrl(CardType.STATS);
diff --git a/apps/frontend/src/pages/Home/cardOptions.ts b/apps/frontend/src/pages/Home/cardOptions.ts
new file mode 100644
index 0000000000000..7e9a3650a60e1
--- /dev/null
+++ b/apps/frontend/src/pages/Home/cardOptions.ts
@@ -0,0 +1,53 @@
+import type { SelectOption } from "../../components/Generic/Select";
+import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
+import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
+import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
+import { DEMO_GIST, DEMO_REPO, DEMO_WAKATIME_USER } from "../../constants";
+
+/**
+ * All user-tunable card parameters collected during the customize stage.
+ * Held as a single state object in `HomeScreen` and updated one key at a time.
+ */
+export interface CardOptions {
+ selectedUserId: string;
+ repo: string;
+ gist: string;
+ wakatimeUser: string;
+ selectedStatsRank: SelectOption;
+ selectedLanguagesLayout: SelectOption;
+ selectedWakatimeLayout: SelectOption;
+ showTitle: boolean;
+ showOwner: boolean;
+ descriptionLines: number | undefined;
+ customTitle: string;
+ langsCount: number | undefined;
+ hideValues: boolean;
+ showAllStats: boolean;
+ showIcons: boolean;
+ includeAllCommits: boolean;
+ enableAnimations: boolean;
+ usePercent: boolean;
+}
+
+export function getDefaultCardOptions(userId: string): CardOptions {
+ return {
+ selectedUserId: userId,
+ repo: DEMO_REPO,
+ gist: DEMO_GIST,
+ wakatimeUser: DEMO_WAKATIME_USER,
+ selectedStatsRank: STATS_DEFAULT_RANK,
+ selectedLanguagesLayout: LANGUAGES_DEFAULT_LAYOUT,
+ selectedWakatimeLayout: WAKATIME_DEFAULT_LAYOUT,
+ showTitle: true,
+ showOwner: false,
+ descriptionLines: undefined,
+ customTitle: "",
+ langsCount: undefined,
+ hideValues: false,
+ showAllStats: false,
+ showIcons: false,
+ includeAllCommits: true,
+ enableAnimations: true,
+ usePercent: false,
+ };
+}
diff --git a/apps/frontend/src/pages/Home/stages/Customize.tsx b/apps/frontend/src/pages/Home/stages/Customize.tsx
index 414231c0dae2d..50c828ec55d04 100644
--- a/apps/frontend/src/pages/Home/stages/Customize.tsx
+++ b/apps/frontend/src/pages/Home/stages/Customize.tsx
@@ -1,7 +1,6 @@
-import type { JSX, default as React } from "react";
+import type { JSX } from "react";
import { CardImage } from "../../../components/Card/CardImage";
-import type { SelectOption } from "../../../components/Generic/Select";
import { CheckboxSection } from "../../../components/Home/CheckboxSection";
import { LanguagesLayoutSection } from "../../../components/Home/LanguagesLayoutSection";
import { NumericSection } from "../../../components/Home/NumericSection";
@@ -18,96 +17,66 @@ import { CardType } from "../../../models/CardType";
import type { CardUrlBuilder } from "../../../models/CardUrl";
import type { StageIndex } from "../../../models/Stage";
import { useIsAuthenticated } from "../../../redux/selectors/userSelectors";
+import type { CardOptions } from "../cardOptions";
-type Updater = React.Dispatch>;
+/**
+ * Extract the trailing path segments from pasted text, so pasting a full
+ * GitHub URL yields just the username (1 segment), owner/repo (2), or Gist id (1).
+ */
+function pastedPathTail(text: string, segments: number): string {
+ let value = text;
+ if (value.endsWith("/")) {
+ value = value.slice(0, -1);
+ }
+ const parts = value.split("/");
+ if (parts.length > segments) {
+ value = parts.slice(-segments).join("/");
+ }
+ return value;
+}
-/** @todo todo consider using React context API to avoid prop drilling */
interface CustomizeStageProps {
selectedCard: CardType;
- selectedStatsRank: SelectOption;
- setSelectedStatsRank: Updater;
- selectedLanguagesLayout: SelectOption;
- setSelectedLanguagesLayout: Updater;
- selectedWakatimeLayout: SelectOption;
- setSelectedWakatimeLayout: Updater;
- selectedUserId: string;
- setSelectedUserId: Updater;
- repo: string;
- setRepo: Updater;
- gist: string;
- setGist: Updater;
- wakatimeUser: string;
- setWakatimeUser: Updater;
- showTitle: boolean;
- setShowTitle: Updater;
- descriptionLines: number | undefined;
- setDescriptionLines: Updater;
- showOwner: boolean;
- setShowOwner: Updater;
- customTitle: string;
- setCustomTitle: Updater;
- langsCount: number | undefined;
- setLangsCount: Updater;
- hideValues: boolean;
- setHideValues: Updater;
- showIcons: boolean;
- setShowIcons: Updater;
- showAllStats: boolean;
- setShowAllStats: Updater;
- includeAllCommits: boolean;
- setIncludeAllCommits: Updater;
- enableAnimations: boolean;
- setEnableAnimations: Updater;
- usePercent: boolean;
- setUsePercent: Updater;
+ options: CardOptions;
+ onOptionChange: (
+ key: K,
+ value: CardOptions[K],
+ ) => void;
card: CardUrlBuilder;
setStage: (stageIndex: StageIndex) => void;
}
export function CustomizeStage({
selectedCard,
- selectedStatsRank,
- setSelectedStatsRank,
- selectedLanguagesLayout,
- setSelectedLanguagesLayout,
- selectedWakatimeLayout,
- setSelectedWakatimeLayout,
- selectedUserId,
- setSelectedUserId,
- repo,
- setRepo,
- gist,
- setGist,
- wakatimeUser,
- setWakatimeUser,
- showTitle,
- setShowTitle,
- showOwner,
- setShowOwner,
- descriptionLines,
- setDescriptionLines,
- customTitle,
- setCustomTitle,
- langsCount,
- setLangsCount,
- hideValues,
- setHideValues,
- showIcons,
- setShowIcons,
- showAllStats,
- setShowAllStats,
- includeAllCommits,
- setIncludeAllCommits,
- enableAnimations,
- setEnableAnimations,
- usePercent,
- setUsePercent,
+ options,
+ onOptionChange,
card,
setStage,
}: CustomizeStageProps): JSX.Element {
const cardType = selectedCard;
const isAuthenticated = useIsAuthenticated();
+ const {
+ selectedUserId,
+ repo,
+ gist,
+ wakatimeUser,
+ selectedStatsRank,
+ selectedLanguagesLayout,
+ selectedWakatimeLayout,
+ showTitle,
+ showOwner,
+ descriptionLines,
+ customTitle,
+ langsCount,
+ hideValues,
+ showAllStats,
+ showIcons,
+ includeAllCommits,
+ enableAnimations,
+ usePercent,
+ } = options;
+
return (
@@ -138,19 +107,16 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_USER}"`}
value={selectedUserId}
- onValueChange={setSelectedUserId}
+ onValueChange={(value) => {
+ onOptionChange("selectedUserId", value);
+ }}
onPaste={(e) => {
e.preventDefault();
- let newValue = e.clipboardData.getData("text");
// if the user pasted a full GitHub URL, extract username
- if (newValue.endsWith("/")) {
- newValue = newValue.slice(0, -1);
- }
- const parts = newValue.split("/");
- if (parts.length > 1) {
- newValue = parts.slice(-1).join("/");
- }
- setSelectedUserId(newValue);
+ onOptionChange(
+ "selectedUserId",
+ pastedPathTail(e.clipboardData.getData("text"), 1),
+ );
}}
disabled={!isAuthenticated}
/>
@@ -182,19 +148,16 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_REPO}"`}
value={repo}
- onValueChange={setRepo}
+ onValueChange={(value) => {
+ onOptionChange("repo", value);
+ }}
onPaste={(e) => {
e.preventDefault();
- let newValue = e.clipboardData.getData("text");
// if the user pasted a full GitHub URL, extract owner/repo
- if (newValue.endsWith("/")) {
- newValue = newValue.slice(0, -1);
- }
- const parts = newValue.split("/");
- if (parts.length > 2) {
- newValue = parts.slice(-2).join("/");
- }
- setRepo(newValue);
+ onOptionChange(
+ "repo",
+ pastedPathTail(e.clipboardData.getData("text"), 2),
+ );
}}
disabled={!isAuthenticated}
/>
@@ -226,19 +189,16 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_GIST}"`}
value={gist}
- onValueChange={setGist}
+ onValueChange={(value) => {
+ onOptionChange("gist", value);
+ }}
onPaste={(e) => {
e.preventDefault();
- let newValue = e.clipboardData.getData("text");
// if the user pasted a full GitHub URL, extract Gist ID
- if (newValue.endsWith("/")) {
- newValue = newValue.slice(0, -1);
- }
- const parts = newValue.split("/");
- if (parts.length > 1) {
- newValue = parts.slice(-1).join("/");
- }
- setGist(newValue);
+ onOptionChange(
+ "gist",
+ pastedPathTail(e.clipboardData.getData("text"), 1),
+ );
}}
disabled={!isAuthenticated}
/>
@@ -261,7 +221,9 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_WAKATIME_USER}"`}
value={wakatimeUser}
- onValueChange={setWakatimeUser}
+ onValueChange={(value) => {
+ onOptionChange("wakatimeUser", value);
+ }}
/>
)}
{cardType === CardType.STATS && (
@@ -270,13 +232,17 @@ export function CustomizeStage({
text="Show all available statistics."
question="Show all stats?"
checked={showAllStats}
- onCheckedChange={setShowAllStats}
+ onCheckedChange={(checked) => {
+ onOptionChange("showAllStats", checked);
+ }}
/>
)}
{cardType === CardType.STATS && (
{
+ onOptionChange("selectedStatsRank", option);
+ }}
/>
)}
{cardType === CardType.STATS && (
@@ -285,7 +251,9 @@ export function CustomizeStage({
text="Show icons next to all stats."
question="Show icons?"
checked={showIcons}
- onCheckedChange={setShowIcons}
+ onCheckedChange={(checked) => {
+ onOptionChange("showIcons", checked);
+ }}
/>
)}
{cardType === CardType.STATS && (
@@ -294,19 +262,25 @@ export function CustomizeStage({
text="Count total commits or just commits of the last 365 days."
question="Include all commits?"
checked={includeAllCommits}
- onCheckedChange={setIncludeAllCommits}
+ onCheckedChange={(checked) => {
+ onOptionChange("includeAllCommits", checked);
+ }}
/>
)}
{cardType === CardType.TOP_LANGS && (
{
+ onOptionChange("selectedLanguagesLayout", option);
+ }}
/>
)}
{cardType === CardType.WAKATIME && (
{
+ onOptionChange("selectedWakatimeLayout", option);
+ }}
/>
)}
{(cardType === CardType.TOP_LANGS ||
@@ -321,7 +295,9 @@ export function CustomizeStage({
>
}
value={langsCount}
- onValueChange={setLangsCount}
+ onValueChange={(value) => {
+ onOptionChange("langsCount", value);
+ }}
min={1}
max={20}
/>
@@ -332,7 +308,9 @@ export function CustomizeStage({
text="Hide language percentages or bytes while keeping the selected layout visible."
question="Hide values?"
checked={hideValues}
- onCheckedChange={setHideValues}
+ onCheckedChange={(checked) => {
+ onOptionChange("hideValues", checked);
+ }}
/>
)}
{cardType === CardType.WAKATIME && (
@@ -341,7 +319,9 @@ export function CustomizeStage({
text="Show time spent in hours or percentages."
question="Show percentages?"
checked={usePercent}
- onCheckedChange={setUsePercent}
+ onCheckedChange={(checked) => {
+ onOptionChange("usePercent", checked);
+ }}
/>
)}
{(cardType === CardType.STATS ||
@@ -352,7 +332,9 @@ export function CustomizeStage({
text="Shows a title at the top of the card."
question="Show title?"
checked={showTitle}
- onCheckedChange={setShowTitle}
+ onCheckedChange={(checked) => {
+ onOptionChange("showTitle", checked);
+ }}
/>
)}
{(cardType === CardType.STATS || cardType === CardType.WAKATIME) && (
@@ -367,7 +349,9 @@ export function CustomizeStage({
}
placeholder='e.g. "My GitHub Stats"'
value={customTitle}
- onValueChange={setCustomTitle}
+ onValueChange={(value) => {
+ onOptionChange("customTitle", value);
+ }}
/>
)}
{(cardType === CardType.STATS ||
@@ -378,7 +362,9 @@ export function CustomizeStage({
// text="Enable Animations."
question="enable animations?"
checked={enableAnimations}
- onCheckedChange={setEnableAnimations}
+ onCheckedChange={(checked) => {
+ onOptionChange("enableAnimations", checked);
+ }}
/>
)}
{(cardType === CardType.PIN || cardType === CardType.GIST) && (
@@ -387,7 +373,9 @@ export function CustomizeStage({
text="Shows the repo owner's name next to the repo name."
question="Show owner?"
checked={showOwner}
- onCheckedChange={setShowOwner}
+ onCheckedChange={(checked) => {
+ onOptionChange("showOwner", checked);
+ }}
/>
)}
{cardType === CardType.PIN && (
@@ -402,7 +390,9 @@ export function CustomizeStage({
>
}
value={descriptionLines}
- onValueChange={setDescriptionLines}
+ onValueChange={(value) => {
+ onOptionChange("descriptionLines", value);
+ }}
min={1}
max={3}
/>