Skip to content

Commit d1ffc61

Browse files
committedAug 1, 2022
WIP
1 parent 25f4c27 commit d1ffc61

18 files changed

+1245
-438
lines changed
 

‎package-lock.json

+749-223
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"@amatiasq/client-storage": "^3.0.3",
2323
"@amatiasq/emitter": "^4.1.2",
2424
"@amatiasq/scheduler": "^3.0.0",
25+
"@emotion/react": "^11.9.3",
26+
"@emotion/styled": "^11.9.3",
2527
"@monaco-editor/react": "^4.3.1",
2628
"axios": "^0.21.2",
2729
"body-parser": "^1.19.0",
@@ -37,11 +39,13 @@
3739
"uuid": "^3.4.0"
3840
},
3941
"devDependencies": {
42+
"@emotion/babel-plugin": "^11.9.5",
4043
"@types/react": "^17.0.1",
4144
"@types/react-dom": "^17.0.0",
4245
"@types/react-router-dom": "^5.1.7",
4346
"@types/uuid": "^8.3.0",
4447
"@typescript-eslint/eslint-plugin": "^4.14.2",
48+
"@vitejs/plugin-react": "^2.0.0",
4549
"cypress": "^9.1.1",
4650
"dotenv": "^10.0.0",
4751
"eslint": "^7.19.0",

‎src/5-app/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { createStore } from '../4-storage';
33
import { AppStorage } from '../4-storage/AppStorage';
44
import { useNavigator } from '../6-hooks/useNavigator';

‎src/7-components/NotesList/NoteGroup.scss

-8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
background-color: var(--group-active-color);
1010
}
1111

12-
.fav-counter {
13-
color: var(--favorite-color);
14-
}
15-
1612
.group-title {
1713
display: flex;
1814
align-items: center;
@@ -28,10 +24,6 @@
2824
flex: 1;
2925
}
3026

31-
.fav-counter,
32-
.counter {
33-
font-size: 0.8rem;
34-
}
3527

3628
&::-webkit-details-marker,
3729
&::marker {

‎src/7-components/NotesList/NoteGroup.tsx

+77-32
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,75 @@
1-
import React, { useEffect, useState } from 'react';
1+
import { css } from '@emotion/react';
2+
import styled from '@emotion/styled';
3+
import { useEffect, useState } from 'react';
24
import { Note } from '../../2-entities/Note';
35
import { useNavigator } from '../../6-hooks/useNavigator';
4-
import { Icon } from '../atoms/Icon';
5-
import './NoteGroup.scss';
6+
import { Disclosure, DisclosureToggleEvent } from '../molecule/Disclosure';
7+
import { GroupCounter } from '../molecule/GroupCounter';
68
import { NoteItem } from './NoteItem';
79

10+
const Title = styled.span`
11+
flex: 1;
12+
`;
13+
14+
const Content = styled.ul`
15+
border-left: var(--group-border-width) solid var(--group-border-color);
16+
border-bottom: var(--group-border-width) solid var(--group-border-color);
17+
`;
18+
19+
const NoteGroupItem = styled(NoteItem)`
20+
color: red;
21+
22+
&:not(.active) {
23+
--status-line-width: 0;
24+
}
25+
26+
&.active {
27+
margin-left: calc(var(--status-line-width) * -1);
28+
--status-line-color: var(--border-color-active);
29+
}
30+
`;
31+
32+
const styles = css`
33+
--status-line-color: var(--group-color);
34+
35+
&:hover summary {
36+
background-color: var(--bg-color-hover);
37+
}
38+
39+
&.has-active-note summary {
40+
background-color: var(--group-active-color);
41+
}
42+
43+
&[open] {
44+
summary {
45+
border-color: var(--group-border-color);
46+
}
47+
48+
${Content} {
49+
animation: details-show var(--animation-speed) ease-in-out;
50+
}
51+
52+
& + & summary {
53+
border-top-width: 0;
54+
}
55+
}
56+
57+
@keyframes details-show {
58+
from {
59+
opacity: 0;
60+
transform: var(--details-translate, translateY(-0.5em));
61+
}
62+
}
63+
`;
64+
865
const getGroupOpenId = (group: string) => `group-open:${group}`;
9-
export const isGroupOpen = (group: string) =>
66+
const isGroupOpen = (group: string) =>
1067
Boolean(localStorage[getGroupOpenId(group)]);
1168

1269
export function NoteGroup({ group, notes }: { group: string; notes: Note[] }) {
1370
const hasActiveNote = (nav: ReturnType<typeof useNavigator>) =>
1471
notes.some(x => nav.isNote(x));
1572

16-
const isOpen = isGroupOpen(group);
1773
const navigator = useNavigator();
1874
const [containsActiveNote, setContainsActiveNote] = useState(
1975
hasActiveNote(navigator),
@@ -30,39 +86,28 @@ export function NoteGroup({ group, notes }: { group: string; notes: Note[] }) {
3086

3187
const favorites = notes.filter(x => x.favorite);
3288

33-
const cn = [
34-
'group',
35-
containsActiveNote ? 'has-active' : '',
36-
favorites.length ? 'has-favorite' : '',
37-
];
38-
3989
return (
40-
<details className={cn.join(' ')} data-group={group} open={isOpen}>
41-
<summary className="group-title" onClick={onGroupClicked}>
42-
<Icon name="angle-right" className="icon-button group-caret" />
43-
<span className="group-name">{group}</span>
44-
{favorites.length ? (
45-
<>
46-
<i className="fav-counter">{favorites.length}</i> /
47-
</>
48-
) : null}
49-
<i className="counter">{notes.length}</i>
50-
</summary>
51-
52-
<ul className="group-content">
90+
<Disclosure
91+
className={containsActiveNote ? 'has-active-note' : undefined}
92+
isOpen={isGroupOpen(group)}
93+
onToggle={onGroupClicked}
94+
css={styles}
95+
>
96+
<>
97+
<Title>{group}</Title>
98+
<GroupCounter items={notes.length} favorites={favorites.length} />
99+
</>
100+
101+
<Content>
53102
{notes.map(x => (
54-
<NoteItem key={x.id} id={x.id} />
103+
<NoteGroupItem key={x.id} id={x.id} />
55104
))}
56-
</ul>
57-
</details>
105+
</Content>
106+
</Disclosure>
58107
);
59108
}
60109

61-
function onGroupClicked(event: React.MouseEvent<HTMLElement, MouseEvent>) {
62-
const target = (event.currentTarget as HTMLElement)
63-
.parentElement as HTMLDetailsElement;
64-
65-
const isOpen = target.hasAttribute('open');
110+
function onGroupClicked({ target, isOpen }: DisclosureToggleEvent) {
66111
const key = getGroupOpenId(target.dataset.group!);
67112

68113
console.debug(`> ${key}: ${!isOpen}`);

‎src/7-components/NotesList/NoteItem.scss

-62
This file was deleted.
+86-55
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,67 @@
1-
import React, { useEffect, useState } from 'react';
1+
import styled from '@emotion/styled';
2+
import React, { useCallback, useEffect, useState } from 'react';
23
import { Link } from 'react-router-dom';
34
import { NoteId } from '../../2-entities/Note';
45
import { useNavigator } from '../../6-hooks/useNavigator';
56
import { useNote } from '../../6-hooks/useNote';
67
import { useUsername } from '../../6-hooks/useUsername';
7-
import { GithubIcon } from '../atoms/GithubIcon';
88
import { IconButton } from '../atoms/IconButton';
9-
import './NoteItem.scss';
9+
import { IconLink } from '../atoms/IconLink';
10+
import { GithubIcon, TrashIcon } from '../atoms/icons';
11+
import { FavoriteButton } from '../molecule/FavoriteButton';
12+
13+
const Actions = styled.div`
14+
display: flex;
15+
align-items: center;
16+
gap: var(--sidebar-gap);
17+
`;
18+
19+
const Title = styled.h5`
20+
flex: 1;
21+
overflow: hidden;
22+
white-space: nowrap;
23+
text-overflow: ellipsis;
24+
line-height: 1.5em;
25+
`;
26+
27+
const NoteItemContainer = styled(Link)`
28+
display: flex;
29+
align-items: center;
30+
cursor: pointer;
31+
gap: var(--sidebar-gap);
32+
padding: var(--sidebar-gap);
33+
font-weight: 500;
34+
// border-left: var(--status-line-width) solid var(--status-line-color);
35+
border-bottom: 1px solid transparent;
36+
background-color: var(--note-item-color);
37+
user-select: none;
38+
color: var(--fg-color);
39+
text-decoration: none;
40+
41+
&:hover {
42+
background-color: var(--bg-color-hover);
43+
}
44+
45+
&:not(:hover) {
46+
${Actions} {
47+
display: none;
48+
}
49+
50+
&:not(.favorite) .star {
51+
visibility: hidden;
52+
}
53+
}
54+
55+
&.active {
56+
color: var(--fg-color-active);
57+
background-color: var(--bg-color-active);
58+
}
59+
`;
1060

1161
export function NoteItem({ id }: { id: NoteId }) {
1262
const navigator = useNavigator();
1363
const username = useUsername();
14-
const [note, { toggleFavorite, remove }] = useNote(id);
64+
const [note, { remove }] = useNote(id);
1565
const [active, setActive] = useState<boolean>(navigator.isNote(id));
1666

1767
useEffect(() =>
@@ -24,64 +74,45 @@ export function NoteItem({ id }: { id: NoteId }) {
2474
}),
2575
);
2676

27-
if (!note) return null;
28-
29-
const githubUrl = `https://github.com/${username}/pensieve-data/blob/main/note/${note.id}`;
77+
const handleRemove = useCallback(
78+
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
79+
if (!confirm(`Delete ${note!.title}?`)) {
80+
return;
81+
}
3082

31-
const cn = [
32-
'note-item',
33-
note.favorite ? 'favorite' : '',
34-
active ? 'active' : '',
35-
];
83+
event.preventDefault();
3684

37-
const extraProps = note.group ? { 'data-group': note.group } : {};
85+
if (navigator.isNote(id)) {
86+
navigator.goRoot();
87+
}
3888

39-
const ghLink = (
40-
<a
41-
target="_blank"
42-
href={githubUrl}
43-
onClick={event => event.stopPropagation()}
44-
>
45-
<GithubIcon title="Open note in Github" />
46-
</a>
89+
return remove();
90+
},
91+
[navigator, note, remove],
4792
);
4893

94+
if (!note) return null;
95+
96+
const githubUrl = `https://github.com/${username}/pensieve-data/blob/main/note/${note.id}`;
97+
4998
return (
50-
<Link className={cn.join(' ')} {...extraProps} to={navigator.toNote(note)}>
51-
<div className="star-part">
99+
<NoteItemContainer
100+
to={navigator.toNote(note)}
101+
className={`${active ? 'active' : ''} ${note.favorite ? 'favorite' : ''}`}
102+
>
103+
<FavoriteButton id={note.id} className="star" />
104+
<Title>{note.title}</Title>
105+
106+
<Actions>
107+
<IconLink
108+
icon={<GithubIcon title="Open note in Github" />}
109+
href={githubUrl}
110+
/>
52111
<IconButton
53-
icon={note.favorite ? 'star' : 'far star'}
54-
onClick={applyFavorite}
112+
icon={<TrashIcon title="Remove note" />}
113+
onClick={handleRemove}
55114
/>
56-
</div>
57-
58-
<h5 className="title-part">{note.title}</h5>
59-
60-
<div className="actions-part">
61-
{ghLink}
62-
<IconButton icon="trash" onClick={applyRemove} />
63-
</div>
64-
</Link>
115+
</Actions>
116+
</NoteItemContainer>
65117
);
66-
67-
type ClickEvent = React.MouseEvent<HTMLButtonElement, MouseEvent>;
68-
69-
function applyRemove(event: ClickEvent) {
70-
if (!confirm(`Delete ${note!.title}?`)) {
71-
return;
72-
}
73-
74-
event.preventDefault();
75-
76-
if (navigator.isNote(id)) {
77-
navigator.goRoot();
78-
}
79-
80-
return remove();
81-
}
82-
83-
function applyFavorite(event: ClickEvent) {
84-
event.preventDefault();
85-
toggleFavorite();
86-
}
87118
}

‎src/7-components/atoms/Button.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { ButtonHTMLAttributes } from 'react';
1+
import { ButtonHTMLAttributes } from 'react';
22

33
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
44

55
export function Button(props: ButtonProps) {
6-
return <button type="button" role="button" {...props}></button>;
6+
return <button type="button" role="button" {...props} />;
77
}

‎src/7-components/atoms/GithubIcon.tsx

-21
This file was deleted.

‎src/7-components/atoms/IconButton.scss

-26
This file was deleted.

‎src/7-components/atoms/IconButton.tsx

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
1-
import React from 'react';
1+
import { css } from '@emotion/react';
22
import { Button, ButtonProps } from './Button';
33
import { Icon } from './Icon';
4-
import './IconButton.scss';
54

6-
export function IconButton(props: ButtonProps & { icon: string }) {
7-
const { icon, className, ...buttonProps } = props;
5+
export interface IconButtonProps extends ButtonProps {
6+
icon: JSX.Element | string;
7+
}
8+
9+
export function IconButton(props: IconButtonProps) {
10+
const { icon, ...buttonProps } = props;
11+
12+
const styles = css`
13+
--size: 2rem;
14+
line-height: var(--size);
15+
height: var(--size);
16+
width: var(--size);
17+
border-radius: 100%;
18+
text-align: center;
19+
color: #b7b7b7;
20+
21+
:hover {
22+
background: rgb(255 255 255 / 0.2);
23+
}
24+
25+
svg {
26+
fill: var(--fg-color);
27+
}
28+
`;
829

9-
const classes = `icon-button ${className || ''}`;
30+
const foo = typeof icon === 'string' ? <Icon name={icon} /> : icon;
1031

1132
return (
12-
<Button {...buttonProps} className={classes}>
13-
<Icon name={icon} />
33+
<Button {...buttonProps} css={styles}>
34+
{foo}
1435
</Button>
1536
);
1637
}

‎src/7-components/atoms/IconLink.tsx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { css } from '@emotion/react';
2+
import { AnchorHTMLAttributes } from 'react';
3+
4+
export interface IconLinkProps extends AnchorHTMLAttributes<any> {
5+
icon: JSX.Element;
6+
}
7+
8+
export function IconLink(props: IconLinkProps) {
9+
const { icon, ...linkProps } = props;
10+
11+
const styles = css`
12+
--size: 2rem;
13+
line-height: var(--size);
14+
height: var(--size);
15+
width: var(--size);
16+
border-radius: 100%;
17+
text-align: center;
18+
19+
text-decoration: none;
20+
color: hsl(0, 0%, 70%);
21+
22+
:hover {
23+
background: rgb(255 255 255 / 0.2);
24+
}
25+
26+
svg {
27+
fill: var(--fg-color);
28+
}
29+
`;
30+
31+
return (
32+
<a
33+
target="_blank"
34+
{...linkProps}
35+
onClick={event => event.stopPropagation()}
36+
css={styles}
37+
>
38+
{icon}
39+
</a>
40+
);
41+
}

‎src/7-components/atoms/icons.tsx

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { jsx } from '@emotion/react';
2+
import { Children } from 'react';
3+
4+
export interface IconProps {
5+
title: string;
6+
// This didn't work, don't know why
7+
// css?: SerializedStyles;
8+
}
9+
10+
function icon(svg: JSX.Element) {
11+
return function Icon({ title }: IconProps) {
12+
const {
13+
type,
14+
props: { children, ...props },
15+
} = svg;
16+
17+
return jsx(
18+
type,
19+
props,
20+
<title>{title}</title>,
21+
...Children.toArray(children),
22+
);
23+
};
24+
}
25+
26+
export const TrashIcon = icon(
27+
<svg
28+
width="24"
29+
height="24"
30+
xmlns="http://www.w3.org/2000/svg"
31+
fillRule="evenodd"
32+
clipRule="evenodd"
33+
>
34+
<path d="M19 24h-14c-1.104 0-2-.896-2-2v-16h18v16c0 1.104-.896 2-2 2m-9-14c0-.552-.448-1-1-1s-1 .448-1 1v9c0 .552.448 1 1 1s1-.448 1-1v-9zm6 0c0-.552-.448-1-1-1s-1 .448-1 1v9c0 .552.448 1 1 1s1-.448 1-1v-9zm6-5h-20v-2h6v-1.5c0-.827.673-1.5 1.5-1.5h5c.825 0 1.5.671 1.5 1.5v1.5h6v2zm-12-2h4v-1h-4v1z" />
35+
</svg>,
36+
);
37+
38+
export const GithubIcon = icon(
39+
<svg
40+
xmlns="http://www.w3.org/2000/svg"
41+
width="24"
42+
height="24"
43+
viewBox="0 0 24 24"
44+
>
45+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
46+
</svg>,
47+
);
48+
49+
export const StarIcon = icon(
50+
<svg
51+
clipRule="evenodd"
52+
fillRule="evenodd"
53+
strokeLinejoin="round"
54+
strokeMiterlimit="2"
55+
viewBox="0 0 24 24"
56+
xmlns="http://www.w3.org/2000/svg"
57+
>
58+
<path
59+
d="m11.322 2.923c.126-.259.39-.423.678-.423.289 0 .552.164.678.423.974 1.998 2.65 5.44 2.65 5.44s3.811.524 6.022.829c.403.055.65.396.65.747 0 .19-.072.383-.231.536-1.61 1.538-4.382 4.191-4.382 4.191s.677 3.767 1.069 5.952c.083.462-.275.882-.742.882-.122 0-.244-.029-.355-.089-1.968-1.048-5.359-2.851-5.359-2.851s-3.391 1.803-5.359 2.851c-.111.06-.234.089-.356.089-.465 0-.825-.421-.741-.882.393-2.185 1.07-5.952 1.07-5.952s-2.773-2.653-4.382-4.191c-.16-.153-.232-.346-.232-.535 0-.352.249-.694.651-.748 2.211-.305 6.021-.829 6.021-.829s1.677-3.442 2.65-5.44z"
60+
fillRule="nonzero"
61+
/>
62+
</svg>,
63+
);
64+
65+
export const CaretIcon = icon(
66+
<svg
67+
clipRule="evenodd"
68+
fillRule="evenodd"
69+
strokeLinejoin="round"
70+
strokeMiterlimit="2"
71+
viewBox="0 0 24 24"
72+
xmlns="http://www.w3.org/2000/svg"
73+
>
74+
<path d="m10.211 7.155c-.141-.108-.3-.157-.456-.157-.389 0-.755.306-.755.749v8.501c0 .445.367.75.755.75.157 0 .316-.05.457-.159 1.554-1.203 4.199-3.252 5.498-4.258.184-.142.29-.36.29-.592 0-.23-.107-.449-.291-.591-1.299-1.002-3.945-3.044-5.498-4.243z" />
75+
</svg>,
76+
);
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import styled from '@emotion/styled';
2+
import React, {
3+
Children,
4+
DetailsHTMLAttributes,
5+
PropsWithChildren,
6+
useCallback,
7+
} from 'react';
8+
import { CaretIcon } from '../atoms/icons';
9+
10+
const Details = styled.details`
11+
summary svg:first-of-type {
12+
transform: rotate(0);
13+
transform-origin: 50% 50%;
14+
transition: var(--animation-speed) transform ease;
15+
}
16+
17+
&[open] summary svg:first-of-type {
18+
transform: rotate(90deg);
19+
}
20+
`;
21+
22+
const Summary = styled.summary`
23+
display: flex;
24+
align-items: center;
25+
gap: var(--sidebar-gap);
26+
padding: var(--sidebar-gap);
27+
padding-right: calc(var(--sidebar-gap) * 2);
28+
cursor: default;
29+
background-color: var(--group-color);
30+
border-left: var(--group-border-width) solid transparent;
31+
border-top: var(--group-border-width) solid transparent;
32+
33+
list-style: none;
34+
&::-webkit-details-marker,
35+
&::marker {
36+
display: none;
37+
content: '';
38+
}
39+
40+
svg:first-of-type {
41+
width: 2em;
42+
fill: var(--fg-color);
43+
}
44+
`;
45+
46+
export interface DisclosureToggleEvent {
47+
target: HTMLDetailsElement;
48+
isOpen: boolean;
49+
}
50+
51+
export interface DisclosureProps
52+
extends Omit<DetailsHTMLAttributes<any>, 'open' | 'onToggle'> {
53+
isOpen: boolean;
54+
onToggle: (event: DisclosureToggleEvent) => void;
55+
}
56+
57+
export function Disclosure({
58+
isOpen,
59+
onToggle,
60+
children,
61+
...props
62+
}: PropsWithChildren<DisclosureProps>) {
63+
const handleClick = useCallback(
64+
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
65+
const target = (event.currentTarget as HTMLElement)
66+
.parentElement as HTMLDetailsElement;
67+
68+
const isOpen = target.hasAttribute('open');
69+
70+
onToggle({ target, isOpen });
71+
},
72+
[onToggle],
73+
);
74+
75+
const [summary, ...content] = Children.toArray(children);
76+
77+
return (
78+
<Details {...props} open={isOpen}>
79+
<Summary onClick={handleClick}>
80+
<CaretIcon title="Open / close group" />
81+
{summary}
82+
</Summary>
83+
{content}
84+
</Details>
85+
);
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import styled from '@emotion/styled';
2+
import { useCallback } from 'react';
3+
import { NoteId } from '../../2-entities/Note';
4+
import { useNote } from '../../6-hooks/useNote';
5+
import { IconButton, IconButtonProps } from '../atoms/IconButton';
6+
import { StarIcon } from '../atoms/icons';
7+
8+
type ClickEvent = React.MouseEvent<HTMLButtonElement, MouseEvent>;
9+
10+
const FavouriteButtonContainer = styled(IconButton)`
11+
svg {
12+
fill: var(--favorite-color);
13+
stroke: var(--favorite-color);
14+
stroke-width: 1px;
15+
}
16+
17+
&.off {
18+
svg {
19+
fill: transparent;
20+
stroke-width: 2px;
21+
}
22+
}
23+
`;
24+
25+
export interface FavoriteButtonProps
26+
extends Omit<IconButtonProps, 'icon' | 'onClick'> {
27+
id: NoteId;
28+
}
29+
30+
export function FavoriteButton({ id, ...props }: FavoriteButtonProps) {
31+
const [note, { toggleFavorite }] = useNote(id);
32+
33+
const handleClick = useCallback(
34+
(event: ClickEvent) => {
35+
event.preventDefault();
36+
toggleFavorite();
37+
},
38+
[toggleFavorite],
39+
);
40+
41+
if (!note) return null;
42+
43+
const icon = note.favorite ? (
44+
<StarIcon title="Remove from favorites" />
45+
) : (
46+
<StarIcon title="Add to favorites" />
47+
);
48+
49+
return (
50+
<FavouriteButtonContainer
51+
{...props}
52+
icon={icon}
53+
className={note.favorite ? 'on' : 'off'}
54+
onClick={handleClick}
55+
/>
56+
);
57+
}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import styled from '@emotion/styled';
2+
3+
const Counter = styled.i`
4+
font-size: 0.8rem;
5+
`;
6+
7+
const FavCounter = styled.i`
8+
color: var(--favorite-color);
9+
font-size: 0.8rem;
10+
`;
11+
12+
export interface GroupCounterProps {
13+
favorites: number;
14+
items: number;
15+
}
16+
17+
export function GroupCounter({ favorites, items }: GroupCounterProps) {
18+
const itemsCounter = <Counter>{items}</Counter>;
19+
20+
if (!favorites) {
21+
return itemsCounter;
22+
}
23+
24+
return (
25+
<>
26+
<FavCounter>{favorites}</FavCounter> / {itemsCounter}
27+
</>
28+
);
29+
}

‎tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"experimentalDecorators": true,
88
"forceConsistentCasingInFileNames": true,
99
"isolatedModules": true,
10-
"jsx": "react",
10+
"jsx": "react-jsx",
11+
"jsxImportSource": "@emotion/react",
1112
"lib": ["DOM", "DOM.Iterable", "ESNext"],
1213
"module": "ESNext",
1314
"moduleResolution": "Node",

‎vite.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// import reactRefresh from '@vitejs/plugin-react-refresh';
2+
import react from '@vitejs/plugin-react';
23
import { readFileSync } from 'fs';
34
import { defineConfig } from 'vite';
45
import { VitePWA } from 'vite-plugin-pwa';
@@ -18,6 +19,12 @@ export default defineConfig({
1819
importScripts: [],
1920
},
2021
}),
22+
react({
23+
jsxImportSource: '@emotion/react',
24+
babel: {
25+
plugins: ['@emotion/babel-plugin'],
26+
},
27+
}),
2128
],
2229
server: {
2330
port: 1234,

0 commit comments

Comments
 (0)
Please sign in to comment.