Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-icons": "^5.7.0",
"react-loading-skeleton": "^3.5.0",
"react-redux": "^9.3.0",
"react-spinners": "^0.17.0",
"react-toastify": "^11.1.0",
Expand Down
17 changes: 12 additions & 5 deletions apps/frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,18 @@ Override background color in dark mode with GitHub's dark mode background color.
background-color: color-mix(in oklab, var(--color-error), #f00 40%);
}

/* Unscoped, this strips the focus ring from the site's search box. */
input:focus,
select:focus {
outline: 0;
border: var(--color-primary) solid 1px;
/* daisyUI's disabled border matches its own background and its text sits at 2.5:1. */
.input:is(:disabled, [disabled]) {
border-color: color-mix(
in oklab,
var(--color-base-content) 20%,
transparent
);

&:is(input),
& :is(input) {
color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/wizard/Home/stages/Customize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ export function CustomizeStage({
</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-[calc(var(--sl-nav-height)+var(--wizard-progress-height,6rem)+0.5rem)]">
<CardImage card={card} stage={2} />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/wizard/Home/stages/Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function DisplayStage({
</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 rounded p-4"
className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-[calc(var(--sl-nav-height)+var(--wizard-progress-height,6rem)+0.5rem)] rounded p-4"
style={{ background: getCardThemeBackdrop(theme, isDark) }}
>
<CardImage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
import { useEffect, useRef } from "react";
import type { JSX, RefObject } from "react";
import { createPortal } from "react-dom";
import { useId } from "react";
import type { JSX } from "react";

import { Button } from "../../../components/Generic/Button";

function useOutsideAlerter(
ref: RefObject<HTMLElement | null>,
action: () => void,
) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
action();
}
}

// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);

return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [action, ref]);
}

interface LoginAccountDeleteModalProps {
onClose: () => void;
onConfirm: () => void;
Expand All @@ -38,36 +13,44 @@ export function LoginAccountDeleteModal(
): JSX.Element {
const { onConfirm, onClose } = props;

const wrapperRef = useRef<HTMLDivElement | null>(null);
useOutsideAlerter(wrapperRef, onClose);

return createPortal(
// `wizard` because this mounts on `body`, outside the element the app's styles are scoped to.
<div className="wizard fixed left-0 top-0 w-full h-full bg-black/50 z-50">
<div className="w-full h-full flex justify-center items-center">
<div
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>
<hr />
<br />
<p>
Are you sure you want to delete your account from GitHub Stats
Extended?
</p>
<br />
<div className="flex flex-wrap">
<Button variant="primary" onClick={onClose}>
Cancel
</Button>
<Button variant="error" className="ml-auto" onClick={onConfirm}>
Delete Account
</Button>
</div>
const titleId = useId();

return (
<dialog
// React re-attaches an inline ref every render, and reopening a dialog throws.
ref={(node) => {
if (node !== null && !node.open) {
node.showModal();
}
}}
className="modal"
aria-labelledby={titleId}
onClose={onClose}
>
<div className="modal-box w-96">
<p id={titleId} className="mb-1 text-2xl">
Delete Account
</p>
<hr />
<br />
<p>
Are you sure you want to delete your account from GitHub Stats
Extended?
</p>
<br />
<div className="flex flex-wrap">
<Button variant="primary" onClick={onClose}>
Cancel
</Button>
<Button variant="error" className="ml-auto" onClick={onConfirm}>
Delete Account
</Button>
</div>
</div>
</div>,
document.body,
{/* daisyUI's backdrop: submitting it closes the dialog, so a click outside dismisses. */}
<form method="dialog" className="modal-backdrop">
<button>Close</button>
</form>
</dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function LoginAccountManagement(): JSX.Element {
};

return (
<LoginBox isOpaque={showDeleteModal}>
<LoginBox>
<div className="mb-4">
{privateAccess ? (
<div className="flex items-center gap-4">
Expand Down
7 changes: 2 additions & 5 deletions apps/frontend/src/wizard/Home/stages/Login/LoginBox.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { clsx } from "clsx";
import type { JSX, ReactNode } from "react";

import { LoginBoxDemoCards } from "./LoginBoxDemoCards";

interface LoginBoxProps {
children: ReactNode;

isOpaque?: boolean;
}

export function LoginBox(props: LoginBoxProps): JSX.Element {
const { children, isOpaque = false } = props;
const { children } = props;

return (
<div className="h-full flex flex-wrap">
<div className={clsx("md:flex", { "opacity-25": isOpaque })}>
<div className="md:flex">
<div className="lg:block lg:w-3/5 lg:p-8">
<div className="bg-base-300 rounded-sm w-full h-full m-auto p-8 shadow lg:h-auto">
{children}
Expand Down
12 changes: 7 additions & 5 deletions apps/frontend/src/wizard/components/Card/SvgInline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { loadConfigFromEnv } from "@stats-organization/github-readme-stats-core"
import axios from "axios";
import { useEffect, useRef, useState } from "react";
import type { JSX } from "react";
import Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";

import { setShouldMock } from "../../../axios-override.js";
import {
Expand Down Expand Up @@ -117,13 +115,17 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
if (forceLoading || !loaded) {
if (compact) {
return (
<Skeleton key="compactSkeleton" style={{ paddingBottom: "58%" }} />
<div
key="compactSkeleton"
className="skeleton w-full"
style={{ paddingBottom: "58%" }}
/>
);
}
// maximum dimensions of cards in SelectCard stage
return (
<div className=" w-[450px]">
<Skeleton key="skeleton" className="h-[245px]" />
<div className="w-[450px]">
<div key="skeleton" className="skeleton h-[245px] w-full" />
</div>
);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/wizard/components/Generic/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface SelectProps {
options: Array<SelectOption>;
selectedOption: SelectOption;

"aria-labelledby"?: string;

className?: string;
disabled?: boolean;

Expand All @@ -24,9 +26,11 @@ export function Select({
onOptionChange,
disabled,
className,
"aria-labelledby": ariaLabelledBy,
}: SelectProps): JSX.Element {
return (
<select
aria-labelledby={ariaLabelledBy}
className={clsx("text-base select select-sm w-40 mt-4", className)}
value={selectedOption.value}
onChange={(e) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useId } from "react";
import type { JSX } from "react";

import { Select } from "../Generic/Select";
Expand Down Expand Up @@ -55,10 +56,13 @@ export function LanguagesLayoutSection({
selectedLanguageLayoutOption,
onLanguageLayoutOptionChange,
}: LanguagesLayoutSectionProps): JSX.Element {
const titleId = useId();

return (
<Section title="Card Layout">
<Section title="Card Layout" titleId={titleId}>
<p>Select a card layout.</p>
<Select
aria-labelledby={titleId}
options={options}
selectedOption={selectedLanguageLayoutOption}
onOptionChange={onLanguageLayoutOptionChange}
Expand Down
11 changes: 9 additions & 2 deletions apps/frontend/src/wizard/components/Home/NumericSection.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useId } from "react";
import type { JSX, ReactNode } from "react";

import { useDebouncedField } from "../../hooks/useDebouncedField";
Expand Down Expand Up @@ -27,18 +28,20 @@ export function NumericSection({
disabled = false,
placeholder,
}: NumericSectionProps): JSX.Element {
const titleId = useId();
const { inputValue, setInputValue } = useDebouncedField({
value,
onValueChange,
type: "number",
});

return (
<Section title={title}>
<Section title={title} titleId={titleId}>
<p>{description}</p>
<input
aria-labelledby={titleId}
type="number"
className="border border-base-content/20 rounded px-2 py-1 mt-2 w-1/4 bg-base-100"
className="input validator mt-2 w-1/4"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
Expand All @@ -49,6 +52,10 @@ export function NumericSection({
disabled={disabled}
placeholder={placeholder}
/>
{/* Hidden until the field is `:user-invalid`, then shown in the error colour. */}
<p className="validator-hint">
Enter a number between {min} and {max}.
</p>
</Section>
);
}
32 changes: 31 additions & 1 deletion apps/frontend/src/wizard/components/Home/Progress.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { clsx } from "clsx";
import { useEffect, useRef } from "react";
import type { JSX, MouseEventHandler } from "react";
import {
FaArrowLeft as LeftArrowIcon,
Expand Down Expand Up @@ -60,9 +61,38 @@ export function ProgressBar({
const leftDisabled = currItemIndex === 0;
const rightDisabled = currItemIndex === items.length - 1;

const barRef = useRef<HTMLDivElement | null>(null);

/*
* The bar resizes because the step labels wrap, so a panel stacking under it cannot use a fixed offset.
* Publish the measured height instead; `Customize` and `Display` pin their card against it.
*/
useEffect(() => {
const bar = barRef.current;
if (bar === null) {
return undefined;
}

const observer = new ResizeObserver(() => {
document.documentElement.style.setProperty(
"--wizard-progress-height",
`${bar.getBoundingClientRect().height}px`,
);
});
observer.observe(bar);

return () => {
observer.disconnect();
document.documentElement.style.removeProperty("--wizard-progress-height");
};
}, []);

return (
// The site header is fixed, so the bar parks directly under it rather than at the viewport top.
<div className="w-full flex items-center sticky top-[var(--sl-nav-height)] bg-base-300 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
<div
ref={barRef}
className="w-full flex items-center sticky top-[var(--sl-nav-height)] bg-base-300 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md"
>
<button
type="button"
aria-label="Previous step"
Expand Down
13 changes: 11 additions & 2 deletions apps/frontend/src/wizard/components/Home/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { HiOutlineLightningBolt as LightningIcon } from "react-icons/hi";
interface SectionProps {
title: string;
children: ReactNode;
/** Set when a control names itself after the title with `aria-labelledby`. */
titleId?: string;
}

export function Section({ title, children }: SectionProps): JSX.Element {
export function Section({
title,
children,
titleId,
}: SectionProps): JSX.Element {
return (
<div className="flex relative pb-12">
<div className="h-full w-10 absolute inset-0 flex items-center justify-center">
Expand All @@ -17,7 +23,10 @@ export function Section({ title, children }: SectionProps): JSX.Element {
</div>

<div className="flex-grow pl-4">
<h2 className="font-medium title-font text-sm text-base-content mb-1 tracking-wider">
<h2
id={titleId}
className="font-medium title-font text-sm text-base-content mb-1 tracking-wider"
>
{title}
</h2>
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useId } from "react";
import type { JSX } from "react";

import { Select } from "../Generic/Select";
Expand Down Expand Up @@ -28,10 +29,13 @@ export function StatsRankSection({
selectedOption,
onOptionChange,
}: StatsRankSectionProps): JSX.Element {
const titleId = useId();

return (
<Section title="Progress Style">
<Section title="Progress Style" titleId={titleId}>
<p>Select a progress style.</p>
<Select
aria-labelledby={titleId}
options={options}
selectedOption={selectedOption}
onOptionChange={onOptionChange}
Expand Down
Loading