Skip to content

Commit

Permalink
Add avatar component (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lisa18289 authored Jan 16, 2024
1 parent 3838d46 commit ff4ea7c
Show file tree
Hide file tree
Showing 30 changed files with 428 additions and 12 deletions.
34 changes: 34 additions & 0 deletions packages/components/src/components/Avatar/Avatar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import "@/styles";
.root {
border-radius: var(--border-radius--round);
width: var(--avatar-size);
height: var(--avatar-size);
overflow: hidden;
}

.initials {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: calc(var(--avatar-size) * 0.4);
}

.image {
width: 100%;
height: 100%;
object-fit: cover;
}

.size-s {
--avatar-size: var(--avatar--size--s);
}

.size-m {
--avatar-size: var(--avatar--size--m);
}

.size-l {
--avatar-size: var(--avatar--size--l);
}
35 changes: 35 additions & 0 deletions packages/components/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { FC, PropsWithChildren } from "react";
import styles from "./Avatar.module.css";
import clsx from "clsx";
import { PropsContext, PropsContextProvider } from "@/lib/propsContext";

interface AvatarProps extends PropsWithChildren {
className?: string;
/** @default "s" */
size?: "s" | "m" | "l";
}

export const Avatar: FC<AvatarProps> = (props) => {
const { children, className, size = "s" } = props;

const rootClassName = clsx(className, styles.root, styles[`size-${size}`]);

const propsContext: PropsContext = {
Initials: {
className: styles.initials,
},
Image: {
className: styles.image,
},
};

return (
<div className={rootClassName}>
<PropsContextProvider props={propsContext}>
{children}
</PropsContextProvider>
</div>
);
};

export default Avatar;
3 changes: 3 additions & 0 deletions packages/components/src/components/Avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Avatar } from "./Avatar";
export { Avatar } from "./Avatar";
export default Avatar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from "@storybook/react";
import Avatar from "../Avatar";
import React from "react";
import { Initials } from "@/components/Initials";
import { Image } from "@/components/Image";
import { dummyText } from "@/lib/dev/dummyText";

const meta: Meta<typeof Avatar> = {
title: "Avatar",
component: Avatar,
render: (props) => (
<Avatar {...props}>
<Image alt="Gopher" src={dummyText.imageSrc} />
</Avatar>
),
parameters: {
controls: { exclude: ["className"] },
},
argTypes: {
size: {
control: "inline-radio",
},
},
};
export default meta;

type Story = StoryObj<typeof Avatar>;

export const Default: Story = {};

export const WithInitials: Story = {
render: (props) => (
<Avatar {...props}>
<Initials>Max Mustermann</Initials>
</Avatar>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react";
import Avatar from "../Avatar";
import defaultMeta from "./Default.stories";

const meta: Meta<typeof Avatar> = {
...defaultMeta,
title: "Avatar/Variants",
};
export default meta;

type Story = StoryObj<typeof Avatar>;

export const SizeS: Story = {};

export const SizeM: Story = { args: { size: "m" } };

export const SizeL: Story = { args: { size: "l" } };
5 changes: 2 additions & 3 deletions packages/components/src/components/Content/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, {
ComponentProps,
createElement,
FC,
HTMLAttributes,
PropsWithChildren,
} from "react";
import styles from "./Content.module.css";
import clsx from "clsx";
import { ClearPropsContext, useProps } from "@/lib/propsContext";

export interface ContentProps
extends PropsWithChildren<HTMLAttributes<HTMLDivElement>> {
export interface ContentProps extends PropsWithChildren<ComponentProps<"div">> {
elementType?: string;
}

Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/components/Heading/Heading.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createElement, FC, HTMLAttributes, PropsWithChildren } from "react";
import { ComponentProps, createElement, FC, PropsWithChildren } from "react";
import styles from "./Heading.module.css";
import clsx from "clsx";
import { useProps } from "@/lib/propsContext";

export interface HeadingProps
extends PropsWithChildren<HTMLAttributes<HTMLHeadingElement>> {
extends PropsWithChildren<
ComponentProps<"h1" | "h2" | "h3" | "h4" | "h5" | "h6">
> {
level?: 1 | 2 | 3 | 4 | 5 | 6;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {
ComponentProps,
FC,
HTMLAttributes,
PropsWithChildren,
SVGAttributes,
useMemo,
Expand Down Expand Up @@ -46,7 +46,7 @@ export const Icon: FC<IconProps> = (props) => {
* Icon is wrapped inside span, so it always behaves as an inline element
* (line-height is applied), even if used in flex/grid layouts.
*/
const spanProps: HTMLAttributes<HTMLSpanElement> = {
const spanProps: ComponentProps<"span"> = {
className: clsx(classNameFromProps, styles.root),
};

Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/components/Image/Image.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@import "@/styles";

.root {
}
16 changes: 16 additions & 0 deletions packages/components/src/components/Image/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { ComponentProps, FC } from "react";
import styles from "./Image.module.css";
import clsx from "clsx";
import { useProps } from "@/lib/propsContext";

export interface ImageProps extends ComponentProps<"img"> {}

export const Image: FC<ImageProps> = (props) => {
const { className, ...rest } = useProps("Image", props);

const rootClassName = clsx(className, styles.root);

return <img className={rootClassName} {...rest} />;
};

export default Image;
3 changes: 3 additions & 0 deletions packages/components/src/components/Image/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Image } from "./Image";
export { type ImageProps, Image } from "./Image";
export default Image;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Meta, StoryObj } from "@storybook/react";
import Image from "../Image";
import React from "react";
import { dummyText } from "@/lib/dev/dummyText";

const meta: Meta<typeof Image> = {
title: "Image",
component: Image,
render: (props) => <Image {...props} alt="Gopher" src={dummyText.imageSrc} />,
};
export default meta;

type Story = StoryObj<typeof Image>;

export const Default: Story = {};
43 changes: 43 additions & 0 deletions packages/components/src/components/Initials/Initials.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@import "@/styles";

.root {
display: flex;
font-weight: bold;
background-color: var(--background-color);
}

.root > *:nth-child(odd) {
color: var(--char-1-color);
}

.root > *:nth-child(even) {
color: var(--char-2-color);
}

.root > *:only-child {
color: var(--char-2-color);
}

.variant-1 {
--background-color: var(--initials--variant-1-background-color);
--char-1-color: var(--initials--variant-1-first-initial-color);
--char-2-color: var(--initials--variant-1-second-initial-color);
}

.variant-2 {
--background-color: var(--initials--variant-2-background-color);
--char-1-color: var(--initials--variant-2-first-initial-color);
--char-2-color: var(--initials--variant-2-second-initial-color);
}

.variant-3 {
--background-color: var(--initials--variant-3-background-color);
--char-1-color: var(--initials--variant-3-first-initial-color);
--char-2-color: var(--initials--variant-3-second-initial-color);
}

.variant-4 {
--background-color: var(--initials--variant-4-background-color);
--char-1-color: var(--initials--variant-4-first-initial-color);
--char-2-color: var(--initials--variant-4-second-initial-color);
}
34 changes: 34 additions & 0 deletions packages/components/src/components/Initials/Initials.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { FC, PropsWithChildren } from "react";
import { getVariantFromInitials } from "./lib/getVariantFromInitials";
import { getInitialsFromString } from "./lib/getInitialsFromString";
import styles from "./Initials.module.css";
import clsx from "clsx";
import { useProps } from "@/lib/propsContext";

export interface InitialsProps extends PropsWithChildren<{ children: string }> {
className?: string;
}

export const Initials: FC<InitialsProps> = (props) => {
const { children, className } = useProps("Initials", props);

const initials = getInitialsFromString(children);

const rootClassName = clsx(
className,
styles.root,
styles[`variant-${getVariantFromInitials(initials)}`],
);

const initialsElements = initials.map((initial, index) => (
<span key={index}>{initial}</span>
));

return (
<div aria-label={children} className={rootClassName}>
{initialsElements}
</div>
);
};

export default Initials;
3 changes: 3 additions & 0 deletions packages/components/src/components/Initials/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Initials } from "./Initials";
export { type InitialsProps, Initials } from "./Initials";
export default Initials;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getInitialsFromString } from "./getInitialsFromString";

describe('"getInitialsFromString()', () => {
test("does return empty array if string is empty", () => {
expect(getInitialsFromString("")).toStrictEqual([]);
});

test.each([
["Max Mustermann", "MM"],
["Max & Mustermann", "MM"],
["Max (Mustermann)", "MM"],
["Max", "M"],
])("builds correct initials for %o", (item, expectedResult) => {
expect(getInitialsFromString(item).join("")).toBe(expectedResult);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const getInitialsFromString = (initials: string): string[] => {
return initials
.replace(/[^\p{L}\s]/giu, "")
.split(" ")
.map((part) => part.trim()[0])
.filter((p) => p !== undefined)
.map((char) => char.toUpperCase())
.slice(0, 2);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getVariantFromInitials } from "./getVariantFromInitials";

describe('"getVariantFromInitials()', () => {
test("does return 1 if array is empty", () => {
expect(getVariantFromInitials([])).toStrictEqual(1);
});

test.each([
[["A"], 2],
[["B"], 3],
[["C", "D"], 4],
[["Z"], 3],
[["Ä"], 1],
[["1"], 2],
])("does get correct variant for given initial", (item, expectedVariant) => {
expect(getVariantFromInitials(item)).toBe(expectedVariant);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const getVariantFromInitials = (initials: string[]): number => {
if (initials.length < 1) {
return 1;
}
return (initials[0].charCodeAt(0) % 4) + 1;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Meta, StoryObj } from "@storybook/react";
import Initials from "../Initials";
import React from "react";

const meta: Meta<typeof Initials> = {
title: "Initials",
component: Initials,
render: (props) => <Initials {...props}>Max Mustermann</Initials>,
parameters: {
controls: { exclude: ["className"] },
},
};
export default meta;

type Story = StoryObj<typeof Initials>;

export const Default: Story = {};

export const OneLetter: Story = {
render: (props) => <Initials {...props}>Max </Initials>,
};
Loading

1 comment on commit ff4ea7c

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/components/

St.
Category Percentage Covered / Total
🟢 Statements 98.63% 144/146
🟢 Branches 93.75% 30/32
🟢 Functions 90.63% 29/32
🟢 Lines 98.47% 129/131

Test suite run success

49 tests passing in 9 suites.

Report generated by 🧪jest coverage report action from ff4ea7c

Please sign in to comment.