diff --git a/.flowconfig b/.flowconfig index b68bfc2..1b2688c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -7,8 +7,27 @@ [lints] [options] -module.name_mapper='^@lib' ->'/src/lib' -module.name_mapper='^@features' ->'/src/features' +all=false +server.max_workers=1 +include_warnings=false +max_header_tokens=5 +esproposal.class_instance_fields=enable +esproposal.class_static_fields=enable +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue +suppress_comment=\\(.\\|\n\\)*\\$off +suppress_comment=\\(.\\|\n\\)*\\$todo +suppress_type=$FlowIssue +suppress_type=$off +suppress_type=$todo +emoji=true +esproposal.decorators=ignore +module.use_strict=true +esproposal.export_star_as=enable + +module.name_mapper='^@lib/' ->'/src/lib/' +module.name_mapper='^@features/' ->'/src/features/' module.name_mapper='^@howtocards/ui' ->'/src/ui' [strict] diff --git a/src/features/cards/index.js b/src/features/cards/index.js index c91b4cc..5bfcf7f 100644 --- a/src/features/cards/index.js +++ b/src/features/cards/index.js @@ -4,7 +4,7 @@ import { type Card } from "./types" export { $registry as $cardsRegistry } from "./model/registry.store" export { cardsToObject } from "./model/registry.model" -export { CardItem, CardsList } from "./organisms" +export { CardItem, CardsList, SkeletonList } from "./organisms" export { cardsRoutes } from "./routes" export { Card } diff --git a/src/features/cards/model/home.js b/src/features/cards/model/home.js index c3e4537..6f7b3ec 100644 --- a/src/features/cards/model/home.js +++ b/src/features/cards/model/home.js @@ -3,6 +3,7 @@ import { createEvent, createEffect, createStore } from "effector" import type { Event, Effect, Store } from "effector" import { createFetching, type Fetching } from "@lib/fetching" + import { cardsApi } from "../api" import type { Card } from "../types" import { $registry } from "./registry.store" @@ -34,5 +35,3 @@ $registry.on(homeCardsLoading.done, (registry, { result }) => { pageReady.watch(() => { homeCardsLoading() }) - -export const cardsFetching = createFetching(homeCardsLoading, "loading") diff --git a/src/features/cards/organisms/cards-list.js b/src/features/cards/organisms/cards-list.js index 53da3ab..8cb4e35 100644 --- a/src/features/cards/organisms/cards-list.js +++ b/src/features/cards/organisms/cards-list.js @@ -1,16 +1,15 @@ -import React from "react" -import PropTypes from "prop-types" -import { createStoreObject } from "effector" +// @flow +import * as React from "react" import { createComponent } from "effector-react" import styled from "styled-components" import { ConditionalList } from "@howtocards/ui" -import { cardsFetching } from "../model/home" import { $registry, getCard } from "../model/registry.store" import { setUsefulMark } from "../model/registry.events" -import { CardSkeleton } from "./card-skeleton" +import { type Card } from "../types" +import { CardItem } from "./card-item" -const onUsefulClick = (cardId) => { +const handleUsefulClick = (cardId) => { const card = getCard(cardId) if (card) { setUsefulMark({ cardId: card.id, isUseful: !card.meta.isUseful }) @@ -18,46 +17,42 @@ const onUsefulClick = (cardId) => { } const selectCards = (props) => - createStoreObject({ - cards: $registry.map((reg) => props.ids.map((id) => reg[id])), - isLoading: cardsFetching.isLoading, - }) + $registry.map((reg) => props.ids.map((id) => reg[id])) -export const CardsList = createComponent( +type Props = { + ids: number[], + renderCard?: (param: { card: Card, onUsefulClick: () => * }) => React.Node, + renderEmpty?: () => React.Node, +} + +export const CardsList = createComponent( selectCards, - ({ renderCard }, { cards, isLoading }) => { - return ( - <> - {isLoading ? ( - <> - - - - - ) : ( - ( - - {list.filter(Boolean).map((card) => - renderCard({ - card, - onUsefulClick: () => onUsefulClick(card.id), - }), - )} - - )} - /> - )} - - ) - }, + ({ renderCard = defaultCardRender, renderEmpty = emptyRenderer }, cards) => ( + ( + + {list.filter(Boolean).map((card) => + renderCard({ + card, + onUsefulClick: () => handleUsefulClick(card.id), + }), + )} + + )} + renderEmpty={renderEmpty} + /> + ), ) -CardsList.propTypes = { - ids: PropTypes.arrayOf(PropTypes.number).isRequired, - renderCard: PropTypes.func.isRequired, -} +const emptyRenderer = () =>

No cards in that list

+const defaultCardRender = ({ + card, + onUsefulClick, +}: { + card: Card, + onUsefulClick: () => *, +}) => export const CardsItemsBlock = styled.div` display: flex; diff --git a/src/features/cards/organisms/index.js b/src/features/cards/organisms/index.js index 7938b67..d175064 100644 --- a/src/features/cards/organisms/index.js +++ b/src/features/cards/organisms/index.js @@ -1,2 +1,5 @@ +// @flow export { CardItem } from "./card-item" +export { CardSkeleton } from "./card-skeleton" export { CardsList } from "./cards-list" +export { SkeletonList } from "./skeleton-list" diff --git a/src/features/cards/organisms/skeleton-list.js b/src/features/cards/organisms/skeleton-list.js new file mode 100644 index 0000000..b7c49a0 --- /dev/null +++ b/src/features/cards/organisms/skeleton-list.js @@ -0,0 +1,34 @@ +// @flow +import * as React from "react" +import { CardsList } from "./cards-list" +import { CardSkeleton } from "./card-skeleton" + +type Props = { + isLoading: boolean, + ids: number[], + count?: number, + renderEmpty: () => React.Node, + renderCard?: *, +} + +export const SkeletonList = ({ + isLoading, + ids, + count = 3, + renderEmpty, + renderCard, +}: Props) => + isLoading ? ( + <> + {Array.from({ length: count }, (_, idx) => ( + + ))} + + ) : ( + + ) + +SkeletonList.defaultProps = { + count: undefined, + renderCard: undefined, +} diff --git a/src/features/cards/pages/home.js b/src/features/cards/pages/home.js index b108391..a685d8b 100644 --- a/src/features/cards/pages/home.js +++ b/src/features/cards/pages/home.js @@ -1,25 +1,28 @@ -import React, { useEffect } from "react" +// @flow +import * as React from "react" import { useStore } from "effector-react" -import { Col, Row } from "lib/styled-components-layout" +import { Col, Row } from "@lib/styled-components-layout" import { H2 } from "@howtocards/ui" -import { $cardsIds, pageReady } from "../model/home" + +import { $cardsIds, pageReady, homeCardsFetching } from "../model/home" import { CardsCommonTemplate } from "../templates/common" -import { CardsList, CardItem } from "../organisms" +import { SkeletonList } from "../organisms" export const CardsHomePage = () => { const ids = useStore($cardsIds) - useEffect(() => { + const isLoading = useStore(homeCardsFetching.isLoading) + + React.useEffect(() => { pageReady() }, []) return ( }> - ( - - )} + renderEmpty={() =>

What about to create new card?

} />
) diff --git a/src/features/cards/pages/view.js b/src/features/cards/pages/view.js index b815fc6..7195bfd 100644 --- a/src/features/cards/pages/view.js +++ b/src/features/cards/pages/view.js @@ -1,40 +1,51 @@ +// @flow /* eslint-disable import/no-duplicates */ -import React from "react" +import * as React from "react" import PropTypes from "prop-types" import { useStore } from "effector-react" import { distanceInWordsToNow } from "date-fns" -import { Col, Row } from "lib/styled-components-layout" +import { Col, Row } from "@lib/styled-components-layout" import { Text } from "@howtocards/ui" import { CardsCommonTemplate } from "../templates/common" import { cardLoading, $card } from "../model/view" -import { CardItem, CardsList } from "../organisms" +import { CardItem, CardSkeleton, CardsList } from "../organisms" -export const CardViewPage = ({ match }) => { +type Props = { + match: { + params: { + cardId: string, + }, + }, +} + +export const CardViewPage = ({ match }: Props) => { const cardId = parseInt(match.params.cardId, 10) + React.useEffect(() => { cardLoading({ cardId }) }, [cardId]) const current = useStore($card) - if (!current) { - return

Loading...

- } - return ( }> - ( - - )} - /> + {current ? ( + ( + + )} + /> + ) : ( + + )} ) } @@ -50,13 +61,26 @@ CardViewPage.propTypes = { const Sidebar = ({ card }) => ( - - Created {distanceInWordsToNow(card.createdAt, { addSuffix: true })} - + {card ? ( + + Created {distanceInWordsToNow(card.createdAt, { addSuffix: true })} + + ) : ( +

 

+ )}
) Sidebar.propTypes = { - card: PropTypes.shape({}).isRequired, + card: PropTypes.shape({}), +} + +Sidebar.defaultProps = { + card: null, +} + +function createdAt(card) { + const date = new Date(card.createdAt) + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` } diff --git a/src/features/users/pages/user.js b/src/features/users/pages/user.js index 5edf3ce..1440e0d 100644 --- a/src/features/users/pages/user.js +++ b/src/features/users/pages/user.js @@ -2,13 +2,13 @@ import * as React from "react" import PropTypes from "prop-types" import { useStore } from "effector-react" +import styled from "styled-components" import { Col, Row } from "@lib/styled-components-layout" import { H3, H1, ZeroTab, Button, Link } from "@howtocards/ui" -import { CardsList, CardItem } from "@features/cards" +import { SkeletonList } from "@features/cards" import { UsersCommonTemplate } from "../templates/common" -import { LoadingView } from "../organisms/loading" import { ErrorView } from "../organisms/error" import { $user, @@ -28,9 +28,9 @@ type Props = { } export const UserPage = ({ match }: Props) => { - const [filterBy, setFilter] = React.useState("all") - + const [tab, setTab] = React.useState<"created" | "useful">("useful") const userId = parseInt(match.params.userId, 10) + const user = useStore($user) const { created, useful } = useStore($cards) const isLoading = useStore($isLoading) @@ -41,72 +41,54 @@ export const UserPage = ({ match }: Props) => { pageMounted({ userId }) }, [userId]) - if (isLoading) return if (isFailed) return - const renderFiltered = (filter) => { - switch (filter) { - case "useful": - return ( - ( - <> - -

No useful cards for you?

-
- - - - - )} - /> - ) - case "my": - default: - return ( - ( - <> - -

You don't have cards yet

-
- - - - - )} - /> - ) - } - } - return ( }> - { - setFilter("my") - }} - > - My cards + setTab("useful")}> + Useful | - { - setFilter("useful") - }} - > - Favorite + setTab("created")}> + My cards - {renderFiltered(filterBy)} + ( + + +

No useful cards?

+
+ + + + + )} + /> + ( + <> + +

No created cards yet

+
+ + + + + )} + />
) } @@ -150,28 +132,28 @@ CurrentUserInfo.propTypes = { const displayName = (user) => (user && user.displayName) || "user" -const NamedCardsList = ({ cards, title, renderEmpty = () => null }) => { - if (cards && cards.length !== 0) { - return ( - <> -

{title}

- - React.createElement(CardItem, { - card, - key: card.id, - onUsefulClick, - }) - } - /> - - ) - } - return {renderEmpty()} +const NamedCardsList = ({ + show, + isLoading, + cards, + title, + renderEmpty = () => null, +}) => { + return ( + +

{title}

+ +
+ ) } NamedCardsList.propTypes = { + show: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, cards: PropTypes.arrayOf(PropTypes.number).isRequired, title: PropTypes.string.isRequired, renderEmpty: PropTypes.func, @@ -180,3 +162,8 @@ NamedCardsList.propTypes = { NamedCardsList.defaultProps = { renderEmpty: () => null, } + +const ListWrapper = styled.div` + display: ${(p) => (p.show ? "flex" : "none")}; + flex-direction: column; +` diff --git a/src/ui/atoms/button.js b/src/ui/atoms/button.js index 596b788..68186c5 100644 --- a/src/ui/atoms/button.js +++ b/src/ui/atoms/button.js @@ -69,4 +69,10 @@ export const ZeroTab = styled(ZeroButton)` &:hover { color: ${({ theme }) => theme.palette.primary.initial.background}; } + + ${(p) => + p.active && + css` + text-decoration: underline; + `} ` diff --git a/src/ui/atoms/index.js b/src/ui/atoms/index.js index e03699e..4bf9be4 100644 --- a/src/ui/atoms/index.js +++ b/src/ui/atoms/index.js @@ -1,3 +1,4 @@ +// @flow export { Button, ButtonPrimary, ZeroButton, ZeroTab } from "./button" export { Card, CardSticky, CardNarrow } from "./card" export { ErrorBox } from "./error-box" diff --git a/src/ui/index.js b/src/ui/index.js index 482040e..791c847 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -1,3 +1,4 @@ +// @flow export * from "./atoms" export * from "./molecules" export * from "./organisms" diff --git a/src/ui/molecules/index.js b/src/ui/molecules/index.js index b57dc87..3b5fb99 100644 --- a/src/ui/molecules/index.js +++ b/src/ui/molecules/index.js @@ -1,2 +1,3 @@ +// @flow export { Sidebar } from "./sidebar" export { TextArea } from "./textarea" diff --git a/src/ui/organisms/conditional-list.js b/src/ui/organisms/conditional-list.js index 2dca207..f93a96f 100644 --- a/src/ui/organisms/conditional-list.js +++ b/src/ui/organisms/conditional-list.js @@ -1,21 +1,24 @@ -import React, { Fragment } from "react" -import PropTypes from "prop-types" +// @flow +import * as React from "react" -export const ConditionalList = ({ list, renderExists, renderEmpty }) => ( - +type Props = { + list: T[], + renderExists: (T[]) => React.Node, + renderEmpty?: () => React.Node, +} + +export const ConditionalList = ({ + list, + renderExists, + renderEmpty = () => null, +}: Props) => ( + <> {list && list.filter(Boolean).length > 0 ? renderExists(list) : renderEmpty()} - + ) -ConditionalList.propTypes = { - // eslint-disable-next-line react/forbid-prop-types - list: PropTypes.array.isRequired, - renderExists: PropTypes.func.isRequired, - renderEmpty: PropTypes.func, -} - ConditionalList.defaultProps = { - renderEmpty: () =>

Not Found

, + renderEmpty: undefined, } diff --git a/src/ui/organisms/index.js b/src/ui/organisms/index.js index 2d6d085..98fafcf 100644 --- a/src/ui/organisms/index.js +++ b/src/ui/organisms/index.js @@ -1,3 +1,4 @@ +// @flow export { PrimitiveFooter } from "./primitive-footer" export { ItemsList } from "./items-list" export { ConditionalList } from "./conditional-list" diff --git a/src/ui/templates/index.js b/src/ui/templates/index.js index 33ef912..ac39be3 100644 --- a/src/ui/templates/index.js +++ b/src/ui/templates/index.js @@ -1,3 +1,4 @@ +// @flow export { CenterContentTemplate } from "./center-content" export { Container } from "./container" export { MainTemplate } from "./main-template"