diff --git a/src/Cards/src/components/card.stories.tsx b/src/Cards/src/components/card.stories.tsx index 00c6f7a..297a9d5 100644 --- a/src/Cards/src/components/card.stories.tsx +++ b/src/Cards/src/components/card.stories.tsx @@ -1,6 +1,9 @@ +import * as A from "fp-ts/Array"; import * as IO from "fp-ts/IO"; import * as O from "fp-ts/Option"; +import * as S from "fp-ts/string"; import * as RAND from "fp-ts/Random"; +import * as ORD from "fp-ts/Ord"; import { flow, pipe } from "fp-ts/function"; import { fakerD } from "../utils/faker"; @@ -9,67 +12,64 @@ import { action } from "@storybook/addon-actions"; import { memoize } from "fp-ts-std/IO"; import { RemoteImageAdt } from "../model/remote-image"; import { Meta, StoryObj } from "../utils/storybook"; -import { Card } from "./card"; +import { Card, CardData, CardProps } from "./card"; -const getCardData = flow( +type StoryData = Pick; + +const ByName: ORD.Ord = pipe( + S.Ord, + ORD.contramap(([name]) => name), +); + +const getStoryData = flow( fakerD, - faker => ({ - name: O.some(`${faker.person.firstName()} ${faker.person.lastName()}`), - job: O.some(faker.person.jobTitle()), - sub: O.some(faker.company.buzzVerb()), - - avatar: faker.image.avatar(), - - phone: O.some(faker.phone.number()), - mail: O.some(faker.internet.email()), - web: O.some(faker.internet.url()), - twitter: O.some(faker.hacker.noun()), - facebook: O.some(faker.hacker.noun()), - youtube: O.some(faker.hacker.noun()), - instagram: O.some(faker.hacker.noun()), - twitch: O.some(faker.hacker.noun()), - github: O.some(faker.hacker.noun()), - linkedIn: O.some(faker.hacker.noun()), - xing: O.some(faker.hacker.noun()), - paypal: O.some(faker.hacker.noun()), - patreon: O.some(faker.hacker.noun()), - pinterest: O.some(faker.hacker.noun()), - npm: O.some(faker.hacker.noun()), - soundcloud: O.some(faker.hacker.noun()), - snapchat: O.some(faker.hacker.noun()), - steam: O.some(faker.hacker.noun()), - cpan: O.some(faker.hacker.noun()), - signal: O.some(faker.hacker.noun()), - telegram: O.some(faker.hacker.noun()), - }), + (faker): StoryData => { + const avatar = faker.image.avatar(); + + return ({ + data: { + name: O.some(`${faker.person.firstName()} ${faker.person.lastName()}`), + job: O.some(faker.person.jobTitle()), + sub: O.some(faker.company.buzzVerb()), + + info: [ + ["phone", faker.phone.number()], + ["mail", faker.internet.email()], + ["web", faker.internet.url()], + ["twitter", faker.hacker.noun()], + ["facebook", faker.hacker.noun()], + ["youtube", faker.hacker.noun()], + ["instagram", faker.hacker.noun()], + ["twitch", faker.hacker.noun()], + ["github", faker.hacker.noun()], + ["linkedIn", faker.hacker.noun()], + ["xing", faker.hacker.noun()], + ["paypal", faker.hacker.noun()], + ["patreon", faker.hacker.noun()], + ["pinterest", faker.hacker.noun()], + ["npm", faker.hacker.noun()], + ["soundcloud", faker.hacker.noun()], + ["snapchat", faker.hacker.noun()], + ["steam", faker.hacker.noun()], + ["cpan", faker.hacker.noun()], + ["signal", faker.hacker.noun()], + ["telegram", faker.hacker.noun()], + ] + }, + avatar: RemoteImageAdt.of.Loaded({ + remoteUrl: avatar, + objectUrl: avatar + }), + }); + }, ); -const emptyCardData = { +const emptyCardData: CardData = { name: O.none, job: O.none, sub: O.none, - phone: O.none, - mail: O.none, - web: O.none, - twitter: O.none, - facebook: O.none, - youtube: O.none, - instagram: O.none, - twitch: O.none, - github: O.none, - linkedIn: O.none, - xing: O.none, - paypal: O.none, - patreon: O.none, - pinterest: O.none, - npm: O.none, - soundcloud: O.none, - snapchat: O.none, - steam: O.none, - cpan: O.none, - signal: O.none, - telegram: O.none, + info: [], }; const randomSeed = memoize(RAND.randomInt(0, 200)); @@ -96,13 +96,10 @@ export const Full: Story = { args: pipe( randomSeed, IO.map(flow( - getCardData, - (data) => ({ + getStoryData, + ({ data, avatar }) => ({ data, - avatar: RemoteImageAdt.of.Loaded({ - remoteUrl: data.avatar, - objectUrl: data.avatar - }), + avatar, maximumDetailsVisible: O.none, }), )), @@ -113,8 +110,8 @@ export const NoAvatar: Story = { args: pipe( randomSeed, IO.map(flow( - getCardData, - (data) => ({ + getStoryData, + ({ data }) => ({ data: { ...emptyCardData, @@ -122,9 +119,10 @@ export const NoAvatar: Story = { job: data.job, sub: data.sub, - twitter: data.twitter, - facebook: data.facebook, - youtube: data.youtube, + info: pipe( + data.info, + A.takeLeft(3), + ) }, maximumDetailsVisible: O.some(3), }), @@ -136,13 +134,10 @@ export const WithJob: Story = { args: pipe( randomSeed, IO.map(flow( - getCardData, - (data) => ({ + getStoryData, + ({ avatar }) => ({ ...NoAvatar.args, - avatar: RemoteImageAdt.of.Loaded({ - remoteUrl: data.avatar, - objectUrl: data.avatar - }), + avatar, }), )), )() @@ -152,8 +147,8 @@ export const WithThreeMediaExpandNotVisible: Story = { args: pipe( randomSeed, IO.map(flow( - getCardData, - (data) => ({ + getStoryData, + ({ data, avatar }) => ({ data: { ...emptyCardData, @@ -161,14 +156,12 @@ export const WithThreeMediaExpandNotVisible: Story = { job: data.job, sub: data.sub, - twitter: data.twitter, - facebook: data.facebook, - youtube: data.youtube, + info: pipe( + data.info, + A.takeLeft(3), + ) }, - avatar: RemoteImageAdt.of.Loaded({ - remoteUrl: data.avatar, - objectUrl: data.avatar - }), + avatar, maximumDetailsVisible: O.some(3), }), )), @@ -178,6 +171,33 @@ export const WithThreeMediaExpandNotVisible: Story = { export const WithThreeMediaExpandVisible: Story = { args: { ...WithThreeMediaExpandNotVisible.args, + expanded: false, maximumDetailsVisible: O.some(2), }, }; + +export const WithDoubleEntries: Story = { + args: pipe( + randomSeed, + IO.map(flow( + getStoryData, + ({ data, avatar }) => ({ + data: { + ...data, + info: pipe( + data.info, + A.takeLeft(5), + xs => [ + ...xs, + ...xs + ], + A.sort(ByName) + ) + }, + + avatar, + maximumDetailsVisible: O.none, + }), + )), + )() +}; diff --git a/src/Cards/src/components/card.tsx b/src/Cards/src/components/card.tsx index 8830cfa..a287e67 100644 --- a/src/Cards/src/components/card.tsx +++ b/src/Cards/src/components/card.tsx @@ -1,9 +1,10 @@ import * as A from "fp-ts/Array"; import * as NEA from "fp-ts/NonEmptyArray"; import * as O from "fp-ts/Option"; -import * as R from "fp-ts/Record"; import * as styles from "./card.css"; +import { constant, pipe } from "fp-ts/function"; +import { DetailLine, DetailLink, DetailList } from "./detail-list"; import { ChevronDownIcon, CpanIcon, @@ -28,48 +29,19 @@ import { XingIcon, YoutubeIcon, } from "./icons"; -import { DetailLine, DetailLink, DetailList } from "./detail-list"; -import { constant, identity, pipe, tuple } from "fp-ts/function"; import type { ADTType } from "@morphic-ts/adt"; -import { AppData } from "../model/app-data"; +import { assignInlineVars } from "@vanilla-extract/dynamic"; import type { FunctionComponent } from "preact"; -import { PageContent } from "./page-content"; +import { Except } from "type-fest"; +import { AppData, AppDataInfo } from "../model/app-data"; import { RemoteImageAdt } from "../model/remote-image"; -import { Simplify } from "type-fest"; -import { assignInlineVars } from "@vanilla-extract/dynamic"; import { getUnionTypeMatcherStrict } from "../utils/utils"; +import { PageContent } from "./page-content"; + +export type CardData = Except; -type Media = Pick; - -export type CardData = Simplify< - & Pick - & Media ->; - -type CardProps = { +export type CardProps = { data: CardData; avatar: ADTType; expanded: boolean; @@ -79,7 +51,7 @@ type CardProps = { }; export const Card: FunctionComponent = ({ - data: { sub, name, job, ...media }, + data: { sub, name, job, info }, avatar, expanded, maximumDetailsVisible, @@ -105,14 +77,14 @@ export const Card: FunctionComponent = ({ )} {pipe( - { ...media, name, job }, - O.fromPredicate(R.some(O.isSome)), + [name, job, NEA.fromArray(info)], + O.fromPredicate(A.some(O.isSome)), O.fold( Empty, - (media) =>
); type DetailProps = { name: O.Option; job: O.Option; - media: Media; + info: AppData["info"]; maximumDetailsVisible: O.Option; expanded: boolean; @@ -149,7 +121,7 @@ type DetailProps = { const Details: FunctionComponent = ({ name, job, - media, + info, maximumDetailsVisible, expanded, @@ -196,51 +168,7 @@ const Details: FunctionComponent = ({ )} {pipe( - // 1. define order, that is used for display - identity>([ - // business - "phone", - "mail", - "web", - "linkedIn", - "xing", - - // social media - "twitter", - "facebook", - "youtube", - "instagram", - "pinterest", - "snapchat", - "signal", - "telegram", - - // dev - "github", - "npm", - "cpan", - - // payment - "paypal", - "patreon", - - // gaming - "twitch", - "steam", - - // media - "soundcloud", - ]), - - // 2. get the value out of details, save ordered with its key - A.map(k => pipe( - k, - k => R.lookup(k, media), - O.flatten, - O.map(v => tuple(k, v)) - )), - - A.compact, + info, splitDetails, // 3. render @@ -249,7 +177,7 @@ const Details: FunctionComponent = ({ details, A.map(([name, value]) => ( - + )) )} @@ -261,7 +189,7 @@ const Details: FunctionComponent = ({ expanded={expanded} animationIndex={idx}> - + )) )} @@ -339,7 +267,7 @@ const AnimatedDetailLine: FunctionComponent = ({ ); -const getLinkForMedium = getUnionTypeMatcherStrict()({ +const getLinkForMedium = getUnionTypeMatcherStrict()({ phone: () => (value: string) => ( ()({ }); type LinkForMediumProps = { - name: keyof Media; - caption: string; + name: AppDataInfo; + value: string; }; const LinkForMedium: FunctionComponent = ({ - name, caption -}) => getLinkForMedium(name)(caption); + name, value +}) => getLinkForMedium(name)(value); diff --git a/src/Cards/src/model/app-data.ts b/src/Cards/src/model/app-data.ts index 41d90c3..a257bb3 100644 --- a/src/Cards/src/model/app-data.ts +++ b/src/Cards/src/model/app-data.ts @@ -1,3 +1,4 @@ +import * as A from "fp-ts/Array"; import * as O from "fp-ts/Option"; import { ADTType, makeADT, ofType } from "@morphic-ts/adt"; @@ -22,33 +23,38 @@ export type AppData = { sub: O.Option; avatar: O.Option; background: O.Option; - phone: O.Option; - mail: O.Option; - web: O.Option; - twitter: O.Option; - facebook: O.Option; - youtube: O.Option; - instagram: O.Option; - twitch: O.Option; - github: O.Option; - linkedIn: O.Option; - xing: O.Option; - paypal: O.Option; - patreon: O.Option; - pinterest: O.Option; - npm: O.Option; - soundcloud: O.Option; - snapchat: O.Option; - steam: O.Option; - cpan: O.Option; - signal: O.Option; - telegram: O.Option; config: { maximumDetailsVisible: O.Option; }; + + info: Array< + | [type: 'phone', value: string] + | [type: 'mail', value: string] + | [type: 'web', value: string] + | [type: 'twitter', value: string] + | [type: 'facebook', value: string] + | [type: 'youtube', value: string] + | [type: 'instagram', value: string] + | [type: 'twitch', value: string] + | [type: 'github', value: string] + | [type: 'linkedIn', value: string] + | [type: 'xing', value: string] + | [type: 'paypal', value: string] + | [type: 'patreon', value: string] + | [type: 'pinterest', value: string] + | [type: 'npm', value: string] + | [type: 'soundcloud', value: string] + | [type: 'snapchat', value: string] + | [type: 'steam', value: string] + | [type: 'cpan', value: string] + | [type: 'signal', value: string] + | [type: 'telegram', value: string] + >; }; +export type AppDataInfo = AppData["info"][number][0]; + export const getAppDataFromUrlParams: FunctionN<[UrlParameters], AppData> = ({ avatar, background, @@ -211,3 +217,9 @@ const makeSnappyUrl = ( ) => `${import.meta.env.BASE_URL}images/wallpapers/${encodeURIComponent(name.replace(/^snappy:/, ""))}`; export const AppDataAdt = makeRemoteResultADT(); + +export const getAppDataInfo = (name: AppDataInfo) => (a: AppData) => pipe( + a.info, + A.findFirst(([key]) => key === name), + O.map(([_, value]) => value), +); diff --git a/src/Cards/src/model/v-card-url.ts b/src/Cards/src/model/v-card-url.ts index 7c5aa34..e2531ae 100644 --- a/src/Cards/src/model/v-card-url.ts +++ b/src/Cards/src/model/v-card-url.ts @@ -2,10 +2,11 @@ import * as O from "fp-ts/Option"; import * as R from "fp-ts/Record"; import * as S from "fp-ts/string"; -import { AppData, AppDataAdt } from "./app-data"; +import { AppData, AppDataAdt, getAppDataInfo } from "./app-data"; import { flow, pipe } from "fp-ts/function"; import type { Base64Data } from "./remote-image"; +import { Simplify } from "type-fest"; import { makeRemoteResultADT } from "@fun-ts/remote-result-adt"; import { pick } from "fp-ts-std/Struct"; @@ -26,9 +27,15 @@ const vCardImageEncoder = O.map( d => `PHOTO;ENCODING=BASE64;TYPE=${d.type.toUpperCase()}:${d.content}\n\n` ); -type VCardInput = - & Pick - & { avatarBase64: O.Option; }; +type VCardInput = Simplify< + & Pick + & { + phone: O.Option; + mail: O.Option; + web: O.Option; + } + & { avatarBase64: O.Option; } +>; const encodeVCardFields = (params: VCardInput): VCardFields => ({ name: pipe(params.name, vCardParamEncoder("N:")), @@ -60,7 +67,14 @@ export const VCardDataAdt = makeRemoteResultADT<{ url: string; }>(); export const vCardFieldsFromAppData = (a: AppData) => pipe( a, - pick(["name", "phone", "mail", "web", "job"]), + pick(["name", "job"]), + (x) => ({ + ...x, + phone: pipe(a, getAppDataInfo("phone")), + mail: pipe(a, getAppDataInfo("mail")), + web: pipe(a, getAppDataInfo("web")), + avatarBase64: pipe(a.avatar, O.map(a => a.url)), + }), ); export const vCardFieldsFromAppDataLoaded = flow(