Skip to content

Commit 494b7ac

Browse files
authored
feat(app): add question item (#385)
* feat(app): add question item * feat(app): add intl utils * refactor(app): QuestionItem refactor * refactor(app): change creationDate type to Date * refactor(app): change prop name * refactor(app): move date formatting into utility function * refactor(app): change heading level * feat(app): add QuestionItem story
1 parent 850efad commit 494b7ac

File tree

6 files changed

+170
-1
lines changed

6 files changed

+170
-1
lines changed

apps/app/src/app/foo/page.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
import { QuestionItem } from "../../components/QuestionItem/QuestionItem";
2+
13
export default function FooPage() {
2-
return <div>siema</div>;
4+
return (
5+
<div className="flex flex-col gap-y-10 p-10">
6+
<QuestionItem
7+
title="Co się stanie gdy EventEmitter wyemituje event 'error', a nic na niego nie
8+
nasłuchuje?"
9+
level="junior"
10+
creationDate={new Date("2023-01-01")}
11+
votes={1}
12+
voted={false}
13+
/>
14+
<QuestionItem
15+
title="Co się stanie gdy EventEmitter wyemituje event 'error', a nic na niego nie
16+
nasłuchuje?"
17+
level="mid"
18+
creationDate={new Date("2023-01-01")}
19+
votes={2}
20+
voted={true}
21+
/>
22+
<QuestionItem
23+
title="Co się stanie gdy EventEmitter wyemituje event 'error', a nic na niego nie
24+
nasłuchuje?"
25+
level="senior"
26+
creationDate={new Date("2023-01-01")}
27+
votes={3}
28+
voted={true}
29+
/>
30+
</div>
31+
);
332
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Meta, StoryObj } from "@storybook/react";
2+
import { QuestionItem } from "./QuestionItem";
3+
4+
const meta: Meta<typeof QuestionItem> = {
5+
title: "QuestionItem",
6+
component: QuestionItem,
7+
args: {
8+
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
9+
creationDate: new Date(),
10+
votes: 1,
11+
voted: true,
12+
},
13+
};
14+
15+
export default meta;
16+
17+
type Story = StoryObj<typeof QuestionItem>;
18+
19+
export const Junior: Story = {
20+
args: {
21+
level: "junior",
22+
},
23+
};
24+
25+
export const Mid: Story = {
26+
args: {
27+
level: "mid",
28+
},
29+
};
30+
31+
export const Senior: Story = {
32+
args: {
33+
level: "senior",
34+
},
35+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Link from "next/link";
2+
import { format } from "../../utils/intl";
3+
import { QuestionLevel } from "./QuestionLevel";
4+
import { QuestionVoting } from "./QuestionVoting";
5+
import type { Level } from "./QuestionLevel";
6+
7+
type QuestionItemProps = Readonly<{
8+
title: string;
9+
votes: number;
10+
voted: boolean;
11+
level: Level;
12+
creationDate: Date;
13+
}>;
14+
15+
export const QuestionItem = ({ title, votes, voted, level, creationDate }: QuestionItemProps) => (
16+
<article className="flex h-36 bg-white p-5 text-sm text-gray-500 shadow-md">
17+
<QuestionVoting votes={votes} voted={voted} />
18+
<h3 className="grow">{title}</h3>
19+
<div className="ml-4 flex min-w-max flex-col items-end">
20+
<QuestionLevel level={level} />
21+
<Link href="#" className="mt-3 text-xs underline">
22+
<time dateTime={creationDate.toISOString()}>{format(creationDate)}</time>
23+
</Link>
24+
</div>
25+
</article>
26+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { twMerge } from "tailwind-merge";
2+
3+
const levelStyles = {
4+
junior: "bg-[#499dff]",
5+
mid: "bg-[#27be31]",
6+
senior: "bg-[#ffb90b]",
7+
};
8+
9+
export type Level = keyof typeof levelStyles;
10+
11+
export const QuestionLevel = ({ level }: { readonly level: Level }) => (
12+
<span
13+
className={twMerge(
14+
"w-20 rounded-full text-center capitalize leading-8 text-white",
15+
levelStyles[level],
16+
)}
17+
>
18+
{level}
19+
</span>
20+
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { twMerge } from "tailwind-merge";
2+
import { pluralize } from "../../utils/intl";
3+
4+
type QuestionVotingProps = Readonly<{
5+
votes: number;
6+
voted: boolean;
7+
}>;
8+
9+
const votesPluralize = pluralize("głos", "głosy", "głosów");
10+
11+
export const QuestionVoting = ({ votes, voted }: QuestionVotingProps) => {
12+
return (
13+
<button
14+
className={twMerge(
15+
"mr-4 flex h-fit items-center gap-x-1.5 text-gray-300 transition-colors",
16+
voted && "text-primary",
17+
)}
18+
type="button"
19+
aria-label={`To pytanie ma ${votes} ${votesPluralize(votes)}. Kliknij, aby ${
20+
voted ? "usunąć" : "dodać"
21+
} swój głos.`}
22+
>
23+
<svg
24+
xmlns="http://www.w3.org/2000/svg"
25+
version="1.1"
26+
width="15"
27+
height="13"
28+
fill="currentColor"
29+
className={twMerge("transition-transform duration-300", voted ? "rotate-180" : "rotate-0")}
30+
>
31+
<polygon points="7.5,0 15,13 0,13" />
32+
</svg>
33+
{votes}
34+
</button>
35+
);
36+
};

apps/app/src/utils/intl.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const LOCALE = "pl-PL";
2+
3+
const dateFormat = new Intl.DateTimeFormat(LOCALE, {
4+
day: "numeric",
5+
month: "long",
6+
year: "numeric",
7+
});
8+
const rules = new Intl.PluralRules(LOCALE);
9+
10+
export const pluralize = (one: string, few: string, many: string) => (count: number) => {
11+
return {
12+
zero: many,
13+
one,
14+
two: few,
15+
few,
16+
many,
17+
other: many,
18+
}[rules.select(count)];
19+
};
20+
21+
export const format = (date: Date) => {
22+
return dateFormat.format(date);
23+
};

0 commit comments

Comments
 (0)