diff --git a/lang/default.json b/lang/default.json index 784c113eba..ec07a834b3 100644 --- a/lang/default.json +++ b/lang/default.json @@ -944,6 +944,9 @@ "defaultMessage": "Verification successful", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "Select Activity..." + }, "DqQvtL": { "defaultMessage": "Unblock", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "guide", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "Please select a date of activity" + }, "P3y9Bo": { "defaultMessage": "Go to sign", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "More", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "Please select..." - }, "VrOoVf": { "defaultMessage": "Matters will never ask your wallet key through any channel.", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "Restricted content" }, + "d5bM8A": { + "defaultMessage": "Select Date..." + }, "dAPUJp": { "defaultMessage": "The dazzling light of a meteor shower is enough to illuminate the night sky. The Meteor Canoe badge signifies your participation in the Nomad Matters.", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" diff --git a/lang/en.json b/lang/en.json index 2fcbe0300c..828e86a159 100644 --- a/lang/en.json +++ b/lang/en.json @@ -529,7 +529,7 @@ "defaultMessage": "Upload file" }, "6pc948": { - "defaultMessage": "Add to FreeWrite" + "defaultMessage": "Add to Free Write" }, "6q0G5e": { "defaultMessage": "Successfully added", @@ -944,6 +944,9 @@ "defaultMessage": "Verification successful", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "Select Activity..." + }, "DqQvtL": { "defaultMessage": "Unblock", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "guide", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "Please select a date of activity" + }, "P3y9Bo": { "defaultMessage": "Go to sign", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "More", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "Please select..." - }, "VrOoVf": { "defaultMessage": "Matters will never ask your wallet key through any channel.", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "Restricted content" }, + "d5bM8A": { + "defaultMessage": "Select Date..." + }, "dAPUJp": { "defaultMessage": "The dazzling light of a meteor shower is enough to illuminate the night sky. The Meteor Canoe badge signifies your participation in the Nomad Matters.", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 82a99d37af..011fb4c6cf 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -529,7 +529,7 @@ "defaultMessage": "上传档案" }, "6pc948": { - "defaultMessage": "投稿七日书自由写" + "defaultMessage": "参与活动" }, "6q0G5e": { "defaultMessage": "加入成功", @@ -944,6 +944,9 @@ "defaultMessage": "验证成功", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "选择活动..." + }, "DqQvtL": { "defaultMessage": "解除屏蔽", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "教学指南", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "请选定参与活动的投稿日程" + }, "P3y9Bo": { "defaultMessage": "前往签署", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "相关推荐", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "请选择⋯" - }, "VrOoVf": { "defaultMessage": "Matters 不会透过任何渠道主动询问你的钱包私钥。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "限制级内容" }, + "d5bM8A": { + "defaultMessage": "投稿日程⋯" + }, "dAPUJp": { "defaultMessage": "流星雨的绚烂光芒足以点亮夜空。流星号徽章纪念你曾参与「游牧者计划」。", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" @@ -2748,7 +2754,7 @@ "description": "src/components/Forms/CreateCircleForm/Profile.tsx" }, "m1wKuC": { - "defaultMessage": "参与七日书活动" + "defaultMessage": "参与活动" }, "m4GG4b": { "defaultMessage": "删除选集" diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index 4d31476398..3fc2c247c0 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -529,7 +529,7 @@ "defaultMessage": "上傳檔案" }, "6pc948": { - "defaultMessage": "投稿七日書自由寫" + "defaultMessage": "參與活動" }, "6q0G5e": { "defaultMessage": "加入成功", @@ -944,6 +944,9 @@ "defaultMessage": "驗證成功", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "選擇活動⋯" + }, "DqQvtL": { "defaultMessage": "解除封鎖", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "教學指南", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "請選定參與活動的投稿日程" + }, "P3y9Bo": { "defaultMessage": "前往簽署", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "相關推薦", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "請選擇⋯" - }, "VrOoVf": { "defaultMessage": "Matters 不會透過任何渠道主動詢問你的錢包私鑰。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "限制級內容" }, + "d5bM8A": { + "defaultMessage": "投稿日程⋯" + }, "dAPUJp": { "defaultMessage": "流星雨的絢爛光芒足以點亮夜空。流星號徽章紀念你曾參與「遊牧者計畫」。", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" @@ -2748,7 +2754,7 @@ "description": "src/components/Forms/CreateCircleForm/Profile.tsx" }, "m1wKuC": { - "defaultMessage": "參與七日書活動" + "defaultMessage": "參與活動" }, "m4GG4b": { "defaultMessage": "刪除選集" diff --git a/package.json b/package.json index d0c625f838..ecf206f0ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "5.7.0", + "version": "5.7.1", "description": "codebase of Matters' website", "author": "Matters ", "engines": { @@ -32,7 +32,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "prepare": "husky install", - "vercel-build": "set -xe; npm run gen:type && if [[ \"$NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF\" =~ release/* ]] ; then cp -va .env.prod .env.local ; echo 'NEXT_PUBLIC_SITE_DOMAIN=web-next.matters.town' | tee -a .env.local; else cp -va .env.dev .env.local ; echo 'NEXT_PUBLIC_SITE_DOMAIN=web-dev.matters.town' | tee -a .env.local ; fi && { echo 'NEXT_PUBLIC_NEXT_ASSET_DOMAIN='; echo 'NEXT_PUBLIC_ADMIN_VIEW=true'; } | tee -a .env.local && npm run build", + "vercel-build": "set -xe; if [[ \"$NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF\" =~ release/* ]] ; then npm run gen:type:prod && cp -va .env.prod .env.local ; echo 'NEXT_PUBLIC_SITE_DOMAIN=web-next.matters.town' | tee -a .env.local; else npm run gen:type && cp -va .env.dev .env.local ; echo 'NEXT_PUBLIC_SITE_DOMAIN=web-dev.matters.town' | tee -a .env.local ; fi && { echo 'NEXT_PUBLIC_NEXT_ASSET_DOMAIN='; echo 'NEXT_PUBLIC_ADMIN_VIEW=true'; } | tee -a .env.local && npm run build", "i18n:extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --id-interpolation-pattern '[sha512:contenthash:base64:6]' --out-file lang/default.json", "i18n:generate": "node bin/i18nGenerate.js", "i18n:compile": "formatjs compile-folder --ast lang compiled-lang", diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index e55ae0d234..d815ab0930 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -94,6 +94,7 @@ type ToPathArgs = campaign: CampaignArgs stage?: CampaignStageArgs featured?: boolean + announcement?: boolean } | { page: 'userProfile' | 'userCollections' @@ -247,7 +248,7 @@ export const toPath = ( href = `${href}?type=${args.stage.id}` } else if (args.featured) { href = `${href}?type=featured` - } else { + } else if (args.announcement) { href = `${href}?type=announcement` } break diff --git a/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx b/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx index 7fc405d49e..4ee68a118a 100644 --- a/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx +++ b/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx @@ -20,7 +20,8 @@ const BaseMobileSettingsDialog = ({ children, canComment, toggleComment, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, indented, @@ -62,7 +63,7 @@ const BaseMobileSettingsDialog = ({ {/* campaign */} - {appliedCampaign && editCampaign && ( + {campaigns && campaigns.length > 0 && editCampaign && (

diff --git a/src/components/Editor/BottomBar/index.tsx b/src/components/Editor/BottomBar/index.tsx index 2d1e24f0f5..7f3b7ec1bb 100644 --- a/src/components/Editor/BottomBar/index.tsx +++ b/src/components/Editor/BottomBar/index.tsx @@ -92,7 +92,8 @@ const BottomBar: React.FC = ({ canComment, toggleComment, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, @@ -148,7 +149,8 @@ const BottomBar: React.FC = ({ toggleComment, disableChangeCanComment: article?.canComment, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, diff --git a/src/components/Editor/SelectCampaign/index.tsx b/src/components/Editor/SelectCampaign/index.tsx index 32f8ada3fb..0761f901ca 100644 --- a/src/components/Editor/SelectCampaign/index.tsx +++ b/src/components/Editor/SelectCampaign/index.tsx @@ -3,7 +3,7 @@ import { useContext } from 'react' import { FormattedMessage } from 'react-intl' import { datetimeFormat } from '~/common/utils' -import { Form, LanguageContext } from '~/components' +import { Form, LanguageContext, Spacer } from '~/components' import { ArticleCampaignInput, CampaignState, @@ -11,91 +11,138 @@ import { } from '~/gql/graphql' export interface SelectCampaignProps { - appliedCampaign: EditorSelectCampaignFragment - selectedStage?: string + campaigns: EditorSelectCampaignFragment[] + selectedCampaign: EditorSelectCampaignFragment | undefined + selectedStage: string | undefined editCampaign: (value?: ArticleCampaignInput) => any } -export const getSelectCampaign = ({ +export const getSelectCampaigns = ({ applied, attached, createdAt, }: { - applied?: EditorSelectCampaignFragment + applied?: EditorSelectCampaignFragment[] attached: Array<{ campaign: { id: string } stage?: { id: string } | null }> - createdAt: string // draft or article creation time + createdAt: string }) => { - const { start } = applied?.writingPeriod || {} - const isCampaignStarted = !!start && new Date(createdAt) >= new Date(start) - const isCampaignActive = applied?.state === CampaignState.Active + const campaigns = applied?.filter((campaign) => { + const { start, end } = campaign?.writingPeriod || {} + const isCampaignStarted = !!start && new Date(createdAt) >= new Date(start) + const isCampaignEnded = !!end && new Date(createdAt) >= new Date(end) + const isCampaignActive = campaign?.state === CampaignState.Active - // only show appliedCampaign if the article or draft is created during the writing period - const appliedCampaign = - isCampaignStarted && isCampaignActive ? applied : undefined - const selectedCampaign = attached.filter( - (c) => c.campaign.id === applied?.id - )[0] - const selectedStage = selectedCampaign?.stage?.id + // only show appliedCampaign if the article or draft is created during the writing period + return isCampaignStarted && !isCampaignEnded && isCampaignActive + }) + + const selectedCampaign = campaigns?.find((campaign) => { + return attached.find((a) => a.campaign.id === campaign.id) + }) + + const selectedStage = attached.find( + (a) => a.campaign.id === selectedCampaign?.id + )?.stage?.id return { - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, } } const SelectCampaign = ({ - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, }: SelectCampaignProps) => { const { lang } = useContext(LanguageContext) - const RESET_OPTION = { - name: , + const RESET_CAMPAIGN_OPTION = { + name: , + value: undefined, + selected: !selectedCampaign?.id, + } + const RESET_STAGE_OPTION = { + name: , value: undefined, selected: !selectedStage, } const now = new Date() - const availableStages = appliedCampaign.stages.filter((s) => { - const period = s.period + const availableStages = selectedCampaign?.id + ? campaigns + .find((c) => c.id === selectedCampaign.id) + ?.stages.filter((s) => { + const period = s.period - if (!period) return false + if (!period) return false - return now >= new Date(period.start) - }) + return now >= new Date(period.start) + }) + : undefined return ( - - name="select-campaign" - onChange={(option) => - editCampaign( - option.value - ? { campaign: appliedCampaign.id, stage: option.value } - : undefined - ) - } - options={[ - RESET_OPTION, - ...availableStages.reverse().map((s) => { - return { - name: s.period?.start - ? `${s.name} - ${datetimeFormat.absolute({ - date: s.period.start, - lang, - optionalYear: false, - utc8: true, - })}` - : s.name, - value: s.id, - selected: s.id === selectedStage, - } - }), - ]} - size={14} - color="freeWriteBlue" - /> + <> + + name="select-campaign" + onChange={(option) => { + editCampaign( + option.value !== undefined ? { campaign: option.value } : undefined + ) + }} + options={[ + RESET_CAMPAIGN_OPTION, + ...campaigns.map((c) => { + return { + name: c.name, + value: c.id, + selected: c.id === selectedCampaign?.id, + } + }), + ]} + size={14} + color="freeWriteBlue" + /> + {selectedCampaign?.id && + availableStages && + availableStages.length > 0 && ( + <> + + + name="select-stage" + onChange={(option) => { + editCampaign( + option.value + ? { campaign: selectedCampaign.id, stage: option.value } + : { campaign: selectedCampaign.id } + ) + }} + options={[ + RESET_STAGE_OPTION, + ...availableStages.reverse().map((s) => { + return { + name: s.period?.start + ? `${s.name} - ${datetimeFormat.absolute({ + date: s.period.start, + lang, + optionalYear: false, + utc8: true, + })}` + : s.name, + value: s.id, + selected: s.id === selectedStage, + } + }), + ]} + size={14} + color="freeWriteBlue" + /> + + )} + ) } @@ -103,6 +150,7 @@ SelectCampaign.fragments = gql` fragment EditorSelectCampaign on WritingChallenge { id state + name writingPeriod { start end diff --git a/src/components/Editor/SettingsDialog/List/index.tsx b/src/components/Editor/SettingsDialog/List/index.tsx index 4fad29f003..edfa838f22 100644 --- a/src/components/Editor/SettingsDialog/List/index.tsx +++ b/src/components/Editor/SettingsDialog/List/index.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from 'react-intl' -import { Dialog } from '~/components' +import { Dialog, toast } from '~/components' import { SetPublishISCNProps } from '~/components/Editor' import ListItem from '../../ListItem' @@ -53,7 +53,8 @@ const SettingsList = ({ collectionCount, tagsCount, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, @@ -68,6 +69,29 @@ const SettingsList = ({ toggleComment, disableChangeCanComment, } + const handleConfirm = () => { + if ( + selectedCampaign && + selectedCampaign.stages.length > 0 && + !selectedStage + ) { + toast.error({ + message: ( + + ), + }) + return + } + + if (onConfirm) { + onConfirm() + } else { + forward('confirm') + } + } return ( <> @@ -78,7 +102,7 @@ const SettingsList = ({ rightBtn={ forward('confirm')} + onClick={handleConfirm} loading={saving} disabled={disabled} /> @@ -108,7 +132,7 @@ const SettingsList = ({ )} - {appliedCampaign && editCampaign && ( + {campaigns && campaigns.length > 0 && editCampaign && (

@@ -187,7 +212,7 @@ const SettingsList = ({ /> forward('confirm')} + onClick={handleConfirm} loading={saving} disabled={disabled} /> diff --git a/src/components/Editor/SettingsDialog/index.tsx b/src/components/Editor/SettingsDialog/index.tsx index 5b3b8ae517..672d44542f 100644 --- a/src/components/Editor/SettingsDialog/index.tsx +++ b/src/components/Editor/SettingsDialog/index.tsx @@ -113,7 +113,8 @@ const BaseEditorSettingsDialog = ({ togglePublishISCN, iscnPublishSaving, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, @@ -183,7 +184,8 @@ const BaseEditorSettingsDialog = ({ } const campaignProps: Partial = { - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, } diff --git a/src/components/Editor/Sidebar/Campaign/index.tsx b/src/components/Editor/Sidebar/Campaign/index.tsx index 743d94d197..011354311d 100644 --- a/src/components/Editor/Sidebar/Campaign/index.tsx +++ b/src/components/Editor/Sidebar/Campaign/index.tsx @@ -8,7 +8,7 @@ import Box from '../Box' import styles from './styles.module.css' const SidebarCampaign: React.FC> = (props) => { - if (!props.appliedCampaign || !props.editCampaign) { + if (!props.campaigns || props.campaigns.length === 0 || !props.editCampaign) { return null } diff --git a/src/components/Editor/TagCustomStagingArea/RecommendedTags/index.tsx b/src/components/Editor/TagCustomStagingArea/RecommendedTags/index.tsx index 9330743df0..cac5fe7747 100644 --- a/src/components/Editor/TagCustomStagingArea/RecommendedTags/index.tsx +++ b/src/components/Editor/TagCustomStagingArea/RecommendedTags/index.tsx @@ -7,7 +7,7 @@ import { EditorRecommendedTagsQuery } from '~/gql/graphql' import styles from './styles.module.css' type EditorRecommendedTagsUserTagsEdgesNode = NonNullable< - NonNullable['tags']['edges'] + NonNullable['tags']['edges'] >[0]['node'] & { __typename: 'Tag' } type RecommendedTagsProps = { diff --git a/src/components/Editor/TagCustomStagingArea/SelectedTags/index.tsx b/src/components/Editor/TagCustomStagingArea/SelectedTags/index.tsx index bcbe940121..5a90ddeee3 100644 --- a/src/components/Editor/TagCustomStagingArea/SelectedTags/index.tsx +++ b/src/components/Editor/TagCustomStagingArea/SelectedTags/index.tsx @@ -5,7 +5,7 @@ import { EditorRecommendedTagsQuery } from '~/gql/graphql' import styles from './styles.module.css' type EditorRecommendedTagsUserTagsEdgesNode = NonNullable< - NonNullable['tags']['edges'] + NonNullable['tags']['edges'] >[0]['node'] & { __typename: 'Tag' } type SelectedTagsProps = { diff --git a/src/components/Editor/TagCustomStagingArea/gql.ts b/src/components/Editor/TagCustomStagingArea/gql.ts index fcdcdb5079..a16ce12cc4 100644 --- a/src/components/Editor/TagCustomStagingArea/gql.ts +++ b/src/components/Editor/TagCustomStagingArea/gql.ts @@ -3,8 +3,8 @@ import gql from 'graphql-tag' import { ListTag } from '~/components/Tag' export const EDITOR_RECOMMENDED_TAGS = gql` - query EditorRecommendedTags($userName: String!) { - user(input: { userName: $userName }) { + query EditorRecommendedTags { + viewer { id tags(input: { first: 10 }) { edges { diff --git a/src/components/Editor/TagCustomStagingArea/index.tsx b/src/components/Editor/TagCustomStagingArea/index.tsx index 15b8d91a78..4d5d301e2b 100644 --- a/src/components/Editor/TagCustomStagingArea/index.tsx +++ b/src/components/Editor/TagCustomStagingArea/index.tsx @@ -1,8 +1,8 @@ +import { useQuery } from '@apollo/react-hooks' import _uniqBy from 'lodash/uniqBy' -import { useContext } from 'react' import { MAX_ARTICLE_TAG_LENGTH } from '~/common/enums' -import { SpinnerBlock, usePublicQuery, ViewerContext } from '~/components' +import { SpinnerBlock } from '~/components' import { SelectTag } from '~/components/SearchSelect/SearchingArea' import { CustomStagingAreaProps } from '~/components/SearchSelect/StagingArea' import { EditorRecommendedTagsQuery } from '~/gql/graphql' @@ -14,7 +14,7 @@ import styles from './styles.module.css' type EditorRecommendedTagsUserTagsEdgesNode = Required< NonNullable< - NonNullable['tags']['edges'] + NonNullable['tags']['edges'] >[0]['node'] > @@ -24,21 +24,16 @@ const TagCustomStagingArea = ({ hint, toStagingArea, }: CustomStagingAreaProps) => { - const viewer = useContext(ViewerContext) - /** * Data Fetching */ // public data - const { data, loading } = usePublicQuery( - EDITOR_RECOMMENDED_TAGS, - { - variables: { userName: viewer.userName }, - } + const { data, loading } = useQuery( + EDITOR_RECOMMENDED_TAGS ) // recommended tags - const userTagsEdges = data?.user?.tags.edges || [] + const userTagsEdges = data?.viewer?.tags.edges || [] let recommendedTags = [...userTagsEdges]?.map((edge) => edge.node) // remove duplicated tags diff --git a/src/components/Hook/useReadTimer.ts b/src/components/Hook/useReadTimer.ts index 67cfd0389a..02f4a3efb2 100644 --- a/src/components/Hook/useReadTimer.ts +++ b/src/components/Hook/useReadTimer.ts @@ -25,7 +25,7 @@ export const useReadTimer = ({ articleId, container }: Props) => { }, 3000) const storeReadTime = () => { - if (articleId && readTimer?.current) + if (articleId && readTimer) analytics.trackEvent('read_time', { articleId, time: readTimer.current, diff --git a/src/views/ArticleDetail/Edit/Header/index.tsx b/src/views/ArticleDetail/Edit/Header/index.tsx index 66e4ddddcc..105271b6e0 100644 --- a/src/views/ArticleDetail/Edit/Header/index.tsx +++ b/src/views/ArticleDetail/Edit/Header/index.tsx @@ -97,10 +97,11 @@ const EditModeHeader = ({ const isSensitiveRevised = restProps.contentSensitive !== article.sensitiveByAuthor const isCampaignRevised = + restProps.selectedCampaign?.id !== article.campaigns[0]?.campaign.id || restProps.selectedStage !== article.campaigns[0]?.stage?.id const isResetCampaign = isCampaignRevised && - (!restProps.appliedCampaign?.id || !restProps.selectedStage) + (!restProps.selectedCampaign?.id || !restProps.selectedStage) const needRepublish = isTitleRevised || @@ -165,7 +166,7 @@ const EditModeHeader = ({ ? [] : [ { - campaign: restProps.appliedCampaign?.id, + campaign: restProps.selectedCampaign?.id, stage: restProps.selectedStage, }, ], diff --git a/src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts b/src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts new file mode 100644 index 0000000000..f117621365 --- /dev/null +++ b/src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react' + +import { getSelectCampaigns } from '~/components/Editor/SelectCampaign' +import { + ArticleCampaignInput, + EditorSelectCampaignFragment, +} from '~/gql/graphql' + +import { Article } from '../index' + +export const useCampaignState = (article: Article) => { + const appliedCampaigns = article.author.campaigns.edges?.map((e) => e.node) + + const { + campaigns: selectableCampaigns, + selectedCampaign: initialSelectedCampaign, + selectedStage: initialSelectedStage, + } = getSelectCampaigns({ + applied: appliedCampaigns, + attached: article.campaigns, + createdAt: article.createdAt, + }) + + const [campaign, setCampaign] = useState( + initialSelectedCampaign?.id + ? { + campaign: initialSelectedCampaign.id, + stage: initialSelectedStage, + } + : undefined + ) + + // UI state for selected campaign/stage + const [selectedCampaign, setSelectedCampaign] = useState< + EditorSelectCampaignFragment | undefined + >(initialSelectedCampaign) + const [selectedStage, setSelectedStage] = useState( + initialSelectedStage + ) + + // Keep UI state in sync with campaign input + useEffect(() => { + setSelectedCampaign( + selectableCampaigns?.find((c) => c.id === campaign?.campaign) + ) + setSelectedStage(campaign?.stage || undefined) + }, [campaign, selectableCampaigns]) + + return { + campaign, + setCampaign, + selectedCampaign, + selectedStage, + selectableCampaigns, + } +} diff --git a/src/views/ArticleDetail/Edit/gql.ts b/src/views/ArticleDetail/Edit/gql.ts index 914eb03b27..3926c0ea22 100644 --- a/src/views/ArticleDetail/Edit/gql.ts +++ b/src/views/ArticleDetail/Edit/gql.ts @@ -35,7 +35,7 @@ export const GET_EDIT_ARTICLE = gql` ownCircles { ...DigestRichCirclePublic } - campaigns(input: { first: 1 }) { + campaigns(input: { first: null }) { edges { node { id diff --git a/src/views/ArticleDetail/Edit/index.tsx b/src/views/ArticleDetail/Edit/index.tsx index 1b88e284ab..3f1f8d4005 100644 --- a/src/views/ArticleDetail/Edit/index.tsx +++ b/src/views/ArticleDetail/Edit/index.tsx @@ -30,10 +30,7 @@ import { } from '~/components/Editor' import BottomBar from '~/components/Editor/BottomBar' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' -import { - getSelectCampaign, - SelectCampaignProps, -} from '~/components/Editor/SelectCampaign' +import { SelectCampaignProps } from '~/components/Editor/SelectCampaign' import Sidebar from '~/components/Editor/Sidebar' import { SidebarIndentProps } from '~/components/Editor/Sidebar/Indent' import { QueryError, useImperativeQuery } from '~/components/GQL' @@ -44,7 +41,6 @@ import { } from '~/components/GQL/mutations/uploadFile' import { ArticleAccessType, - ArticleCampaignInput, ArticleDigestDropdownArticleFragment, ArticleLicenseType, AssetFragment, @@ -60,10 +56,11 @@ import { import { GET_EDIT_ARTICLE, GET_EDIT_ARTICLE_ASSETS } from './gql' import EditHeader from './Header' +import { useCampaignState } from './Hooks/useCampaignState' import PublishState from './PublishState' import styles from './styles.module.css' -type Article = NonNullable< +export type Article = NonNullable< QueryEditArticleQuery['article'] & { __typename: 'Article' } @@ -140,22 +137,8 @@ const BaseEdit = ({ article }: { article: Article }) => { setLicense(newLicense) } - // campaign - const appliedCampaigns = article.author.campaigns.edges?.map((e) => e.node) - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: appliedCampaigns && appliedCampaigns[0], - attached: article.campaigns, - createdAt: article.createdAt, - }) - - const [campaign, setCampaign] = useState( - appliedCampaign?.id && selectedStage - ? { - campaign: appliedCampaign.id, - stage: selectedStage, - } - : undefined - ) + const { setCampaign, selectedCampaign, selectedStage, selectableCampaigns } = + useCampaignState(article) const [requestForDonation, setRequestForDonation] = useState( article.requestForDonation @@ -212,8 +195,9 @@ const BaseEdit = ({ article }: { article: Article }) => { indentSaving: false, } const campaignProps: Partial = { - appliedCampaign, - selectedStage: campaign?.stage, + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, editCampaign: setCampaign, } diff --git a/src/views/ArticleDetail/Header/gql.ts b/src/views/ArticleDetail/Header/gql.ts index 1bbed3ca0c..ab30f43887 100644 --- a/src/views/ArticleDetail/Header/gql.ts +++ b/src/views/ArticleDetail/Header/gql.ts @@ -13,6 +13,9 @@ export const fragments = { nameZhHant: name(input: { language: zh_hant }) nameZhHans: name(input: { language: zh_hans }) nameEn: name(input: { language: en }) + announcements { + id + } } } stage { diff --git a/src/views/ArticleDetail/Header/index.tsx b/src/views/ArticleDetail/Header/index.tsx index 47d1077a00..fec515b8b7 100644 --- a/src/views/ArticleDetail/Header/index.tsx +++ b/src/views/ArticleDetail/Header/index.tsx @@ -22,6 +22,9 @@ const Header = ({ article }: HeaderProps) => { const campaign = article.campaigns[0]?.campaign const campaignStage = article.campaigns[0]?.stage const { lang } = useContext(LanguageContext) + const isAnnouncement = article.campaigns[0]?.campaign?.announcements?.some( + (announcement: { id: string }) => announcement.id === article.id + ) return (
@@ -36,6 +39,7 @@ const Header = ({ article }: HeaderProps) => { page: 'campaignDetail', campaign, stage: campaignStage || undefined, + announcement: isAnnouncement, }).href } onClick={() => { diff --git a/src/views/CampaignDetail/Apply/Button/index.tsx b/src/views/CampaignDetail/Apply/Button/index.tsx index cc52edcbe9..b811fceba5 100644 --- a/src/views/CampaignDetail/Apply/Button/index.tsx +++ b/src/views/CampaignDetail/Apply/Button/index.tsx @@ -34,7 +34,8 @@ const ApplyCampaignButton = ({ const isRejected = applicationState === 'rejected' const isNotApplied = !applicationState const isAppliedDuringPeriod = - appliedAt && new Date(appliedAt) <= new Date(appEnd) + (appliedAt && appEnd && new Date(appliedAt) <= new Date(appEnd)) || + (appliedAt && !appEnd) const isApplicationStarted = now >= new Date(appStart) const isActiveCampaign = campaign.state === 'active' diff --git a/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts b/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts index 7a6508b378..0b87aa36af 100644 --- a/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts +++ b/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts @@ -21,6 +21,7 @@ export const CAMPAIGN_ARTICLES_PUBLIC = gql` edges { cursor featured + announcement node { id campaigns { diff --git a/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx b/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx index 148a6de4f2..30185f0b7e 100644 --- a/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx +++ b/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx @@ -49,17 +49,21 @@ const getArticleStage = (article: CampaignArticlesPublicQueryArticle) => { return stage } -const getArticleStageName = ( +const getLabel = ( article: CampaignArticlesPublicQueryArticle, - lang: string + lang: string, + announcement: boolean ) => { const stage = getArticleStage(article) - // announcement if nullish - if (!stage) { + if (announcement) { return } + if (!stage) { + return '' + } + return stage[ `name${lang === 'en' ? 'En' : lang === 'zh-Hans' ? 'ZhHans' : 'ZhHant'}` ] @@ -218,20 +222,22 @@ const MainFeed = ({ feedType, camapign }: MainFeedProps) => { return ( - {edges.map(({ node, featured }, i) => ( + {edges.map(({ node, featured, announcement }, i) => ( - {(isAll || isFeatured) && ( + {(isAll || + isFeatured || + getLabel(node, lang, announcement)) && ( - {getArticleStageName(node, lang)} + {getLabel(node, lang, announcement)} )} {!isFeatured && featured && } diff --git a/src/views/Me/DraftDetail/BottomBar.tsx b/src/views/Me/DraftDetail/BottomBar.tsx index ce21c0f591..703e29093e 100644 --- a/src/views/Me/DraftDetail/BottomBar.tsx +++ b/src/views/Me/DraftDetail/BottomBar.tsx @@ -10,7 +10,7 @@ import { import BottomBar from '~/components/Editor/BottomBar' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' import { - getSelectCampaign, + getSelectCampaigns, SelectCampaignProps, } from '~/components/Editor/SelectCampaign' import { SidebarIndentProps } from '~/components/Editor/Sidebar/Indent' @@ -71,8 +71,12 @@ const EditDraftBottomBar = ({ const hasOwnCircle = ownCircles && ownCircles.length >= 1 const tags = (draft.tags || []).map(toDigestTagPlaceholder) - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: campaigns && campaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, + } = getSelectCampaigns({ + applied: campaigns, attached: draft.campaigns, createdAt: draft.createdAt, }) @@ -124,9 +128,10 @@ const EditDraftBottomBar = ({ toggleIndent, indentSaving, - appliedCampaign, + campaigns: selectableCampaigns, + selectedCampaign, selectedStage, - editCampaign, + editCampaign: (value) => editCampaign(value as any), } return ( diff --git a/src/views/Me/DraftDetail/SettingsButton/index.tsx b/src/views/Me/DraftDetail/SettingsButton/index.tsx index f8f13e81ce..b9c177325f 100644 --- a/src/views/Me/DraftDetail/SettingsButton/index.tsx +++ b/src/views/Me/DraftDetail/SettingsButton/index.tsx @@ -10,7 +10,7 @@ import { SetTagsProps, } from '~/components/Editor' import { - getSelectCampaign, + getSelectCampaigns, SelectCampaignProps, } from '~/components/Editor/SelectCampaign' import { EditorSettingsDialog } from '~/components/Editor/SettingsDialog' @@ -130,16 +130,21 @@ const SettingsButton = ({ iscnPublishSaving, } - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: campaigns && campaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, + } = getSelectCampaigns({ + applied: campaigns, attached: draft.campaigns, createdAt: draft.createdAt, }) const campaignProps: Partial = { - appliedCampaign, + campaigns: selectableCampaigns, + selectedCampaign, selectedStage, - editCampaign, + editCampaign: (value) => editCampaign(value as any), } const responseProps: SetResponseProps = { diff --git a/src/views/Me/DraftDetail/Sidebar/index.tsx b/src/views/Me/DraftDetail/Sidebar/index.tsx index 816cf36e1f..a2f5b7e951 100644 --- a/src/views/Me/DraftDetail/Sidebar/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/index.tsx @@ -1,7 +1,7 @@ import { ENTITY_TYPE } from '~/common/enums' import { toDigestTagPlaceholder } from '~/components' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' -import { getSelectCampaign } from '~/components/Editor/SelectCampaign' +import { getSelectCampaigns } from '~/components/Editor/SelectCampaign' import Sidebar from '~/components/Editor/Sidebar' import { DigestRichCirclePublicFragment, @@ -144,17 +144,22 @@ const EditDraftIndent = ({ draft }: SidebarProps) => { const EditDraftCampaign = ({ draft, campaigns }: SidebarProps) => { const { edit } = useEditDraftCampaign() - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: campaigns && campaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, + } = getSelectCampaigns({ + applied: campaigns, attached: draft.campaigns, createdAt: draft.createdAt, }) return ( edit(value as any)} /> ) } diff --git a/src/views/Me/DraftDetail/gql.ts b/src/views/Me/DraftDetail/gql.ts index bb11e35494..d720f84404 100644 --- a/src/views/Me/DraftDetail/gql.ts +++ b/src/views/Me/DraftDetail/gql.ts @@ -61,7 +61,7 @@ export const DRAFT_DETAIL_VIEWER = gql` query DraftDetailViewerQuery { viewer { id - campaigns(input: { first: 1 }) { + campaigns(input: { first: null }) { edges { node { id