From dfe3f7497037d0e5dd20a3dadb8ccb9b384005cb Mon Sep 17 00:00:00 2001 From: Zeck Li Date: Fri, 26 Nov 2021 10:50:02 +0800 Subject: [PATCH 1/7] feat(logbook): add logbook badge and entry to traveloggers page --- public/static/icons/16px/logbook.svg | 22 +++++++ public/static/icons/24px/logbook.svg | 4 ++ src/common/enums/text.ts | 3 + src/components/Avatar/index.tsx | 28 ++++++++- src/components/Avatar/styles.css | 63 +++++++++++++++++++ src/components/Context/Viewer/index.tsx | 6 ++ src/components/GQL/updates/circleFollowers.ts | 1 + src/components/Icon/IconLogbook24.tsx | 5 ++ src/components/Icon/IconLogbookBadge16.tsx | 5 ++ src/components/Icon/index.tsx | 2 + src/components/UserDigest/Mini/gql.ts | 2 + src/components/UserDigest/Rich/gql.ts | 2 + src/components/UserDigest/Verbose/gql.ts | 2 + .../UserProfile/DropdownActions/index.tsx | 28 +++++++++ src/components/UserProfile/gql.ts | 2 + src/components/UserProfile/index.tsx | 13 +++- src/definitions/type-fest.d.ts | 4 ++ src/stories/mocks/index.ts | 17 +++++ 18 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 public/static/icons/16px/logbook.svg create mode 100644 public/static/icons/24px/logbook.svg create mode 100644 src/components/Icon/IconLogbook24.tsx create mode 100644 src/components/Icon/IconLogbookBadge16.tsx diff --git a/public/static/icons/16px/logbook.svg b/public/static/icons/16px/logbook.svg new file mode 100644 index 0000000000..382b7162f8 --- /dev/null +++ b/public/static/icons/16px/logbook.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/icons/24px/logbook.svg b/public/static/icons/24px/logbook.svg new file mode 100644 index 0000000000..c08886dde3 --- /dev/null +++ b/public/static/icons/24px/logbook.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/enums/text.ts b/src/common/enums/text.ts index b30662a5dd..9888b2f55e 100644 --- a/src/common/enums/text.ts +++ b/src/common/enums/text.ts @@ -165,6 +165,7 @@ export const TEXT = { LIKER_EMAIL_EXISTS: 'Liker ID 電子信箱已被其他人使用', LIKER_NOT_FOUND: 'Liker ID 不存在', LIKER_USER_ID_EXISTS: 'Liker ID 已被其他人使用', + logbook: '航行日誌', login: '登入', loginPassword: '登入密碼', logout: '登出', @@ -494,6 +495,7 @@ export const TEXT = { LIKER_EMAIL_EXISTS: 'Liker ID 邮箱已被其他人使用', LIKER_NOT_FOUND: 'Liker ID 不存在', LIKER_USER_ID_EXISTS: 'Liker ID 已被其他人使用', + logbook: '航行日誌', login: '登录', loginPassword: '登录密码', logout: '登出', @@ -840,6 +842,7 @@ export const TEXT = { LIKER_EMAIL_EXISTS: 'Liker ID email is already used by others.', LIKER_NOT_FOUND: 'Liker ID not found', LIKER_USER_ID_EXISTS: 'Liker ID is already used by others.', + logbook: 'Logbook', login: 'Log in', loginPassword: 'Password', logout: 'Log Out', diff --git a/src/components/Avatar/index.tsx b/src/components/Avatar/index.tsx index ea6770b5fd..cca901ae51 100644 --- a/src/components/Avatar/index.tsx +++ b/src/components/Avatar/index.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import { ResponsiveImage } from '~/components' +import { IconLogbookBadge16, ResponsiveImage } from '~/components' import ICON_AVATAR_DEFAULT from '@/public/static/icons/72px/avatar-default.svg' import IMAGE_MATTERS_ARCHITECT_RING from '@/public/static/icons/architect-ring.svg' @@ -10,11 +10,14 @@ import IMAGE_CIVIC_LIKER_RING from '@/public/static/icons/civic-liker-ring.svg' import styles from './styles.css' import { AvatarUser } from './__generated__/AvatarUser' +import { AvatarUserLogbook } from './__generated__/AvatarUserLogbook' export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl' +export type AvatarLogbook = PartialDeep + export interface AvatarProps { - user?: AvatarUser + user?: AvatarUser & AvatarLogbook size?: AvatarSize src?: string | null inEditor?: boolean @@ -34,6 +37,18 @@ const fragments = { } } `, + logbook: gql` + fragment AvatarUserLogbook on User { + info { + cryptoWallet { + id + nfts { + id + } + } + } + } + `, } export const Avatar = (props: AvatarProps) => { @@ -44,10 +59,14 @@ export const Avatar = (props: AvatarProps) => { const isCivicLiker = user?.liker.civicLiker const badges = user?.info?.badges || [] const hasArchitectBadge = badges.some((b) => b.type === 'architect') + const hasLogbook = + Array.isArray(user?.info?.cryptoWallet?.nfts) && + (user?.info?.cryptoWallet?.nfts || []).length > 0 const avatarClasses = classNames({ avatar: true, [size]: true, hasRing: isCivicLiker || hasArchitectBadge, + hasBadge: hasLogbook, }) return ( @@ -60,6 +79,11 @@ export const Avatar = (props: AvatarProps) => { {isCivicLiker && } {hasArchitectBadge && } + {hasLogbook && ( +
+ +
+ )} diff --git a/src/components/Avatar/styles.css b/src/components/Avatar/styles.css index 7bb3099255..d02fc95144 100644 --- a/src/components/Avatar/styles.css +++ b/src/components/Avatar/styles.css @@ -29,6 +29,21 @@ } } +.hasBadge { + & .badge { + @mixin flex-center-all; + + position: absolute; + right: 0; + bottom: 0; + + & :global(svg) { + height: auto; + transform: translateX(50%); + } + } +} + /** * Sizes */ @@ -37,24 +52,56 @@ .xs { width: 1rem; height: 1rem; + + &.hasBadge { + & .badge { + & :global(svg) { + width: calc(1rem * 0.35); + } + } + } } /* 20px */ .sm { width: 1.25rem; height: 1.25rem; + + &.hasBadge { + & .badge { + & :global(svg) { + width: calc(1.25rem * 0.35); + } + } + } } /* 24px */ .md { width: 1.5rem; height: 1.5rem; + + &.hasBadge { + & .badge { + & :global(svg) { + width: calc(1.5rem * 0.35); + } + } + } } /* 32px */ .lg { width: 2rem; height: 2rem; + + &.hasBadge { + & .badge { + & :global(svg) { + width: calc(2rem * 0.35); + } + } + } } /* 40px */ @@ -72,6 +119,14 @@ left: -4px; } } + + &.hasBadge { + & .badge { + & :global(svg) { + width: calc(2.5rem * 0.35); + } + } + } } /* 48px */ @@ -117,4 +172,12 @@ left: -8px; } } + + &.hasBadge { + & .badge { + & :global(svg) { + transform: translateX(25%); + } + } + } } diff --git a/src/components/Context/Viewer/index.tsx b/src/components/Context/Viewer/index.tsx index b3a1eb5ff0..378f3c85e4 100644 --- a/src/components/Context/Viewer/index.tsx +++ b/src/components/Context/Viewer/index.tsx @@ -37,6 +37,12 @@ const ViewerFragments = { badges { type } + cryptoWallet { + id + nfts { + id + } + } } settings { language diff --git a/src/components/GQL/updates/circleFollowers.ts b/src/components/GQL/updates/circleFollowers.ts index 80d201f889..d914f76e01 100644 --- a/src/components/GQL/updates/circleFollowers.ts +++ b/src/components/GQL/updates/circleFollowers.ts @@ -48,6 +48,7 @@ const update = ({ info: { description: viewer.info.description, badges: viewer.info.badges, + cryptoWallet: viewer.info.cryptoWallet, __typename: 'UserInfo', }, isBlocked: false, diff --git a/src/components/Icon/IconLogbook24.tsx b/src/components/Icon/IconLogbook24.tsx new file mode 100644 index 0000000000..d4b1dfc71f --- /dev/null +++ b/src/components/Icon/IconLogbook24.tsx @@ -0,0 +1,5 @@ +import { ReactComponent as Icon } from '@/public/static/icons/24px/logbook.svg' + +import { withIcon } from './withIcon' + +export const IconLogbook24 = withIcon(Icon) diff --git a/src/components/Icon/IconLogbookBadge16.tsx b/src/components/Icon/IconLogbookBadge16.tsx new file mode 100644 index 0000000000..2c0c67d44f --- /dev/null +++ b/src/components/Icon/IconLogbookBadge16.tsx @@ -0,0 +1,5 @@ +import { ReactComponent as Icon } from '@/public/static/icons/16px/logbook.svg' + +import { withIcon } from './withIcon' + +export const IconLogbookBadge16 = withIcon(Icon) diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 6ac9476d80..9e6015db38 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -73,6 +73,8 @@ export * from './IconImage24' export * from './IconInfo16' export * from './IconIPFS24' export * from './IconLeft32' +export * from './IconLogbook24' +export * from './IconLogbookBadge16' export * from './IconLogo' export * from './IconLogoGraph' export * from './IconLogout24' diff --git a/src/components/UserDigest/Mini/gql.ts b/src/components/UserDigest/Mini/gql.ts index 5c36324311..ce9773478e 100644 --- a/src/components/UserDigest/Mini/gql.ts +++ b/src/components/UserDigest/Mini/gql.ts @@ -12,7 +12,9 @@ export const fragments = { state } ...AvatarUser + ...AvatarUserLogbook } ${Avatar.fragments.user} + ${Avatar.fragments.logbook} `, } diff --git a/src/components/UserDigest/Rich/gql.ts b/src/components/UserDigest/Rich/gql.ts index 4b0b13283b..13c32e4147 100644 --- a/src/components/UserDigest/Rich/gql.ts +++ b/src/components/UserDigest/Rich/gql.ts @@ -18,8 +18,10 @@ export const fragments = { state } ...AvatarUser + ...AvatarUserLogbook } ${Avatar.fragments.user} + ${Avatar.fragments.logbook} `, private: gql` fragment UserDigestRichUserPrivate on User { diff --git a/src/components/UserDigest/Verbose/gql.ts b/src/components/UserDigest/Verbose/gql.ts index 448a49eea2..f6bf3f9fa9 100644 --- a/src/components/UserDigest/Verbose/gql.ts +++ b/src/components/UserDigest/Verbose/gql.ts @@ -18,8 +18,10 @@ export const fragments = { state } ...AvatarUser + ...AvatarUserLogbook } ${Avatar.fragments.user} + ${Avatar.fragments.logbook} `, private: gql` fragment UserDigestVerboseUserPrivate on User { diff --git a/src/components/UserProfile/DropdownActions/index.tsx b/src/components/UserProfile/DropdownActions/index.tsx index cd48147377..1b40e4e7a5 100644 --- a/src/components/UserProfile/DropdownActions/index.tsx +++ b/src/components/UserProfile/DropdownActions/index.tsx @@ -7,6 +7,7 @@ import { Button, DropdownDialog, IconEdit16, + IconLogbook24, IconMore32, IconSettings32, LanguageContext, @@ -35,6 +36,7 @@ interface DialogProps { interface Controls { hasEditProfile: boolean hasBlockUser: boolean + hasLogbook: boolean } type BaseDropdownActionsProps = DropdownActionsProps & DialogProps & Controls @@ -44,6 +46,14 @@ const fragments = { public: gql` fragment DropdownActionsUserPublic on User { id + info { + cryptoWallet { + id + nfts { + id + } + } + } ...BlockUserPublic ...EditProfileDialogUserPublic } @@ -67,11 +77,15 @@ const BaseDropdownActions = ({ hasEditProfile, hasBlockUser, + hasLogbook, openEditProfileDialog, openBlockUserDialog, }: BaseDropdownActionsProps) => { const { lang } = useContext(LanguageContext) + const logbookUrl = `https://traveloggers.matters.news${ + lang === 'en' ? '/' : '/zh/' + }logbooks` const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( @@ -83,6 +97,14 @@ const BaseDropdownActions = ({ )} + {hasLogbook && ( + + } size="md" spacing="base"> + + + + )} + {hasBlockUser && ( )} @@ -120,9 +142,15 @@ const BaseDropdownActions = ({ } const DropdownActions = ({ user, isMe }: DropdownActionsProps) => { + const { + info: { cryptoWallet }, + } = user const controls = { hasEditProfile: isMe, hasBlockUser: !isMe, + hasLogbook: + Array.isArray(cryptoWallet?.nfts) && + (cryptoWallet?.nfts || []).length > 0, } if (_isEmpty(_pickBy(controls))) { diff --git a/src/components/UserProfile/gql.ts b/src/components/UserProfile/gql.ts index 930ff21da7..88f5a91f81 100644 --- a/src/components/UserProfile/gql.ts +++ b/src/components/UserProfile/gql.ts @@ -37,9 +37,11 @@ const fragments = { ...DigestRichCirclePublic } ...AvatarUser + ...AvatarUserLogbook ...DropdownActionsUserPublic } ${Avatar.fragments.user} + ${Avatar.fragments.logbook} ${CircleDigest.Rich.fragments.circle.public} ${DropdownActions.fragments.user.public} `, diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index 8e82689d86..55f11f0bae 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -6,6 +6,7 @@ import { Error, Expandable, FollowUserButton, + LanguageContext, Layout, Spinner, Throw404, @@ -40,6 +41,7 @@ import { UserProfileUserPublic } from './__generated__/UserProfileUserPublic' export const UserProfile = () => { const { getQuery } = useRoute() const viewer = useContext(ViewerContext) + const { lang } = useContext(LanguageContext) // public data const userName = getQuery('name') @@ -141,6 +143,9 @@ export const UserProfile = () => { const isUserArchived = userState === 'archived' const isUserBanned = userState === 'banned' const isUserInactive = isUserArchived || isUserBanned + const logbookUrl = `https://traveloggers.matters.news${ + lang === 'en' ? '/' : '/zh/' + }logbooks` /** * Inactive User @@ -186,7 +191,13 @@ export const UserProfile = () => {
- + {hasTraveloggersBadge ? ( + + + + ) : ( + + )}
{!isMe && } diff --git a/src/definitions/type-fest.d.ts b/src/definitions/type-fest.d.ts index 16df62dcf8..9b2e65cb60 100644 --- a/src/definitions/type-fest.d.ts +++ b/src/definitions/type-fest.d.ts @@ -20,3 +20,7 @@ type SetRequired = Required> extends infer InferredType // If `InferredType` extends the previous, then for each key, use the inferred type key. ? { [KeyType in keyof InferredType]: InferredType[KeyType] } : never + +type PartialDeep = { + [P in keyof T]?: PartialDeep +} diff --git a/src/stories/mocks/index.ts b/src/stories/mocks/index.ts index 9ec288f962..4b3dca3abb 100644 --- a/src/stories/mocks/index.ts +++ b/src/stories/mocks/index.ts @@ -13,6 +13,17 @@ export const MOCK_USER = { __typename: 'UserInfo' as any, badges: null, description: 'Matters 唯一官方帳號', + cryptoWallet: { + __typename: 'CryptoWallet' as any, + id: 'crypto-wallet-0000', + address: '0x0x0x0x0x0x0x0x0x0x0x', + nfts: [ + { + __typename: 'NFTAsset' as any, + id: '1', + }, + ], + }, }, liker: { __typename: 'Liker' as any, @@ -168,4 +179,10 @@ export const MOCK_CRYPTO_WALLET = { __typename: 'CryptoWallet' as any, id: 'crypto-wallet-0000', address: '0x0x0x0x0x0x0x0x0x0x0x', + nfts: [ + { + __typename: 'NFTAsset' as any, + id: '1', + }, + ], } From 46095e8c9c87bc5a592489fe97a6f2d966eac03e Mon Sep 17 00:00:00 2001 From: Zeck Li Date: Fri, 3 Dec 2021 17:51:53 +0800 Subject: [PATCH 2/7] feat(logbook): revise logbook icon and add redirect url --- .env.dev | 1 + .env.local.example | 1 + .env.prod | 1 + .env.stage | 1 + public/static/icons/16px/logbook.svg | 30 +++++----------- public/static/images/logbook.gif | Bin 0 -> 25832 bytes src/components/Avatar/index.tsx | 7 ++-- src/components/Avatar/styles.css | 33 +++++++++++++++--- src/components/Context/Viewer/index.tsx | 1 + .../UserProfile/DropdownActions/index.tsx | 14 ++++---- src/components/UserProfile/index.tsx | 23 ++++++++---- 11 files changed, 70 insertions(+), 42 deletions(-) create mode 100644 public/static/images/logbook.gif diff --git a/.env.dev b/.env.dev index 3f2904e6b5..d4a4fd202f 100644 --- a/.env.dev +++ b/.env.dev @@ -21,3 +21,4 @@ NEXT_PUBLIC_RECAPTCHA_KEY=6Lfn4fsUAAAAACKsig4Mr54UP0Vn4ombv4zmOWJk NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_51GrOGRCE0HD6LY9Uo7rK3pSmiIG4KTjWO1rOrJavFFJob3zQlSjZg6uavIhcGbp8o1wZGcEuph2MgUfHhsB48vx000z2S0FiUx NEXT_PUBLIC_BUILD_TYPE=dynamic NEXT_PUBLIC_PROGRAMMABLE_SEARCH_ENGINE_ID=004538121411474993797:xkl3sdy-9su +NEXT_PUBLIC_TRAVELOGGERS_URL=https://nft-develop.matters.news diff --git a/.env.local.example b/.env.local.example index cf5a3a8c89..bfd92d924c 100644 --- a/.env.local.example +++ b/.env.local.example @@ -20,3 +20,4 @@ NEXT_PUBLIC_RECAPTCHA_KEY=6Lfn4fsUAAAAACKsig4Mr54UP0Vn4ombv4zmOWJk NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_51GrOGRCE0HD6LY9Uo7rK3pSmiIG4KTjWO1rOrJavFFJob3zQlSjZg6uavIhcGbp8o1wZGcEuph2MgUfHhsB48vx000z2S0FiUx NEXT_PUBLIC_BUILD_TYPE=dynamic NEXT_PUBLIC_PROGRAMMABLE_SEARCH_ENGINE_ID=004538121411474993797:xkl3sdy-9su +NEXT_PUBLIC_TRAVELOGGERS_URL=https://nft-develop.matters.news diff --git a/.env.prod b/.env.prod index c15ba2ab60..d674fe9df3 100644 --- a/.env.prod +++ b/.env.prod @@ -21,3 +21,4 @@ NEXT_PUBLIC_RECAPTCHA_KEY=6LfZ6uAUAAAAAAQ6m0I2hY62sRQgNQz2J9ba-5Ys NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_live_NfdCmUIPzQxKEMjLkInsjULK00W7AXBBrG NEXT_PUBLIC_BUILD_TYPE=dynamic NEXT_PUBLIC_PROGRAMMABLE_SEARCH_ENGINE_ID=004538121411474993797:xkl3sdy-9su +NEXT_PUBLIC_TRAVELOGGERS_URL=https://traveloggers.matters.news diff --git a/.env.stage b/.env.stage index 8abd377eab..3f1999052c 100644 --- a/.env.stage +++ b/.env.stage @@ -21,3 +21,4 @@ NEXT_PUBLIC_RECAPTCHA_KEY=6Lfn4fsUAAAAACKsig4Mr54UP0Vn4ombv4zmOWJk NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_51ITI0rKY3RzN3G1SN3FGU2jdw7Przd8qpdlMXlw1Vx796tqp7zdS4in6Yql7xgs08fCO6x0bZR1QMtZVaoM5uI7S00Hwo1ruoN NEXT_PUBLIC_BUILD_TYPE=dynamic NEXT_PUBLIC_PROGRAMMABLE_SEARCH_ENGINE_ID=004538121411474993797:xkl3sdy-9su +NEXT_PUBLIC_TRAVELOGGERS_URL=https://nft-develop.matters.news diff --git a/public/static/icons/16px/logbook.svg b/public/static/icons/16px/logbook.svg index 382b7162f8..8f4a2c5330 100644 --- a/public/static/icons/16px/logbook.svg +++ b/public/static/icons/16px/logbook.svg @@ -1,22 +1,10 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/public/static/images/logbook.gif b/public/static/images/logbook.gif new file mode 100644 index 0000000000000000000000000000000000000000..f0ec83dc6fa28a97dd308492a15daf56abf0ab79 GIT binary patch literal 25832 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 CEo0*V literal 0 HcmV?d00001 diff --git a/src/components/Avatar/index.tsx b/src/components/Avatar/index.tsx index cca901ae51..7e81b4eafa 100644 --- a/src/components/Avatar/index.tsx +++ b/src/components/Avatar/index.tsx @@ -6,6 +6,7 @@ import { IconLogbookBadge16, ResponsiveImage } from '~/components' import ICON_AVATAR_DEFAULT from '@/public/static/icons/72px/avatar-default.svg' import IMAGE_MATTERS_ARCHITECT_RING from '@/public/static/icons/architect-ring.svg' import IMAGE_CIVIC_LIKER_RING from '@/public/static/icons/civic-liker-ring.svg' +import LOGBOOK from '@/public/static/images/logbook.gif' import styles from './styles.css' @@ -21,6 +22,7 @@ export interface AvatarProps { size?: AvatarSize src?: string | null inEditor?: boolean + inProfile?: boolean } const fragments = { @@ -42,6 +44,7 @@ const fragments = { info { cryptoWallet { id + address nfts { id } @@ -52,7 +55,7 @@ const fragments = { } export const Avatar = (props: AvatarProps) => { - const { user, size = 'default', src, inEditor } = props + const { user, size = 'default', src, inEditor, inProfile } = props const source = src || user?.avatar || ICON_AVATAR_DEFAULT const isFallback = (!src && !user?.avatar) || source.indexOf('data:image') >= 0 @@ -81,7 +84,7 @@ export const Avatar = (props: AvatarProps) => { {hasArchitectBadge && } {hasLogbook && (
- + {inProfile ? : }
)} diff --git a/src/components/Avatar/styles.css b/src/components/Avatar/styles.css index d02fc95144..8bc595786d 100644 --- a/src/components/Avatar/styles.css +++ b/src/components/Avatar/styles.css @@ -6,7 +6,7 @@ flex-shrink: 0; border-radius: 50%; - & :global(img) { + & :global(> img) { @mixin object-fit-cover; background-color: var(--color-grey-lighter); @@ -37,6 +37,12 @@ right: 0; bottom: 0; + & :global(img) { + width: var(--spacing-base); + height: auto; + transform: translateX(25%); + } + & :global(svg) { height: auto; transform: translateX(50%); @@ -56,7 +62,7 @@ &.hasBadge { & .badge { & :global(svg) { - width: calc(1rem * 0.35); + width: calc(1rem * 0.4); } } } @@ -70,7 +76,7 @@ &.hasBadge { & .badge { & :global(svg) { - width: calc(1.25rem * 0.35); + width: calc(1.25rem * 0.4); } } } @@ -98,7 +104,7 @@ &.hasBadge { & .badge { & :global(svg) { - width: calc(2rem * 0.35); + width: calc(2rem * 0.3); } } } @@ -123,7 +129,7 @@ &.hasBadge { & .badge { & :global(svg) { - width: calc(2.5rem * 0.35); + width: calc(2.5rem * 0.3); } } } @@ -142,6 +148,15 @@ left: -5px; } } + + &.hasBadge { + & .badge { + & :global(svg) { + width: calc(3rem * 0.3); + transform: translateX(20%); + } + } + } } /* 56px */ @@ -157,6 +172,14 @@ left: -6px; } } + + &.hasBadge { + & .badge { + & :global(svg) { + transform: translateX(25%); + } + } + } } /* 72px */ diff --git a/src/components/Context/Viewer/index.tsx b/src/components/Context/Viewer/index.tsx index 378f3c85e4..e0b5408281 100644 --- a/src/components/Context/Viewer/index.tsx +++ b/src/components/Context/Viewer/index.tsx @@ -39,6 +39,7 @@ const ViewerFragments = { } cryptoWallet { id + address nfts { id } diff --git a/src/components/UserProfile/DropdownActions/index.tsx b/src/components/UserProfile/DropdownActions/index.tsx index 1b40e4e7a5..9a881076b2 100644 --- a/src/components/UserProfile/DropdownActions/index.tsx +++ b/src/components/UserProfile/DropdownActions/index.tsx @@ -49,6 +49,7 @@ const fragments = { info { cryptoWallet { id + address nfts { id } @@ -83,9 +84,9 @@ const BaseDropdownActions = ({ openBlockUserDialog, }: BaseDropdownActionsProps) => { const { lang } = useContext(LanguageContext) - const logbookUrl = `https://traveloggers.matters.news${ - lang === 'en' ? '/' : '/zh/' - }logbooks` + const logbookUrl = `${process.env.NEXT_PUBLIC_TRAVELOGGERS_URL}${ + lang === 'en' ? '/' : 'zh/' + }owner/${user.info.cryptoWallet?.address}` const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( @@ -142,15 +143,12 @@ const BaseDropdownActions = ({ } const DropdownActions = ({ user, isMe }: DropdownActionsProps) => { - const { - info: { cryptoWallet }, - } = user const controls = { hasEditProfile: isMe, hasBlockUser: !isMe, hasLogbook: - Array.isArray(cryptoWallet?.nfts) && - (cryptoWallet?.nfts || []).length > 0, + Array.isArray(user.info.cryptoWallet?.nfts) && + (user.info.cryptoWallet?.nfts || []).length > 0, } if (_isEmpty(_pickBy(controls))) { diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index 55f11f0bae..db6656fe54 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -10,6 +10,7 @@ import { Layout, Spinner, Throw404, + Tooltip, Translate, usePublicQuery, useRoute, @@ -143,9 +144,9 @@ export const UserProfile = () => { const isUserArchived = userState === 'archived' const isUserBanned = userState === 'banned' const isUserInactive = isUserArchived || isUserBanned - const logbookUrl = `https://traveloggers.matters.news${ + const logbookUrl = `${process.env.NEXT_PUBLIC_TRAVELOGGERS_URL}${ lang === 'en' ? '/' : '/zh/' - }logbooks` + }owner/${user.info.cryptoWallet?.address}` /** * Inactive User @@ -192,11 +193,21 @@ export const UserProfile = () => {
{hasTraveloggersBadge ? ( - - - + + } + > + + + + ) : ( - + )}
From cecdb41d4e6453e07b676297573b673a5829abdc Mon Sep 17 00:00:00 2001 From: Zeck Li Date: Mon, 6 Dec 2021 11:23:39 +0800 Subject: [PATCH 3/7] fix(avatar): fix styles of avatar --- src/components/Avatar/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar/styles.css b/src/components/Avatar/styles.css index 8bc595786d..9424434bc2 100644 --- a/src/components/Avatar/styles.css +++ b/src/components/Avatar/styles.css @@ -6,7 +6,7 @@ flex-shrink: 0; border-radius: 50%; - & :global(> img) { + & :global(picture > img) { @mixin object-fit-cover; background-color: var(--color-grey-lighter); From 0dd091aa50a6cc0d45e3c07aa9830908bb650790 Mon Sep 17 00:00:00 2001 From: Zeck Li Date: Mon, 6 Dec 2021 21:45:18 +0800 Subject: [PATCH 4/7] fix(avatar): fix styles of avatar --- src/components/Avatar/index.tsx | 8 ++++++-- src/components/Avatar/styles.css | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Avatar/index.tsx b/src/components/Avatar/index.tsx index 7e81b4eafa..1f0eeb25a0 100644 --- a/src/components/Avatar/index.tsx +++ b/src/components/Avatar/index.tsx @@ -83,8 +83,12 @@ export const Avatar = (props: AvatarProps) => { {isCivicLiker && } {hasArchitectBadge && } {hasLogbook && ( -
- {inProfile ? : } +
+ {inProfile ? ( + + ) : ( + + )}
)} diff --git a/src/components/Avatar/styles.css b/src/components/Avatar/styles.css index 9424434bc2..20cc347059 100644 --- a/src/components/Avatar/styles.css +++ b/src/components/Avatar/styles.css @@ -6,7 +6,7 @@ flex-shrink: 0; border-radius: 50%; - & :global(picture > img) { + & :global(img:not(.logbook)) { @mixin object-fit-cover; background-color: var(--color-grey-lighter); From 2c529b1bcb6101ae0960d930a114bc7583df09e2 Mon Sep 17 00:00:00 2001 From: tx0c <> Date: Thu, 2 Dec 2021 01:19:48 +0000 Subject: [PATCH 5/7] set traveloggers avatar in user settings - resolves #2295 - resolves #2306 --- src/common/enums/csp.ts | 10 +++ src/common/enums/text.ts | 5 +- .../EditProfileDialog/Content.tsx | 80 ++++++++++++++++++- .../DropdownActions/EditProfileDialog/gql.ts | 2 + .../EditProfileDialog/index.tsx | 3 +- .../EditProfileDialog/styles.css | 49 ++++++++++++ .../UserProfile/DropdownActions/index.tsx | 3 +- 7 files changed, 147 insertions(+), 5 deletions(-) diff --git a/src/common/enums/csp.ts b/src/common/enums/csp.ts index 13941c0599..96c0fd05eb 100644 --- a/src/common/enums/csp.ts +++ b/src/common/enums/csp.ts @@ -32,6 +32,10 @@ const STYLE_SRC = [ 'www.google.com/cse/', ].join(' ') +/* const { hostname: NEXT_PUBLIC_API_HOSTNAME } = new URL( + process.env.NEXT_PUBLIC_API_URL as string +) */ + const IMG_SRC = [ "'self'", @@ -39,6 +43,10 @@ const IMG_SRC = [ 'data:', process.env.NEXT_PUBLIC_ASSET_DOMAIN, + // 'server-develop.matters.news', + // NEXT_PUBLIC_API_HOSTNAME as string, + new URL(process.env.NEXT_PUBLIC_API_URL as string).hostname, + // for some old articles were using this s3 urls directly 'matters-server-production.s3-ap-southeast-1.amazonaws.com', @@ -55,6 +63,8 @@ const CONNECT_SRC = [ process.env.NEXT_PUBLIC_API_URL, process.env.NEXT_PUBLIC_OAUTH_API_URL, + // 'server-develop.matters.news', + // Sentry 'sentry.matters.one', diff --git a/src/common/enums/text.ts b/src/common/enums/text.ts index 9888b2f55e..3e59b1e9fb 100644 --- a/src/common/enums/text.ts +++ b/src/common/enums/text.ts @@ -166,6 +166,7 @@ export const TEXT = { LIKER_NOT_FOUND: 'Liker ID 不存在', LIKER_USER_ID_EXISTS: 'Liker ID 已被其他人使用', logbook: '航行日誌', + myNFTCollections: '我的 NFT 收藏', login: '登入', loginPassword: '登入密碼', logout: '登出', @@ -495,7 +496,8 @@ export const TEXT = { LIKER_EMAIL_EXISTS: 'Liker ID 邮箱已被其他人使用', LIKER_NOT_FOUND: 'Liker ID 不存在', LIKER_USER_ID_EXISTS: 'Liker ID 已被其他人使用', - logbook: '航行日誌', + logbook: '航行日志', + myNFTCollections: '我的 NFT 收藏', login: '登录', loginPassword: '登录密码', logout: '登出', @@ -843,6 +845,7 @@ export const TEXT = { LIKER_NOT_FOUND: 'Liker ID not found', LIKER_USER_ID_EXISTS: 'Liker ID is already used by others.', logbook: 'Logbook', + myNFTCollections: 'My NFT Collections', login: 'Log in', loginPassword: 'Password', logout: 'Log Out', diff --git a/src/components/UserProfile/DropdownActions/EditProfileDialog/Content.tsx b/src/components/UserProfile/DropdownActions/EditProfileDialog/Content.tsx index 56392629fa..0ca3968cc3 100644 --- a/src/components/UserProfile/DropdownActions/EditProfileDialog/Content.tsx +++ b/src/components/UserProfile/DropdownActions/EditProfileDialog/Content.tsx @@ -1,13 +1,14 @@ import { useFormik } from 'formik' import gql from 'graphql-tag' import _pickBy from 'lodash/pickBy' -import { useContext } from 'react' +import React, { useContext, useState } from 'react' import { AvatarUploader, CoverUploader, Dialog, Form, + IconChecked, LanguageContext, Translate, useMutation, @@ -25,11 +26,16 @@ import IMAGE_COVER from '@/public/static/images/profile-cover.png' import styles from './styles.css' +import { + EditProfileDialogUserPrivate, + EditProfileDialogUserPrivate_info_cryptoWallet_nfts, +} from './__generated__/EditProfileDialogUserPrivate' import { EditProfileDialogUserPublic } from './__generated__/EditProfileDialogUserPublic' import { UpdateUserInfoProfile } from './__generated__/UpdateUserInfoProfile' interface FormProps { - user: EditProfileDialogUserPublic + user: EditProfileDialogUserPublic & Partial + // user: DropdownActionsUserPublic & Partial closeDialog: () => void } @@ -59,6 +65,35 @@ const UPDATE_USER_INFO = gql` */ const UNCHANGED_FIELD = 'UNCHANGED_FIELD' +const TravlogNFTImage = ({ + index, + selectedIndex, + nft: { id, name, description, imagePreviewUrl }, + onClick, +}: { + index: number + selectedIndex: number + nft: EditProfileDialogUserPrivate_info_cryptoWallet_nfts + onClick: (event?: React.MouseEvent) => any +}) => { + return ( + + {name} + {index === selectedIndex && ( +
+ +
+ )} + +
+ ) +} + const EditProfileDialogContent: React.FC = ({ user, closeDialog, @@ -139,6 +174,11 @@ const EditProfileDialogContent: React.FC = ({ }, }) + const [selectedNFTIndex, setSelectedNFTIndex] = useState(-1) + + const nfts = user.info.cryptoWallet + ?.nfts as EditProfileDialogUserPrivate_info_cryptoWallet_nfts[] + const InnerForm = (
@@ -160,6 +200,42 @@ const EditProfileDialogContent: React.FC = ({ />
+ {Array.isArray(nfts) && nfts.length > 0 && ( +
+
+ +
+
+ {nfts.map( + ( + nftProps: EditProfileDialogUserPrivate_info_cryptoWallet_nfts, + index: number + ) => ( + { + setSelectedNFTIndex(index) + setFieldValue('avatar', nftProps.imagePreviewUrl) + }} + /> + ) + /* */ + )} + {/*
{JSON.stringify(user.info.cryptoWallet?.nfts)}
*/} +
+
點擊圖片替換頭像
+
+ )} + } type="text" diff --git a/src/components/UserProfile/DropdownActions/EditProfileDialog/gql.ts b/src/components/UserProfile/DropdownActions/EditProfileDialog/gql.ts index 1763078a97..3e4456eba9 100644 --- a/src/components/UserProfile/DropdownActions/EditProfileDialog/gql.ts +++ b/src/components/UserProfile/DropdownActions/EditProfileDialog/gql.ts @@ -35,6 +35,8 @@ export const fragments = { address nfts { id + imageUrl + imagePreviewUrl name description } diff --git a/src/components/UserProfile/DropdownActions/EditProfileDialog/index.tsx b/src/components/UserProfile/DropdownActions/EditProfileDialog/index.tsx index a9d6cf1145..657728b39b 100644 --- a/src/components/UserProfile/DropdownActions/EditProfileDialog/index.tsx +++ b/src/components/UserProfile/DropdownActions/EditProfileDialog/index.tsx @@ -4,10 +4,11 @@ import { Dialog, Spinner, useDialogSwitch } from '~/components' import { fragments } from './gql' +import { EditProfileDialogUserPrivate } from './__generated__/EditProfileDialogUserPrivate' import { EditProfileDialogUserPublic } from './__generated__/EditProfileDialogUserPublic' interface EditProfileDialogProps { - user: EditProfileDialogUserPublic + user: EditProfileDialogUserPublic & Partial children: ({ openDialog }: { openDialog: () => void }) => React.ReactNode } diff --git a/src/components/UserProfile/DropdownActions/EditProfileDialog/styles.css b/src/components/UserProfile/DropdownActions/EditProfileDialog/styles.css index cffeff9df4..0691c7ddba 100644 --- a/src/components/UserProfile/DropdownActions/EditProfileDialog/styles.css +++ b/src/components/UserProfile/DropdownActions/EditProfileDialog/styles.css @@ -2,3 +2,52 @@ padding-left: var(--spacing-base); margin-top: calc(var(--spacing-xx-loose) * -1); } + +.nft-collection { + margin: var(--spacing-loose) var(--spacing-base); + + & section { + padding: var(--spacing-xx-tight); + + /* font-size: 1rem; */ + background-color: var(--color-grey-lighter); + border-radius: 5px; + + &:focus { + background-color: var(--color-green-lighter); + border-bottom-color: var(--color-matters-green); + } + + & span { + position: relative; + margin: 0 2px; + + & img { + width: 72px; + + &:hover { + border: 1px solid grey; + border-radius: 5px; + } + } + + & div.checked { + position: absolute; + right: 0; + bottom: 0; + } + } + } + + & header { + margin: var(--spacing-base) 0 var(--spacing-x-tight); + font-size: var(--font-size-sm); + color: var(--color-grey-dark); + } + + & footer { + margin-top: var(--spacing-x-tight); + font-size: var(--font-size-xs); + color: var(--color-grey); + } +} diff --git a/src/components/UserProfile/DropdownActions/index.tsx b/src/components/UserProfile/DropdownActions/index.tsx index 9a881076b2..8acebc5269 100644 --- a/src/components/UserProfile/DropdownActions/index.tsx +++ b/src/components/UserProfile/DropdownActions/index.tsx @@ -21,10 +21,11 @@ import { translate } from '~/common/utils' import { EditProfileDialog } from './EditProfileDialog' +import { DropdownActionsUserPrivate } from './__generated__/DropdownActionsUserPrivate' import { DropdownActionsUserPublic } from './__generated__/DropdownActionsUserPublic' interface DropdownActionsProps { - user: DropdownActionsUserPublic + user: DropdownActionsUserPublic & Partial isMe: boolean } From 76bef7def6c5f4f23897243d3b192b6e55b734d3 Mon Sep 17 00:00:00 2001 From: Zeck Li Date: Tue, 7 Dec 2021 14:00:12 +0800 Subject: [PATCH 6/7] fix(logbook): correct logbook url pttern --- src/components/UserProfile/DropdownActions/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UserProfile/DropdownActions/index.tsx b/src/components/UserProfile/DropdownActions/index.tsx index 8acebc5269..505b234e9c 100644 --- a/src/components/UserProfile/DropdownActions/index.tsx +++ b/src/components/UserProfile/DropdownActions/index.tsx @@ -86,7 +86,7 @@ const BaseDropdownActions = ({ }: BaseDropdownActionsProps) => { const { lang } = useContext(LanguageContext) const logbookUrl = `${process.env.NEXT_PUBLIC_TRAVELOGGERS_URL}${ - lang === 'en' ? '/' : 'zh/' + lang === 'en' ? '/' : '/zh/' }owner/${user.info.cryptoWallet?.address}` const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( From 7d1c76083c9c1768541a4bbbdbf3adb9df4d01c1 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 9 Dec 2021 15:56:43 +0800 Subject: [PATCH 7/7] chore(release): v3.36.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4a93f89a0..421fb58ee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matters-web", - "version": "3.34.0", + "version": "3.36.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "matters-web", - "version": "3.34.0", + "version": "3.36.0", "license": "Apache-2.0", "dependencies": { "@apollo/react-common": "^3.1.3", diff --git a/package.json b/package.json index 1ab04d0db1..3545a0ef34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "3.35.0", + "version": "3.36.0", "description": "codebase of Matters' website", "sideEffects": false, "author": "Matters ",