diff --git a/frontend/__tests__/components/Button.spec.tsx b/frontend/__tests__/components/Button.spec.tsx deleted file mode 100644 index 4c0ca6db2746..000000000000 --- a/frontend/__tests__/components/Button.spec.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { render, cleanup } from "@solidjs/testing-library"; -import { describe, it, expect, vi, afterEach } from "vitest"; - -import { Button } from "../../src/ts/components/common/Button"; - -describe("Button component", () => { - afterEach(() => { - cleanup(); - }); - - it("renders a button element when onClick is provided", () => { - const onClick = vi.fn(); - - const { container } = render(() => ( - - )); - - const child = container.querySelector('[data-testid="child"]'); - expect(child).toBeTruthy(); - expect(child?.textContent).toBe("Child"); - }); -}); diff --git a/frontend/__tests__/components/AnimatedModal.spec.tsx b/frontend/__tests__/components/common/AnimatedModal.spec.tsx similarity index 96% rename from frontend/__tests__/components/AnimatedModal.spec.tsx rename to frontend/__tests__/components/common/AnimatedModal.spec.tsx index 354cdf0f997c..b57934428ace 100644 --- a/frontend/__tests__/components/AnimatedModal.spec.tsx +++ b/frontend/__tests__/components/common/AnimatedModal.spec.tsx @@ -1,7 +1,7 @@ import { render } from "@solidjs/testing-library"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { AnimatedModal } from "../../src/ts/components/common/AnimatedModal"; +import { AnimatedModal } from "../../../src/ts/components/common/AnimatedModal"; describe("AnimatedModal", () => { beforeEach(() => { diff --git a/frontend/__tests__/components/common/Button.spec.tsx b/frontend/__tests__/components/common/Button.spec.tsx new file mode 100644 index 000000000000..0488c8b81d9a --- /dev/null +++ b/frontend/__tests__/components/common/Button.spec.tsx @@ -0,0 +1,297 @@ +import { cleanup, render } from "@solidjs/testing-library"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { Button } from "../../../src/ts/components/common/Button"; + +describe("Button component", () => { + afterEach(() => { + cleanup(); + }); + + it("renders a button element when onClick is provided", () => { + const onClick = vi.fn(); + + const { container } = render(() => ( + + )); + + const child = container.querySelector('[data-testid="child"]'); + expect(child).toBeTruthy(); + expect(child).toHaveTextContent("Child"); + }); + + it("applies custom class list when classList prop is provided", () => { + const { container } = render(() => ( + diff --git a/frontend/src/ts/components/common/DiscordAvatar.tsx b/frontend/src/ts/components/common/DiscordAvatar.tsx new file mode 100644 index 000000000000..aaf1a96b91c6 --- /dev/null +++ b/frontend/src/ts/components/common/DiscordAvatar.tsx @@ -0,0 +1,48 @@ +import { createSignal, JSXElement, Show } from "solid-js"; +import { createStore } from "solid-js/store"; + +import { FaSolidIcon } from "../../types/font-awesome"; + +import { Fa } from "./Fa"; + +//cache successful and missing avatars +const [avatar, setAvatar] = createStore>(); + +export function DiscordAvatar(props: { + discordId: string | undefined; + discordAvatar: string | undefined; + size?: number; + missingIcon?: FaSolidIcon; +}): JSXElement { + const cacheKey = (): string => `${props.discordId}/${props.discordAvatar}`; + const [showSpinner, setShowSpinner] = createSignal(true); + return ( +
+ } + > + <> + + + + { + setAvatar(cacheKey(), true); + setShowSpinner(false); + }} + onError={() => { + setAvatar(cacheKey(), false); + }} + /> + + +
+ ); +} diff --git a/frontend/src/ts/components/common/Fa.tsx b/frontend/src/ts/components/common/Fa.tsx index 60604d84f930..ae8d20b92543 100644 --- a/frontend/src/ts/components/common/Fa.tsx +++ b/frontend/src/ts/components/common/Fa.tsx @@ -1,18 +1,20 @@ import { JSXElement } from "solid-js"; import { FaObject } from "../../types/font-awesome"; +import { cn } from "../../utils/cn"; export type FaProps = { fixedWidth?: boolean; spin?: boolean; size?: number; + class?: string; } & FaObject; export function Fa(props: FaProps): JSXElement { const variant = (): string => props.variant ?? "solid"; return ( + + + + {props.text} + + ); +} + +export function H2(props: { text: string; fa?: FaProps }): JSXElement { + return ( +

+ + + + {props.text} +

+ ); +} + +export function H3(props: { text: string; fa: FaProps }): JSXElement { + return ( +

+ + {props.text} +

+ ); +} diff --git a/frontend/src/ts/components/common/LoadingCircle.tsx b/frontend/src/ts/components/common/LoadingCircle.tsx new file mode 100644 index 000000000000..180703e4a841 --- /dev/null +++ b/frontend/src/ts/components/common/LoadingCircle.tsx @@ -0,0 +1,10 @@ +import { JSXElement } from "solid-js"; + +import { Fa } from "./Fa"; +export function LoadingCircle(): JSXElement { + return ( +
+ +
+ ); +} diff --git a/frontend/src/ts/components/common/User.tsx b/frontend/src/ts/components/common/User.tsx new file mode 100644 index 000000000000..079ca696a473 --- /dev/null +++ b/frontend/src/ts/components/common/User.tsx @@ -0,0 +1,99 @@ +import { User as UserType } from "@monkeytype/schemas/users"; +import { For, JSXElement, Show } from "solid-js"; + +import { + badges, + UserBadge as UserBadgeType, +} from "../../controllers/badge-controller"; +import { + getMatchingFlags, + SupportsFlags, + UserFlag, + UserFlagOptions, +} from "../../controllers/user-flag-controller"; + +import { Button } from "./Button"; +import { DiscordAvatar } from "./DiscordAvatar"; +import { Fa } from "./Fa"; + +export function User( + props: { + user: SupportsFlags & + Pick & { + badgeId?: number; + }; + showAvatar?: boolean; + } & UserFlagOptions, +): JSXElement { + return ( +
+ + + +
+ ); +} + +function UserFlags(props: SupportsFlags & UserFlagOptions): JSXElement { + const flags = (): UserFlag[] => getMatchingFlags(props); + + return ( + + {(flag) => ( + }> +
+ {} +
+
+ )} +
+ ); +} + +function UserBadge(props: { id?: number }): JSXElement { + const badge = (): UserBadgeType | undefined => + props.id !== undefined ? badges[props.id] : undefined; + return ( + +
+ + + + +
+
+ ); +} diff --git a/frontend/src/ts/components/pages/AboutPage.tsx b/frontend/src/ts/components/pages/AboutPage.tsx index 20335d005d87..6b79cb26dc18 100644 --- a/frontend/src/ts/components/pages/AboutPage.tsx +++ b/frontend/src/ts/components/pages/AboutPage.tsx @@ -15,25 +15,9 @@ import { qsr } from "../../utils/dom"; import AsyncContent from "../common/AsyncContent"; import { Button } from "../common/Button"; import { ChartJs } from "../common/ChartJs"; -import { Fa, FaProps } from "../common/Fa"; - -function H2(props: { text: string; fa: FaProps }): JSXElement { - return ( -

- - {props.text} -

- ); -} - -function H3(props: { text: string; fa: FaProps }): JSXElement { - return ( -

- - {props.text} -

- ); -} +import { Fa } from "../common/Fa"; +import { H1, H3 } from "../common/Headers"; +import { User } from "../common/User"; qsr("nav .view-about").on("mouseenter", () => { prefetch(); @@ -64,6 +48,53 @@ export function AboutPage(): JSXElement { return (
+
+ {/*TODO remove after testing */} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
Created with love by Miodec.
@@ -173,7 +204,7 @@ export function AboutPage(): JSXElement {
-

+

Monkeytype is a minimalistic and customizable typing test. It features many test modes, an account system to save your typing speed history, @@ -281,7 +312,7 @@ export function AboutPage(): JSXElement {

-

+

Thanks to everyone who has supported this project. It would not be possible without you and your continued support. @@ -299,7 +330,7 @@ export function AboutPage(): JSXElement {

-

+

If you encounter a bug, have a feature request or just want to say hi - here are the different ways you can contact me directly. @@ -333,7 +364,7 @@ export function AboutPage(): JSXElement {

-

+