diff --git a/lang/default.json b/lang/default.json index a7acebadcf..c41b51dc2e 100644 --- a/lang/default.json +++ b/lang/default.json @@ -383,6 +383,9 @@ "4l6vz1": { "defaultMessage": "Copy" }, + "4nHH2x": { + "defaultMessage": "Applied successfully" + }, "4tqFCR": { "defaultMessage": "The author has not bound the USDT wallet yet" }, @@ -445,6 +448,9 @@ "5sg7KC": { "defaultMessage": "Password" }, + "5y5rID": { + "defaultMessage": "We will review your application shortly, stay tuned for the event!" + }, "6+eeJ4": { "defaultMessage": "Lowercase letters, numbers and underscores", "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" @@ -519,6 +525,9 @@ "6oOCCL": { "defaultMessage": "Upload file" }, + "6pc948": { + "defaultMessage": "Add to FreeWrite" + }, "6q0G5e": { "defaultMessage": "Successfully added", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" @@ -582,6 +591,9 @@ "defaultMessage": "Still quiet here. {br}Be the first one to say hello!", "description": "src/components/Empty/EmptyComment.tsx" }, + "8+Z5E9": { + "defaultMessage": "The badge signifies your participation and completion in the \"Free Write in 7 days\"." + }, "80bF0W": { "defaultMessage": "No comments", "description": "src/components/Forms/MomentCommentForm/index.tsx" @@ -657,6 +669,9 @@ "9J0iCw": { "defaultMessage": "Deleted user" }, + "9OPnTz": { + "defaultMessage": "System or connection abnormality, please refresh the page and click “Join” again." + }, "9SXN7s": { "defaultMessage": "No more comments", "description": "src/views/ArticleDetail/Comments/LatestComments/index.tsx" @@ -724,6 +739,10 @@ "defaultMessage": "Followers", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" }, + "AeIRlL": { + "defaultMessage": "Articles", + "description": "src/views/Follow/Tabs/index.tsx" + }, "AeVndq": { "defaultMessage": "mentioned you in a comment at {commentMoment}" }, @@ -776,6 +795,9 @@ "Bc20la": { "defaultMessage": "days" }, + "BhRxYr": { + "defaultMessage": "Free Write Badge" + }, "Bjdw71": { "defaultMessage": "followers_empty", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -850,6 +872,9 @@ "D9R/ol": { "defaultMessage": "Add articles to your circle" }, + "D9tEst": { + "defaultMessage": "Matters Architect" + }, "DP3yqI": { "defaultMessage": "Donation count" }, @@ -945,6 +970,9 @@ "defaultMessage": "You do not have permission to perform this operation", "description": "FORBIDDEN_BY_STATE" }, + "ErV/vT": { + "defaultMessage": "Confirm to join" + }, "F3zk7E": { "defaultMessage": "(This action cannot be undone)", "description": "src/components/DraftDigest/Feed/DeleteButton.tsx" @@ -969,6 +997,9 @@ "defaultMessage": "Switch to Optimism network now?", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" }, + "FYeEw1": { + "defaultMessage": "Application period{tz}:" + }, "FaTb0A": { "defaultMessage": "Install MetaMask", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -1062,6 +1093,9 @@ "HAlOn1": { "defaultMessage": "Name" }, + "HB2VOx": { + "defaultMessage": "System or connection abnormality, please refresh the page and click “Apply” again." + }, "HBxXD/": { "defaultMessage": "License" }, @@ -1090,6 +1124,10 @@ "HbEL82": { "defaultMessage": "Comment has been deleted" }, + "HgY+72": { + "defaultMessage": "Apply", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "HkozYU": { "defaultMessage": "Switch wallet", "description": "src/components/Forms/WalletAuthForm/Connect.tsx" @@ -1188,6 +1226,10 @@ "defaultMessage": "New followers", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" }, + "JpAsUV": { + "defaultMessage": "More moments", + "description": "src/views/Follow/Feed/UserPostMomentActivity" + }, "JpS59y": { "defaultMessage": "Accepted", "description": "src/views/Circle/Settings/ManageInvitation/Invites/index.tsx" @@ -1324,6 +1366,9 @@ "LnesNr": { "defaultMessage": "Successfully removed collaborator" }, + "LoQ3BF": { + "defaultMessage": "This badge represents your completion of Free Write in 7 days. Congratulations on finishing this meaningful writing journey!" + }, "Lp6CiR": { "defaultMessage": "Settings - Misc", "description": "src/views/Me/Settings/Misc/index.tsx" @@ -1535,6 +1580,10 @@ "Pp/0po": { "defaultMessage": "Shuffle" }, + "Pq/7m5": { + "defaultMessage": "Reviewing...", + "description": "type:join" + }, "PtV68+": { "defaultMessage": "Incorrect email or password", "description": "USER_EMAIL_NOT_FOUND" @@ -1560,6 +1609,9 @@ "defaultMessage": "Set the Circle URL (cannot be modified after creation)", "description": "src/components/Forms/CreateCircleForm/Init.tsx" }, + "QbjADp": { + "defaultMessage": "Seed User" + }, "QfVedX": { "defaultMessage": "Insert divider" }, @@ -1680,6 +1732,9 @@ "defaultMessage": "Payout", "description": "src/components/Transaction/index.tsx" }, + "Sfql0+": { + "defaultMessage": "Congratulations! You've got the Grand Badge!" + }, "SuRTsQ": { "defaultMessage": "Register for ISCN" }, @@ -1727,6 +1782,9 @@ "defaultMessage": "Payment will be processed by Stripe, allowing your support to be unrestricted by region.", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx" }, + "TZgskS": { + "defaultMessage": "No one has published yet, check back later!" + }, "TcTp+J": { "defaultMessage": "Image uploaded" }, @@ -1827,6 +1885,9 @@ "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" @@ -1845,6 +1906,9 @@ "defaultMessage": "Last 3 months", "description": "src/views/Me/Analytics/SelectPeriod/index.tsx" }, + "W3hNBA": { + "defaultMessage": "Free Write in 7 days Grand Badge" + }, "W3tqQO": { "defaultMessage": "[image]" }, @@ -2093,6 +2157,9 @@ "defaultMessage": "docs", "description": "src/components/Dialogs/ENSDialog/ENSDescription/index.tsx" }, + "aKEiNd": { + "defaultMessage": "You missed the registration period, you can still join as a latecomer. Apply earlier next time for the chance to get the badge." + }, "aKlTO2": { "defaultMessage": "You can add {count} more collaborators." }, @@ -2124,6 +2191,9 @@ "ai7kS4": { "defaultMessage": "My Works" }, + "al5/yQ": { + "defaultMessage": "Joined successfully" + }, "aqX2Bt": { "defaultMessage": "go to the homepage", "description": "src/views/Callback/UI.tsx" @@ -2160,6 +2230,9 @@ "beLe/F": { "defaultMessage": "Broadcast" }, + "buf5vO": { + "defaultMessage": "Event Information" + }, "c/z318": { "defaultMessage": "Incorrect email or password", "description": "src/components/Forms/EmailLoginForm/index.tsx" @@ -2319,6 +2392,10 @@ "f24jaP": { "defaultMessage": "Credit card support requires emails related to financial information, please verify your email address first." }, + "f5jWMJ": { + "defaultMessage": "Confirm", + "description": "src/views/CampaignDetail/Apply/Dialog/index.tsx" + }, "fBzH+2": { "defaultMessage": "Disallow readers to respond to this article (you can enable responses later by editing this article)" }, @@ -2367,6 +2444,10 @@ "g5pX+a": { "defaultMessage": "About" }, + "gCafm/": { + "defaultMessage": "Join", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "gK6OxL": { "defaultMessage": "Leave a comment?", "description": "src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx" @@ -2435,6 +2516,10 @@ "defaultMessage": "are following", "description": "src/views/TagDetail/Followers/index.tsx" }, + "hfRsLB": { + "defaultMessage": "Writers {count}", + "description": "src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx" + }, "hgtWIO": { "defaultMessage": "Articles have been collected", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" @@ -2511,6 +2596,10 @@ "defaultMessage": "Sign up with email", "description": "src/components/Forms/SelectAuthMethodForm/NormalFeed.tsx" }, + "jLkKbI": { + "defaultMessage": "Reviewing...", + "description": "type:apply" + }, "jiB0Z2": { "defaultMessage": "Unable to bind wallet", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -2574,6 +2663,9 @@ "defaultMessage": "Remove", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "krvjo9": { + "defaultMessage": "Event period{tz}:" + }, "ksIL/T": { "defaultMessage": "Kind reminder: This wallet address is different from the wallet address you use to log in to Matters" }, @@ -2630,6 +2722,9 @@ "defaultMessage": "commented", "description": "src/components/Notice/CommentNotice/ArticleNewCommentNotice.tsx" }, + "lfiBVR": { + "defaultMessage": "CIVIC LIKER" + }, "liBHHE": { "defaultMessage": "Any thoughts? Leave a kind comment~" }, @@ -2646,6 +2741,9 @@ "defaultMessage": "Circle Description", "description": "src/components/Forms/CreateCircleForm/Profile.tsx" }, + "m1wKuC": { + "defaultMessage": "Add to Free Write" + }, "m4GG4b": { "defaultMessage": "Delete collection" }, @@ -2758,6 +2856,9 @@ "oGiO//": { "defaultMessage": "Insert audio" }, + "oLOus+": { + "defaultMessage": "Application has been submitted 🎉" + }, "ob+HDS": { "defaultMessage": "View Circle" }, @@ -2772,6 +2873,9 @@ "onDesU": { "defaultMessage": "Congratulations! Now you can browse all works within the circle for free and chat with everyone." }, + "orIq4X": { + "defaultMessage": "More than 100 supports" + }, "p5qZnJ": { "defaultMessage": "liked", "description": "src/components/Notice/ArticleNotice/ArticleNewAppreciationNotice.tsx" @@ -2968,6 +3072,10 @@ "sy+pv5": { "defaultMessage": "Email" }, + "syBMnY": { + "defaultMessage": "writers", + "description": "src/views/CampaignDetail/InfoHeader/Participants/index.tsx" + }, "syEQFE": { "defaultMessage": "Publish" }, @@ -3241,6 +3349,10 @@ "xkr+zo": { "defaultMessage": "Terms" }, + "xl95XN": { + "defaultMessage": "Writers", + "description": "src/views/CampaignDetail/SideParticipants/index.tsx" + }, "xmcVZ0": { "defaultMessage": "Search" }, diff --git a/lang/en.json b/lang/en.json index f8a1c1a55d..c7a8bcd2ee 100644 --- a/lang/en.json +++ b/lang/en.json @@ -383,6 +383,9 @@ "4l6vz1": { "defaultMessage": "Copy" }, + "4nHH2x": { + "defaultMessage": "Applied successfully" + }, "4tqFCR": { "defaultMessage": "The author has not bound the USDT wallet yet" }, @@ -445,6 +448,9 @@ "5sg7KC": { "defaultMessage": "Password" }, + "5y5rID": { + "defaultMessage": "We will review your application shortly, stay tuned for the event!" + }, "6+eeJ4": { "defaultMessage": "Lowercase letters, numbers and underscores", "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" @@ -519,6 +525,9 @@ "6oOCCL": { "defaultMessage": "Upload file" }, + "6pc948": { + "defaultMessage": "Add to FreeWrite" + }, "6q0G5e": { "defaultMessage": "Successfully added", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" @@ -582,6 +591,9 @@ "defaultMessage": "Still quiet here. {br}Be the first one to say hello!", "description": "src/components/Empty/EmptyComment.tsx" }, + "8+Z5E9": { + "defaultMessage": "The badge signifies your participation and completion in the \"Free Write in 7 days\"." + }, "80bF0W": { "defaultMessage": "No comments", "description": "src/components/Forms/MomentCommentForm/index.tsx" @@ -657,6 +669,9 @@ "9J0iCw": { "defaultMessage": "Deleted user" }, + "9OPnTz": { + "defaultMessage": "System or connection abnormality, please refresh the page and click “Join” again." + }, "9SXN7s": { "defaultMessage": "No more comments", "description": "src/views/ArticleDetail/Comments/LatestComments/index.tsx" @@ -724,6 +739,10 @@ "defaultMessage": "Followers", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" }, + "AeIRlL": { + "defaultMessage": "Articles", + "description": "src/views/Follow/Tabs/index.tsx" + }, "AeVndq": { "defaultMessage": "mentioned you in a comment at {commentMoment}" }, @@ -776,6 +795,9 @@ "Bc20la": { "defaultMessage": "days" }, + "BhRxYr": { + "defaultMessage": "Free Write Badge" + }, "Bjdw71": { "defaultMessage": "followers_empty", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -850,6 +872,9 @@ "D9R/ol": { "defaultMessage": "Add articles to your circle" }, + "D9tEst": { + "defaultMessage": "Matters Architect" + }, "DP3yqI": { "defaultMessage": "Donation count" }, @@ -945,6 +970,9 @@ "defaultMessage": "You do not have permission to perform this operation", "description": "FORBIDDEN_BY_STATE" }, + "ErV/vT": { + "defaultMessage": "Confirm to join" + }, "F3zk7E": { "defaultMessage": "(This action cannot be undone)", "description": "src/components/DraftDigest/Feed/DeleteButton.tsx" @@ -969,6 +997,9 @@ "defaultMessage": "Switch to Optimism network now?", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" }, + "FYeEw1": { + "defaultMessage": "Application period{tz}:" + }, "FaTb0A": { "defaultMessage": "Install MetaMask", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -1062,6 +1093,9 @@ "HAlOn1": { "defaultMessage": "Name" }, + "HB2VOx": { + "defaultMessage": "System or connection abnormality, please refresh the page and click “Apply” again." + }, "HBxXD/": { "defaultMessage": "License" }, @@ -1090,6 +1124,10 @@ "HbEL82": { "defaultMessage": "Comment has been deleted" }, + "HgY+72": { + "defaultMessage": "Apply", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "HkozYU": { "defaultMessage": "Switch wallet", "description": "src/components/Forms/WalletAuthForm/Connect.tsx" @@ -1188,6 +1226,10 @@ "defaultMessage": "New followers", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" }, + "JpAsUV": { + "defaultMessage": "More moments", + "description": "src/views/Follow/Feed/UserPostMomentActivity" + }, "JpS59y": { "defaultMessage": "Accepted", "description": "src/views/Circle/Settings/ManageInvitation/Invites/index.tsx" @@ -1324,6 +1366,9 @@ "LnesNr": { "defaultMessage": "Successfully removed collaborator" }, + "LoQ3BF": { + "defaultMessage": "This badge represents your completion of Free Write in 7 days. Congratulations on finishing this meaningful writing journey!" + }, "Lp6CiR": { "defaultMessage": "Settings - Misc", "description": "src/views/Me/Settings/Misc/index.tsx" @@ -1535,6 +1580,10 @@ "Pp/0po": { "defaultMessage": "Shuffle" }, + "Pq/7m5": { + "defaultMessage": "Reviewing...", + "description": "type:join" + }, "PtV68+": { "defaultMessage": "Incorrect email or password", "description": "USER_EMAIL_NOT_FOUND" @@ -1560,6 +1609,9 @@ "defaultMessage": "Set the Circle URL (cannot be modified after creation)", "description": "src/components/Forms/CreateCircleForm/Init.tsx" }, + "QbjADp": { + "defaultMessage": "Seed User" + }, "QfVedX": { "defaultMessage": "Insert divider" }, @@ -1680,6 +1732,9 @@ "defaultMessage": "Payout", "description": "src/components/Transaction/index.tsx" }, + "Sfql0+": { + "defaultMessage": "Congratulations! You've got the Grand Badge!" + }, "SuRTsQ": { "defaultMessage": "Register for ISCN" }, @@ -1727,6 +1782,9 @@ "defaultMessage": "Payment will be processed by Stripe, allowing your support to be unrestricted by region.", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx" }, + "TZgskS": { + "defaultMessage": "No one has published yet, check back later!" + }, "TcTp+J": { "defaultMessage": "Image uploaded" }, @@ -1827,6 +1885,9 @@ "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" @@ -1845,6 +1906,9 @@ "defaultMessage": "Last 3 months", "description": "src/views/Me/Analytics/SelectPeriod/index.tsx" }, + "W3hNBA": { + "defaultMessage": "Free Write in 7 days Grand Badge" + }, "W3tqQO": { "defaultMessage": "[image]" }, @@ -2093,6 +2157,9 @@ "defaultMessage": "docs", "description": "src/components/Dialogs/ENSDialog/ENSDescription/index.tsx" }, + "aKEiNd": { + "defaultMessage": "You missed the registration period, you can still join as a latecomer. Apply earlier next time for the chance to get the badge." + }, "aKlTO2": { "defaultMessage": "You can add {count} more collaborators." }, @@ -2124,6 +2191,9 @@ "ai7kS4": { "defaultMessage": "My Works" }, + "al5/yQ": { + "defaultMessage": "Joined successfully" + }, "aqX2Bt": { "defaultMessage": "go to the homepage", "description": "src/views/Callback/UI.tsx" @@ -2160,6 +2230,9 @@ "beLe/F": { "defaultMessage": "Broadcast" }, + "buf5vO": { + "defaultMessage": "Event Information" + }, "c/z318": { "defaultMessage": "Incorrect email or password", "description": "src/components/Forms/EmailLoginForm/index.tsx" @@ -2319,6 +2392,10 @@ "f24jaP": { "defaultMessage": "Credit card support requires emails related to financial information, please verify your email address first." }, + "f5jWMJ": { + "defaultMessage": "Confirm", + "description": "src/views/CampaignDetail/Apply/Dialog/index.tsx" + }, "fBzH+2": { "defaultMessage": "Disallow readers to respond to this article (you can enable responses later by editing this article)" }, @@ -2367,6 +2444,10 @@ "g5pX+a": { "defaultMessage": "About" }, + "gCafm/": { + "defaultMessage": "Join", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "gK6OxL": { "defaultMessage": "Leave a comment?", "description": "src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx" @@ -2435,6 +2516,10 @@ "defaultMessage": "are following", "description": "src/views/TagDetail/Followers/index.tsx" }, + "hfRsLB": { + "defaultMessage": "Writers {count}", + "description": "src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx" + }, "hgtWIO": { "defaultMessage": "Articles have been collected", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" @@ -2511,6 +2596,10 @@ "defaultMessage": "Sign up with email", "description": "src/components/Forms/SelectAuthMethodForm/NormalFeed.tsx" }, + "jLkKbI": { + "defaultMessage": "Reviewing...", + "description": "type:apply" + }, "jiB0Z2": { "defaultMessage": "Unable to bind wallet", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -2574,6 +2663,9 @@ "defaultMessage": "Remove", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "krvjo9": { + "defaultMessage": "Event period{tz}:" + }, "ksIL/T": { "defaultMessage": "Kind reminder: This wallet address is different from the wallet address you use to log in to Matters" }, @@ -2630,6 +2722,9 @@ "defaultMessage": "commented", "description": "src/components/Notice/CommentNotice/ArticleNewCommentNotice.tsx" }, + "lfiBVR": { + "defaultMessage": "CIVIC LIKER" + }, "liBHHE": { "defaultMessage": "Any thoughts? Leave a kind comment~" }, @@ -2646,6 +2741,9 @@ "defaultMessage": "Circle Description", "description": "src/components/Forms/CreateCircleForm/Profile.tsx" }, + "m1wKuC": { + "defaultMessage": "Add to Free Write" + }, "m4GG4b": { "defaultMessage": "Delete collection" }, @@ -2758,6 +2856,9 @@ "oGiO//": { "defaultMessage": "Insert audio" }, + "oLOus+": { + "defaultMessage": "Application has been submitted 🎉" + }, "ob+HDS": { "defaultMessage": "View Circle" }, @@ -2772,6 +2873,9 @@ "onDesU": { "defaultMessage": "Congratulations! Now you can browse all works within the circle for free and chat with everyone." }, + "orIq4X": { + "defaultMessage": "More than 100 supports" + }, "p5qZnJ": { "defaultMessage": "liked", "description": "src/components/Notice/ArticleNotice/ArticleNewAppreciationNotice.tsx" @@ -2968,6 +3072,10 @@ "sy+pv5": { "defaultMessage": "Email" }, + "syBMnY": { + "defaultMessage": "writers", + "description": "src/views/CampaignDetail/InfoHeader/Participants/index.tsx" + }, "syEQFE": { "defaultMessage": "Publish" }, @@ -3241,6 +3349,10 @@ "xkr+zo": { "defaultMessage": "Terms" }, + "xl95XN": { + "defaultMessage": "Writers", + "description": "src/views/CampaignDetail/SideParticipants/index.tsx" + }, "xmcVZ0": { "defaultMessage": "Search" }, diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 8074a06b90..18f21ba015 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -1,6 +1,6 @@ { "+12veD": { - "defaultMessage": "开启回应" + "defaultMessage": "开启评论" }, "+3ny7b": { "defaultMessage": "{totalCount} 人赞赏了作品" @@ -251,7 +251,7 @@ "defaultMessage": "众聊" }, "20yT4t": { - "defaultMessage": "回应设置", + "defaultMessage": "评论设置", "description": "src/components/Editor/Sidebar/Response/index.tsx" }, "24jhEp": { @@ -383,6 +383,9 @@ "4l6vz1": { "defaultMessage": "复制" }, + "4nHH2x": { + "defaultMessage": "报名申请已递交 🎉" + }, "4tqFCR": { "defaultMessage": "作者尚未启用 USDT 钱包" }, @@ -445,6 +448,9 @@ "5sg7KC": { "defaultMessage": "密码" }, + "5y5rID": { + "defaultMessage": "我们将尽快处理你的申请,敬请期待活动开跑!" + }, "6+eeJ4": { "defaultMessage": "可使用小写英文、数字及下划线", "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" @@ -463,7 +469,7 @@ "defaultMessage": "开始创作" }, "6B+QXo": { - "defaultMessage": "回应设置", + "defaultMessage": "评论设置", "description": "src/components/Editor/ToggleResponse/index.tsx" }, "6BXcdo": { @@ -519,6 +525,9 @@ "6oOCCL": { "defaultMessage": "上传档案" }, + "6pc948": { + "defaultMessage": "投稿七日书自由写" + }, "6q0G5e": { "defaultMessage": "加入成功", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" @@ -582,6 +591,9 @@ "defaultMessage": "暂无评论", "description": "src/components/Empty/EmptyComment.tsx" }, + "8+Z5E9": { + "defaultMessage": "纪念你参与「七日书」并完成七天书写" + }, "80bF0W": { "defaultMessage": "暂无留言", "description": "src/components/Forms/MomentCommentForm/index.tsx" @@ -657,6 +669,9 @@ "9J0iCw": { "defaultMessage": "已注销用户" }, + "9OPnTz": { + "defaultMessage": "系统或连线异常,请刷新页面后重新点击晚鸟参与" + }, "9SXN7s": { "defaultMessage": "没有更多", "description": "src/views/ArticleDetail/Comments/LatestComments/index.tsx" @@ -724,6 +739,10 @@ "defaultMessage": "追踪", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" }, + "AeIRlL": { + "defaultMessage": "只看文章", + "description": "src/views/Follow/Tabs/index.tsx" + }, "AeVndq": { "defaultMessage": "在动态留言中提及了你 {commentMoment}" }, @@ -776,6 +795,9 @@ "Bc20la": { "defaultMessage": "天" }, + "BhRxYr": { + "defaultMessage": "自由写纪念徽章" + }, "Bjdw71": { "defaultMessage": "目前总追踪人数", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -850,6 +872,9 @@ "D9R/ol": { "defaultMessage": "将作品加入围炉,让更多人一起参与讨论" }, + "D9tEst": { + "defaultMessage": "马特市建筑师" + }, "DP3yqI": { "defaultMessage": "支持人数" }, @@ -945,6 +970,9 @@ "defaultMessage": "你无权限进行该操作", "description": "FORBIDDEN_BY_STATE" }, + "ErV/vT": { + "defaultMessage": "晚鸟参与七日书" + }, "F3zk7E": { "defaultMessage": "(草稿删除后无法恢复)", "description": "src/components/DraftDigest/Feed/DeleteButton.tsx" @@ -969,6 +997,9 @@ "defaultMessage": "目前非 Optimism 网络,立即切换?", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" }, + "FYeEw1": { + "defaultMessage": "报名期{tz}:" + }, "FaTb0A": { "defaultMessage": "安装 MetaMask", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -1062,6 +1093,9 @@ "HAlOn1": { "defaultMessage": "用户名" }, + "HB2VOx": { + "defaultMessage": "系统或连线异常,请刷新页面后重新点击报名参加" + }, "HBxXD/": { "defaultMessage": "版权声明" }, @@ -1090,6 +1124,10 @@ "HbEL82": { "defaultMessage": "评论已刪除" }, + "HgY+72": { + "defaultMessage": "报名参加", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "HkozYU": { "defaultMessage": "更换钱包", "description": "src/components/Forms/WalletAuthForm/Connect.tsx" @@ -1188,6 +1226,10 @@ "defaultMessage": "被追踪", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" }, + "JpAsUV": { + "defaultMessage": "更多动态", + "description": "src/views/Follow/Feed/UserPostMomentActivity" + }, "JpS59y": { "defaultMessage": "已接受", "description": "src/views/Circle/Settings/ManageInvitation/Invites/index.tsx" @@ -1324,6 +1366,9 @@ "LnesNr": { "defaultMessage": "移除协作者成功" }, + "LoQ3BF": { + "defaultMessage": "这枚徽章代表你完成了七天写作,恭喜走完这趟意义非凡的写作之旅!" + }, "Lp6CiR": { "defaultMessage": "设置 - 其他", "description": "src/views/Me/Settings/Misc/index.tsx" @@ -1535,6 +1580,10 @@ "Pp/0po": { "defaultMessage": "换一批" }, + "Pq/7m5": { + "defaultMessage": "晚鸟参与处理中", + "description": "type:join" + }, "PtV68+": { "defaultMessage": "邮箱或密码错误", "description": "USER_EMAIL_NOT_FOUND" @@ -1560,6 +1609,9 @@ "defaultMessage": "设置围炉网址(创建后不可修改)", "description": "src/components/Forms/CreateCircleForm/Init.tsx" }, + "QbjADp": { + "defaultMessage": "种子用户" + }, "QfVedX": { "defaultMessage": "插入分隔线" }, @@ -1637,7 +1689,7 @@ "description": "src/components/ArticleDigest/DropdownActions/PinButton.tsx" }, "SFsQ1E": { - "defaultMessage": "查看回应" + "defaultMessage": "查看评论" }, "SHWkv8": { "defaultMessage": "为什么先采用 Optimism ?", @@ -1680,6 +1732,9 @@ "defaultMessage": "提现", "description": "src/components/Transaction/index.tsx" }, + "Sfql0+": { + "defaultMessage": "恭喜获得七日书大满贯" + }, "SuRTsQ": { "defaultMessage": "注册 ISCN" }, @@ -1711,7 +1766,7 @@ "defaultMessage": "临时密码已过期,请尝试重新发送" }, "TInwt3": { - "defaultMessage": "关闭回应" + "defaultMessage": "关闭评论" }, "TKsfIS": { "defaultMessage": "流星号" @@ -1727,6 +1782,9 @@ "defaultMessage": "付款将由 Stripe 处理,让你的支持不受地域限制", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx" }, + "TZgskS": { + "defaultMessage": "还没有人投稿,晚点再来瞧瞧吧!" + }, "TcTp+J": { "defaultMessage": "图片上传成功" }, @@ -1827,6 +1885,9 @@ "defaultMessage": "相关推荐", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, + "VrK0Q0": { + "defaultMessage": "请选择⋯" + }, "VrOoVf": { "defaultMessage": "Matters 不会透过任何渠道主动询问你的钱包私钥。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -1845,6 +1906,9 @@ "defaultMessage": "最近 3 个月", "description": "src/views/Me/Analytics/SelectPeriod/index.tsx" }, + "W3hNBA": { + "defaultMessage": "七日书大满贯" + }, "W3tqQO": { "defaultMessage": "[图]" }, @@ -2093,6 +2157,9 @@ "defaultMessage": "官方文档", "description": "src/components/Dialogs/ENSDialog/ENSDescription/index.tsx" }, + "aKEiNd": { + "defaultMessage": "错过报名期没关系,仍然可以用晚鸟身份参与,一起书写人生故事。下次早点报名,就有机会获得大满贯徽章!" + }, "aKlTO2": { "defaultMessage": "你还可以添加 {count} 名协作者" }, @@ -2124,6 +2191,9 @@ "ai7kS4": { "defaultMessage": "我的创作" }, + "al5/yQ": { + "defaultMessage": "晚鸟参与通过" + }, "aqX2Bt": { "defaultMessage": "前往首页", "description": "src/views/Callback/UI.tsx" @@ -2160,6 +2230,9 @@ "beLe/F": { "defaultMessage": "广播" }, + "buf5vO": { + "defaultMessage": "活动公告" + }, "c/z318": { "defaultMessage": "邮箱或密码错误", "description": "src/components/Forms/EmailLoginForm/index.tsx" @@ -2275,7 +2348,7 @@ "defaultMessage": "涉及未成年人的色情" }, "eIlMHB": { - "defaultMessage": "允许读者回应本文(开启后无法关闭)" + "defaultMessage": "允许读者评论本文(开启后无法关闭)" }, "eKVBm/": { "defaultMessage": "Optimism 是独立运行的区块链,若你在其他链上已有 USDT 货币,需要将它们转移到 Optimism 网络才能使用,详情参考{link}。" @@ -2319,8 +2392,12 @@ "f24jaP": { "defaultMessage": "信用卡支持需发送财务信息相关邮件,请您先验证电子邮件地址。" }, + "f5jWMJ": { + "defaultMessage": "确认参加", + "description": "src/views/CampaignDetail/Apply/Dialog/index.tsx" + }, "fBzH+2": { - "defaultMessage": "不允许读者回应本文(可通过编辑打开回应功能)" + "defaultMessage": "不允许读者评论本文(可通过编辑打开评论功能)" }, "fDdcbi": { "defaultMessage": "{type}已被原作者删除" @@ -2367,6 +2444,10 @@ "g5pX+a": { "defaultMessage": "关于" }, + "gCafm/": { + "defaultMessage": "晚鸟参与", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "gK6OxL": { "defaultMessage": "留个友善的评论?", "description": "src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx" @@ -2435,6 +2516,10 @@ "defaultMessage": "人追踪", "description": "src/views/TagDetail/Followers/index.tsx" }, + "hfRsLB": { + "defaultMessage": "作者 {count}", + "description": "src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx" + }, "hgtWIO": { "defaultMessage": "作品被关联", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" @@ -2511,6 +2596,10 @@ "defaultMessage": "邮箱注册", "description": "src/components/Forms/SelectAuthMethodForm/NormalFeed.tsx" }, + "jLkKbI": { + "defaultMessage": "报名处理中", + "description": "type:apply" + }, "jiB0Z2": { "defaultMessage": "无法绑定钱包", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -2574,6 +2663,9 @@ "defaultMessage": "确认移出", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "krvjo9": { + "defaultMessage": "活动时间{tz}:" + }, "ksIL/T": { "defaultMessage": "贴心提醒:此钱包地址不同于您用于登录 Matters 的钱包地址" }, @@ -2630,6 +2722,9 @@ "defaultMessage": "评论了", "description": "src/components/Notice/CommentNotice/ArticleNewCommentNotice.tsx" }, + "lfiBVR": { + "defaultMessage": "赞赏公民" + }, "liBHHE": { "defaultMessage": "什么看法,都可以留个友善的评论~" }, @@ -2646,6 +2741,9 @@ "defaultMessage": "围炉描述", "description": "src/components/Forms/CreateCircleForm/Profile.tsx" }, + "m1wKuC": { + "defaultMessage": "参与七日书活动" + }, "m4GG4b": { "defaultMessage": "删除选集" }, @@ -2758,6 +2856,9 @@ "oGiO//": { "defaultMessage": "插入音频" }, + "oLOus+": { + "defaultMessage": "报名申请已递交 🎉" + }, "ob+HDS": { "defaultMessage": "马上逛逛" }, @@ -2772,6 +2873,9 @@ "onDesU": { "defaultMessage": "恭喜成为围炉一员。现在你可以免费浏览围炉内作品,还可以去围炉与大家谈天说地。" }, + "orIq4X": { + "defaultMessage": "支持超过 100 次" + }, "p5qZnJ": { "defaultMessage": "赞赏了", "description": "src/components/Notice/ArticleNotice/ArticleNewAppreciationNotice.tsx" @@ -2968,6 +3072,10 @@ "sy+pv5": { "defaultMessage": "邮箱" }, + "syBMnY": { + "defaultMessage": "位写作者", + "description": "src/views/CampaignDetail/InfoHeader/Participants/index.tsx" + }, "syEQFE": { "defaultMessage": "发布" }, @@ -3241,6 +3349,10 @@ "xkr+zo": { "defaultMessage": "协议" }, + "xl95XN": { + "defaultMessage": "作者", + "description": "src/views/CampaignDetail/SideParticipants/index.tsx" + }, "xmcVZ0": { "defaultMessage": "搜索" }, diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index 630d2718bb..44aa708eb0 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -1,6 +1,6 @@ { "+12veD": { - "defaultMessage": "開啟回應" + "defaultMessage": "開啟評論" }, "+3ny7b": { "defaultMessage": "{totalCount} 人讚賞了作品" @@ -251,7 +251,7 @@ "defaultMessage": "眾聊" }, "20yT4t": { - "defaultMessage": "回應設置", + "defaultMessage": "評論設置", "description": "src/components/Editor/Sidebar/Response/index.tsx" }, "24jhEp": { @@ -383,6 +383,9 @@ "4l6vz1": { "defaultMessage": "複製" }, + "4nHH2x": { + "defaultMessage": "報名成功" + }, "4tqFCR": { "defaultMessage": "作者尚未啓用 USDT 錢包" }, @@ -445,6 +448,9 @@ "5sg7KC": { "defaultMessage": "密碼" }, + "5y5rID": { + "defaultMessage": "我們將盡快處理你的申請,敬請期待活動開跑!" + }, "6+eeJ4": { "defaultMessage": "可使用小寫英文、數字及下劃線", "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" @@ -463,7 +469,7 @@ "defaultMessage": "開始創作" }, "6B+QXo": { - "defaultMessage": "回應設置", + "defaultMessage": "評論設置", "description": "src/components/Editor/ToggleResponse/index.tsx" }, "6BXcdo": { @@ -519,6 +525,9 @@ "6oOCCL": { "defaultMessage": "上傳檔案" }, + "6pc948": { + "defaultMessage": "投稿七日書自由寫" + }, "6q0G5e": { "defaultMessage": "加入成功", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" @@ -582,6 +591,9 @@ "defaultMessage": "暫無評論", "description": "src/components/Empty/EmptyComment.tsx" }, + "8+Z5E9": { + "defaultMessage": "紀念你參與「七日書」並完成七天書寫" + }, "80bF0W": { "defaultMessage": "暫無留言", "description": "src/components/Forms/MomentCommentForm/index.tsx" @@ -657,6 +669,9 @@ "9J0iCw": { "defaultMessage": "已註銷用戶" }, + "9OPnTz": { + "defaultMessage": "系統或連線異常,請刷新頁面後重新點擊晚鳥參與" + }, "9SXN7s": { "defaultMessage": "沒有更多", "description": "src/views/ArticleDetail/Comments/LatestComments/index.tsx" @@ -724,6 +739,10 @@ "defaultMessage": "追蹤", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" }, + "AeIRlL": { + "defaultMessage": "只看文章", + "description": "src/views/Follow/Tabs/index.tsx" + }, "AeVndq": { "defaultMessage": "在動態留言中提及了你 {commentMoment}" }, @@ -776,6 +795,9 @@ "Bc20la": { "defaultMessage": "天" }, + "BhRxYr": { + "defaultMessage": "自由寫紀念徽章" + }, "Bjdw71": { "defaultMessage": "目前總追蹤人數", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -850,6 +872,9 @@ "D9R/ol": { "defaultMessage": "將作品加入圍爐,讓更多人一起參與討論" }, + "D9tEst": { + "defaultMessage": "馬特市建築師" + }, "DP3yqI": { "defaultMessage": "支持人數" }, @@ -945,6 +970,9 @@ "defaultMessage": "你無權限進行該操作", "description": "FORBIDDEN_BY_STATE" }, + "ErV/vT": { + "defaultMessage": "晚鳥參與七日書" + }, "F3zk7E": { "defaultMessage": " (草稿刪除後無法恢復)", "description": "src/components/DraftDigest/Feed/DeleteButton.tsx" @@ -969,6 +997,9 @@ "defaultMessage": "目前非 Optimism 網路,立即切換?", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" }, + "FYeEw1": { + "defaultMessage": "報名期{tz}:" + }, "FaTb0A": { "defaultMessage": "安裝 MetaMask", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -1062,6 +1093,9 @@ "HAlOn1": { "defaultMessage": "用戶名" }, + "HB2VOx": { + "defaultMessage": "系统或连线异常,请刷新页面后重新点击报名参加" + }, "HBxXD/": { "defaultMessage": "版權聲明" }, @@ -1090,6 +1124,10 @@ "HbEL82": { "defaultMessage": "評論已刪除" }, + "HgY+72": { + "defaultMessage": "報名參加", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "HkozYU": { "defaultMessage": "更換錢包", "description": "src/components/Forms/WalletAuthForm/Connect.tsx" @@ -1188,6 +1226,10 @@ "defaultMessage": "被追蹤", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" }, + "JpAsUV": { + "defaultMessage": "更多動態", + "description": "src/views/Follow/Feed/UserPostMomentActivity" + }, "JpS59y": { "defaultMessage": "已接受", "description": "src/views/Circle/Settings/ManageInvitation/Invites/index.tsx" @@ -1324,6 +1366,9 @@ "LnesNr": { "defaultMessage": "移除協作者成功" }, + "LoQ3BF": { + "defaultMessage": "這枚徽章代表你完成了七天寫作,恭喜走完這趟意義非凡的寫作之旅!" + }, "Lp6CiR": { "defaultMessage": "設定 - 其他", "description": "src/views/Me/Settings/Misc/index.tsx" @@ -1535,6 +1580,10 @@ "Pp/0po": { "defaultMessage": "換一批" }, + "Pq/7m5": { + "defaultMessage": "晚鳥參與處理中", + "description": "type:join" + }, "PtV68+": { "defaultMessage": "郵件地址或密碼錯誤", "description": "USER_EMAIL_NOT_FOUND" @@ -1560,6 +1609,9 @@ "defaultMessage": "設置圍爐網址(創建後不可修改)", "description": "src/components/Forms/CreateCircleForm/Init.tsx" }, + "QbjADp": { + "defaultMessage": "種子用戶" + }, "QfVedX": { "defaultMessage": "插入分隔線" }, @@ -1637,7 +1689,7 @@ "description": "src/components/ArticleDigest/DropdownActions/PinButton.tsx" }, "SFsQ1E": { - "defaultMessage": "查看回應" + "defaultMessage": "查看評論" }, "SHWkv8": { "defaultMessage": "為什麼採用 Optimism?", @@ -1680,6 +1732,9 @@ "defaultMessage": "提現", "description": "src/components/Transaction/index.tsx" }, + "Sfql0+": { + "defaultMessage": "恭喜獲得七日書大滿貫" + }, "SuRTsQ": { "defaultMessage": "註冊 ISCN" }, @@ -1711,7 +1766,7 @@ "defaultMessage": "臨時密碼已過期,請嘗試重新發送" }, "TInwt3": { - "defaultMessage": "關閉回應" + "defaultMessage": "關閉評論" }, "TKsfIS": { "defaultMessage": "流星號" @@ -1727,6 +1782,9 @@ "defaultMessage": "付款將由 Stripe 處理,讓你的支持不受地域限制", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx" }, + "TZgskS": { + "defaultMessage": "還沒有人投稿,晚點再來瞧瞧吧!" + }, "TcTp+J": { "defaultMessage": "圖片上傳成功" }, @@ -1827,6 +1885,9 @@ "defaultMessage": "相關推薦", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, + "VrK0Q0": { + "defaultMessage": "請選擇⋯" + }, "VrOoVf": { "defaultMessage": "Matters 不會透過任何渠道主動詢問你的錢包私鑰。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -1845,6 +1906,9 @@ "defaultMessage": "最近 3 個月", "description": "src/views/Me/Analytics/SelectPeriod/index.tsx" }, + "W3hNBA": { + "defaultMessage": "七日書大滿貫" + }, "W3tqQO": { "defaultMessage": "[圖]" }, @@ -2093,6 +2157,9 @@ "defaultMessage": "官方文檔", "description": "src/components/Dialogs/ENSDialog/ENSDescription/index.tsx" }, + "aKEiNd": { + "defaultMessage": "錯過報名期沒關係,仍然可以用晚鳥身份參與,一起書寫人生故事。下次早點報名,就有機會獲得大滿貫徽章!" + }, "aKlTO2": { "defaultMessage": "你還可以添加 {count} 名協作者" }, @@ -2124,6 +2191,9 @@ "ai7kS4": { "defaultMessage": "我的創作" }, + "al5/yQ": { + "defaultMessage": "晚鳥參與通過" + }, "aqX2Bt": { "defaultMessage": "前往首頁", "description": "src/views/Callback/UI.tsx" @@ -2160,6 +2230,9 @@ "beLe/F": { "defaultMessage": "廣播" }, + "buf5vO": { + "defaultMessage": "活動公告" + }, "c/z318": { "defaultMessage": "郵件地址或密碼錯誤", "description": "src/components/Forms/EmailLoginForm/index.tsx" @@ -2275,7 +2348,7 @@ "defaultMessage": "涉及未成年人的色情" }, "eIlMHB": { - "defaultMessage": "允許讀者回應本文(開啟後無法關閉)" + "defaultMessage": "允許讀者評論本文(開啟後無法關閉)" }, "eKVBm/": { "defaultMessage": "Optimism 是獨立運行的區塊鏈,若你在其他鏈上已有 USDT 貨幣,需要將它們轉移到 Optimism 網絡才能使用,詳情參考{link}。" @@ -2319,8 +2392,12 @@ "f24jaP": { "defaultMessage": "信用卡支持需發送財務信息相關郵件,請您先驗證電子郵件地址。" }, + "f5jWMJ": { + "defaultMessage": "確認參加", + "description": "src/views/CampaignDetail/Apply/Dialog/index.tsx" + }, "fBzH+2": { - "defaultMessage": "不允許讀者回應本文(可通過編輯打開回應功能)" + "defaultMessage": "不允許讀者評論本文(可通過編輯打開評論功能)" }, "fDdcbi": { "defaultMessage": "{type}已被原作者刪除" @@ -2367,6 +2444,10 @@ "g5pX+a": { "defaultMessage": "關於" }, + "gCafm/": { + "defaultMessage": "晚鳥參與", + "description": "src/views/CampaignDetail/Apply/Button/index.tsx" + }, "gK6OxL": { "defaultMessage": "留個友善的評論?", "description": "src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx" @@ -2435,6 +2516,10 @@ "defaultMessage": "人追蹤", "description": "src/views/TagDetail/Followers/index.tsx" }, + "hfRsLB": { + "defaultMessage": "作者 {count}", + "description": "src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx" + }, "hgtWIO": { "defaultMessage": "作品被關聯", "description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx" @@ -2511,6 +2596,10 @@ "defaultMessage": "電子郵件註冊", "description": "src/components/Forms/SelectAuthMethodForm/NormalFeed.tsx" }, + "jLkKbI": { + "defaultMessage": "報名處理中", + "description": "type:apply" + }, "jiB0Z2": { "defaultMessage": "無法綁定錢包", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -2574,6 +2663,9 @@ "defaultMessage": "確認移出", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "krvjo9": { + "defaultMessage": "活動時間{tz}:" + }, "ksIL/T": { "defaultMessage": "貼心提醒:此錢包位址不同於您用於登入 Matters 的錢包位址" }, @@ -2630,6 +2722,9 @@ "defaultMessage": "評論了", "description": "src/components/Notice/CommentNotice/ArticleNewCommentNotice.tsx" }, + "lfiBVR": { + "defaultMessage": "讚賞公民" + }, "liBHHE": { "defaultMessage": "什麼看法,都可以留個友善的評論~" }, @@ -2646,6 +2741,9 @@ "defaultMessage": "圍爐描述", "description": "src/components/Forms/CreateCircleForm/Profile.tsx" }, + "m1wKuC": { + "defaultMessage": "參與七日書活動" + }, "m4GG4b": { "defaultMessage": "刪除選集" }, @@ -2758,6 +2856,9 @@ "oGiO//": { "defaultMessage": "插入音訊" }, + "oLOus+": { + "defaultMessage": "報名申請已遞交 🎉" + }, "ob+HDS": { "defaultMessage": "馬上逛逛" }, @@ -2772,6 +2873,9 @@ "onDesU": { "defaultMessage": "恭喜成為圍爐一員。現在你可以免費瀏覽圍爐內作品,還可以去圍爐與大家談天說地。" }, + "orIq4X": { + "defaultMessage": "支持超過 100 次" + }, "p5qZnJ": { "defaultMessage": "讚賞了", "description": "src/components/Notice/ArticleNotice/ArticleNewAppreciationNotice.tsx" @@ -2968,6 +3072,10 @@ "sy+pv5": { "defaultMessage": "電子郵件" }, + "syBMnY": { + "defaultMessage": "位寫作者", + "description": "src/views/CampaignDetail/InfoHeader/Participants/index.tsx" + }, "syEQFE": { "defaultMessage": "發布" }, @@ -3241,6 +3349,10 @@ "xkr+zo": { "defaultMessage": "協議" }, + "xl95XN": { + "defaultMessage": "作者", + "description": "src/views/CampaignDetail/SideParticipants/index.tsx" + }, "xmcVZ0": { "defaultMessage": "搜尋" }, diff --git a/package-lock.json b/package-lock.json index 4fcf23ffd6..b43a7dd40d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matters-web", - "version": "5.1.3", + "version": "5.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "matters-web", - "version": "5.1.3", + "version": "5.2.0", "license": "Apache-2.0", "dependencies": { "@apollo/react-common": "^3.1.3", @@ -52,7 +52,8 @@ "d3-shape": "^2.1.0", "d3-time-format": "^2.3.0", "d3-transition": "^3.0.1", - "date-fns": "^2.30.0", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", "embla-carousel-react": "^7.1.0", "express": "^4.17.1", "file-type": "^16.5.4", @@ -118,7 +119,6 @@ "@types/d3": "^7.0.0", "@types/express": "^4.17.9", "@types/fingerprintjs2": "^2.0.0", - "@types/grecaptcha": "^3.0.4", "@types/js-cookie": "^3.0.3", "@types/jump.js": "^1.0.4", "@types/lodash": "^4.14.195", @@ -138,6 +138,7 @@ "babel-loader": "^9.1.3", "babel-plugin-dynamic-import-node": "^2.3.3", "babel-polyfill": "^6.26.0", + "concurrently": "^8.2.2", "css-has-pseudo": "^6.0.0", "cz-conventional-changelog": "^3.3.0", "eslint": "^8.47.0", @@ -163,7 +164,6 @@ "stylelint-config-recess-order": "^4.2.0", "stylelint-config-standard": "^34.0.0", "stylelint-prettier": "^4.0.0", - "ts-node": "^10.9.1", "typescript": "^5.1.6", "vite-plugin-svgr": "^3.3.0", "vitest": "^0.34.6", @@ -2772,28 +2772,6 @@ "node": ">=v10.22.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.3.tgz", @@ -14866,30 +14844,6 @@ "node": ">=10.13.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true - }, "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -15380,12 +15334,6 @@ "@types/node": "*" } }, - "node_modules/@types/grecaptcha": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/grecaptcha/-/grecaptcha-3.0.4.tgz", - "integrity": "sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg==", - "dev": true - }, "node_modules/@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -18056,12 +18004,6 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -20501,6 +20443,199 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concurrently/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/concurrently/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -20724,12 +20859,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -21820,18 +21949,20 @@ "dev": true }, "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.1.3.tgz", + "integrity": "sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==", + "peerDependencies": { + "date-fns": "^3.0.0" } }, "node_modules/dayjs": { @@ -22572,15 +22703,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -31355,12 +31477,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -40858,9 +40974,9 @@ } }, "node_modules/shell-quote": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", - "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -41207,6 +41323,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -43027,6 +43149,15 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -43103,49 +43234,6 @@ "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==", "dev": true }, - "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/ts-pnp": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", @@ -44275,12 +44363,6 @@ "node": ">=6" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -46538,15 +46620,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -48422,27 +48495,6 @@ "dev": true, "optional": true }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } - } - }, "@csstools/cascade-layer-name-parser": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.3.tgz", @@ -56604,30 +56656,6 @@ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true }, - "@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true - }, "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -57118,12 +57146,6 @@ "@types/node": "*" } }, - "@types/grecaptcha": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/grecaptcha/-/grecaptcha-3.0.4.tgz", - "integrity": "sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg==", - "dev": true - }, "@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -59285,12 +59307,6 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -61185,6 +61201,147 @@ } } }, + "concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -61368,12 +61525,6 @@ "sha.js": "^2.4.8" } }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -62161,12 +62312,14 @@ "dev": true }, "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "requires": { - "@babel/runtime": "^7.21.0" - } + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" + }, + "date-fns-tz": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.1.3.tgz", + "integrity": "sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==" }, "dayjs": { "version": "1.10.7", @@ -62748,12 +62901,6 @@ "dequal": "^2.0.0" } }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -69521,12 +69668,6 @@ "semver": "^6.0.0" } }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -76465,9 +76606,9 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "shell-quote": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", - "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true }, "side-channel": { @@ -76728,6 +76869,12 @@ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", "dev": true }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -78092,6 +78239,12 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -78148,27 +78301,6 @@ "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==", "dev": true }, - "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - } - }, "ts-pnp": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", @@ -78973,12 +79105,6 @@ } } }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -80455,12 +80581,6 @@ "fd-slicer": "~1.1.0" } }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index c850d3a968..23592c8356 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "5.1.3", + "version": "5.2.0", "description": "codebase of Matters' website", "author": "Matters ", "engines": { @@ -10,7 +10,7 @@ "sideEffects": false, "scripts": { "dev": "PORT=\"${PORT:-3000}\"; cross-env NODE_OPTIONS='--no-experimental-fetch --heapsnapshot-signal=SIGUSR2' next -p $PORT", - "test": "npm run test:unit && npm run test:e2e", + "test": "concurrently -c \"auto\" \"npm run test:unit\" \"npm run test:e2e\"", "test:e2e": "playwright test", "test:e2e:prepare": "playwright install --with-deps", "test:unit": "vitest run", @@ -21,7 +21,7 @@ "export": "next export", "format": "prettier --write \"{,!(.next|build|node_modules|coverage|out|lang)/**/*.{js,jsx,ts,tsx,json}}\"", "format:check": "npm run format -- --list-different", - "lint": "npm run lint:ts && npm run lint:css", + "lint": "concurrently -c \"auto\" \"npm run lint:ts\" \"npm run lint:css\"", "lint:ts": "tsc --project tsconfig.json --noEmit && next lint", "lint:ts:fix": "next lint --fix", "lint:css": "stylelint 'src/**/*.css'", @@ -82,7 +82,8 @@ "d3-shape": "^2.1.0", "d3-time-format": "^2.3.0", "d3-transition": "^3.0.1", - "date-fns": "^2.30.0", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", "embla-carousel-react": "^7.1.0", "express": "^4.17.1", "file-type": "^16.5.4", @@ -148,7 +149,6 @@ "@types/d3": "^7.0.0", "@types/express": "^4.17.9", "@types/fingerprintjs2": "^2.0.0", - "@types/grecaptcha": "^3.0.4", "@types/js-cookie": "^3.0.3", "@types/jump.js": "^1.0.4", "@types/lodash": "^4.14.195", @@ -168,6 +168,7 @@ "babel-loader": "^9.1.3", "babel-plugin-dynamic-import-node": "^2.3.3", "babel-polyfill": "^6.26.0", + "concurrently": "^8.2.2", "css-has-pseudo": "^6.0.0", "cz-conventional-changelog": "^3.3.0", "eslint": "^8.47.0", @@ -193,7 +194,6 @@ "stylelint-config-recess-order": "^4.2.0", "stylelint-config-standard": "^34.0.0", "stylelint-prettier": "^4.0.0", - "ts-node": "^10.9.1", "typescript": "^5.1.6", "vite-plugin-svgr": "^3.3.0", "vitest": "^0.34.6", diff --git a/public/static/icons/24px/badge-grand.svg b/public/static/icons/24px/badge-grand.svg new file mode 100644 index 0000000000..554aa277a3 --- /dev/null +++ b/public/static/icons/24px/badge-grand.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/static/icons/24px/badge-nomad1-moon.svg b/public/static/icons/24px/badge-nomad1-moon.svg index fa6fea564f..8a27e18c7c 100644 --- a/public/static/icons/24px/badge-nomad1-moon.svg +++ b/public/static/icons/24px/badge-nomad1-moon.svg @@ -1,12 +1,13 @@ - - + + - - - - - - + + + + + + + diff --git a/public/static/icons/24px/badge-nomad2-star.svg b/public/static/icons/24px/badge-nomad2-star.svg index a2c8bcc7ea..71a85a42fd 100644 --- a/public/static/icons/24px/badge-nomad2-star.svg +++ b/public/static/icons/24px/badge-nomad2-star.svg @@ -1,15 +1,16 @@ - - + + - - - - - - - - - + + + + + + + + + + diff --git a/public/static/icons/24px/badge-nomad3-light.svg b/public/static/icons/24px/badge-nomad3-light.svg index e6e5b61cc4..bf76b6281a 100644 --- a/public/static/icons/24px/badge-nomad3-light.svg +++ b/public/static/icons/24px/badge-nomad3-light.svg @@ -1,18 +1,19 @@ - - + + - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/public/static/icons/24px/badge-nomad4-fire.svg b/public/static/icons/24px/badge-nomad4-fire.svg index 6cbe7f9613..a92568a514 100644 --- a/public/static/icons/24px/badge-nomad4-fire.svg +++ b/public/static/icons/24px/badge-nomad4-fire.svg @@ -1,16 +1,17 @@ - - + + - - - - - - + + + + + + + - + diff --git a/public/static/icons/24px/check.svg b/public/static/icons/24px/check.svg new file mode 100644 index 0000000000..96e2550f0e --- /dev/null +++ b/public/static/icons/24px/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/static/icons/24px/civic-liker.svg b/public/static/icons/24px/civic-liker.svg index d9e5553224..624e6544eb 100644 --- a/public/static/icons/24px/civic-liker.svg +++ b/public/static/icons/24px/civic-liker.svg @@ -1,3 +1,9 @@ - + + + + + + + diff --git a/public/static/icons/24px/golden-motor-award.svg b/public/static/icons/24px/golden-motor-award.svg index b4ca15727d..acb62d1da4 100644 --- a/public/static/icons/24px/golden-motor-award.svg +++ b/public/static/icons/24px/golden-motor-award.svg @@ -1,3 +1,9 @@ - + + + + + + + diff --git a/public/static/icons/24px/matters-architect.svg b/public/static/icons/24px/matters-architect.svg index 125f68922b..5da1bdc259 100644 --- a/public/static/icons/24px/matters-architect.svg +++ b/public/static/icons/24px/matters-architect.svg @@ -1,3 +1,6 @@ - + + + + diff --git a/public/static/icons/24px/read.svg b/public/static/icons/24px/read.svg new file mode 100644 index 0000000000..bcabb20a15 --- /dev/null +++ b/public/static/icons/24px/read.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/static/icons/24px/seed-user.svg b/public/static/icons/24px/seed-user.svg index ad903731e5..390d1c5712 100644 --- a/public/static/icons/24px/seed-user.svg +++ b/public/static/icons/24px/seed-user.svg @@ -1,3 +1,9 @@ - + + + + + + + diff --git a/public/static/icons/24px/traveloggers.svg b/public/static/icons/24px/traveloggers.svg index 4647dbdb9c..9979526d2f 100644 --- a/public/static/icons/24px/traveloggers.svg +++ b/public/static/icons/24px/traveloggers.svg @@ -1,12 +1,19 @@ - - + + + + + + + + + - + - + diff --git a/public/static/icons/48px/badge-grand.svg b/public/static/icons/48px/badge-grand.svg new file mode 100644 index 0000000000..3542a6fc7e --- /dev/null +++ b/public/static/icons/48px/badge-grand.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/images/badge-grand-background.svg b/public/static/images/badge-grand-background.svg new file mode 100644 index 0000000000..7f2214b666 --- /dev/null +++ b/public/static/images/badge-grand-background.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/common/enums/events.ts b/src/common/enums/events.ts index 9264c11612..c7376dbb5a 100644 --- a/src/common/enums/events.ts +++ b/src/common/enums/events.ts @@ -26,7 +26,8 @@ export const OPEN_UNIVERSAL_AUTH_DIALOG = 'openUniversalAuthDialog' export const CLOSE_ACTIVE_DIALOG = 'closeActiveDialog' export const OPEN_SUBSCRIBE_CIRCLE_DIALOG = 'openSubscribeCircleDialog' export const OPEN_SET_USER_NAME_DIALOG = 'openSetUserNameDialog' -export const OPEN_SHOW_NOMAD_BADGE_DIALOG = 'openShowNomadBadgeDialog' +export const OPEN_NOMAD_BADGE_DIALOG = 'openNomadBadgeDialog' +export const OPEN_GRAND_BADGE_DIALOG = 'openGrandBadgeDialog' export const OPEN_COMMENT_DETAIL_DIALOG = 'openCommentDetailDialog' export const OPEN_SET_PAYMENT_PASSWORD_DIALOG = 'openSetPaymentPasswordDialog' export const CLOSE_SET_PAYMENT_PASSWORD_DIALOG = 'closeSetPaymentPasswordDialog' diff --git a/src/common/enums/route.ts b/src/common/enums/route.ts index 43091e35d1..1b72bccdf5 100644 --- a/src/common/enums/route.ts +++ b/src/common/enums/route.ts @@ -21,6 +21,8 @@ type ROUTE_KEY = | 'ARTICLE_DETAIL' | 'ARTICLE_DETAIL_EDIT' | 'ARTICLE_DETAIL_HISTORY' + // Moment + | 'MOMENT_DETAIL' // User | 'USER_ARTICLES' | 'USER_COLLECTIONS' @@ -104,6 +106,9 @@ export const ROUTES: { { key: 'ARTICLE_DETAIL_EDIT', pathname: '/a/[shortHash]/edit' }, { key: 'ARTICLE_DETAIL_HISTORY', pathname: '/a/[shortHash]/history' }, + // Moment + { key: 'MOMENT_DETAIL', pathname: '/m/[shortHash]' }, + // Circle { key: 'CIRCLE_DETAIL', pathname: '/[name]' }, { key: 'CIRCLE_DISCUSSION', pathname: '/[name]/discussion' }, diff --git a/src/common/enums/url.ts b/src/common/enums/url.ts index 563b03731b..cd1c115ab8 100644 --- a/src/common/enums/url.ts +++ b/src/common/enums/url.ts @@ -4,6 +4,11 @@ export const URL_FRAGMENT = { export const URL_USER_PROFILE = { OPEN_NOMAD_BADGE_DIALOG: { key: 'dialog', value: 'nomad-badge' }, + OPEN_GRAND_BADGE_DIALOG: { key: 'dialog', value: 'grand-badge' }, + GRAND_BADGE_DIALOG_STEP: { + key: 'step', + value: 'congrats', + }, } export const URL_ME_SETTINGS = { diff --git a/src/common/styles/variables/colors.css b/src/common/styles/variables/colors.css index 1b73e236f8..f21de02661 100644 --- a/src/common/styles/variables/colors.css +++ b/src/common/styles/variables/colors.css @@ -32,6 +32,9 @@ --color-insufficient-red: #ffe8e8; --color-red-lighter: #fae7e3; /* FIXME: undocumented */ --color-analytics-red: #c25050; /* FIXME: undocumented */ + --color-free-write-blue-bg: #f0f9fe; + --color-free-write-blue: #1999d0; + --color-free-write-blue-border: #d6ebf7; /* others */ --color-noir: #000; diff --git a/src/common/utils/analytics.ts b/src/common/utils/analytics.ts index 5d1d181496..3b684b94be 100644 --- a/src/common/utils/analytics.ts +++ b/src/common/utils/analytics.ts @@ -82,6 +82,7 @@ export interface ClickButtonProp { | 'edit_support_copy' | 'history_version' | 'ipfs' + | 'campaign_detail_entrance' | 'edit' | 'edited' | 'appreciate' @@ -99,6 +100,8 @@ export interface ClickButtonProp { | 'hottest' | 'icymi' | 'newest' + | 'campaign_detail_link' + | `campaign_detail_tab_${string}` pageType?: PageType pageComponent?: PageComponent } @@ -252,6 +255,7 @@ export type ActivityType = | 'UserFollowUserActivity' | 'UserDonateArticleActivity' | 'UserBookmarkArticleActivity' + | 'UserPostMomentActivity' | 'UserAddArticleTagActivity' | 'RecommendArticleActivity' | 'ArticleRecommendationActivity' @@ -300,6 +304,7 @@ type ArticleFeedType = | 'article_detail_author_sidebar_collection' | 'article_detail_author_sidebar_author' | 'article_detail_author_sidebar_recommendation' + | `campaign_detail_${string}` type CollectionFeedType = | 'user_collection' @@ -356,6 +361,7 @@ type PageType = | 'user_profile' | 'circle_detail' | 'edit_draft' + | 'campaign_detail' type PageComponent = | 'home_feed_tab' diff --git a/src/common/utils/comment.ts b/src/common/utils/comment/index.ts similarity index 75% rename from src/common/utils/comment.ts rename to src/common/utils/comment/index.ts index d63ca50662..4cc1f00005 100644 --- a/src/common/utils/comment.ts +++ b/src/common/utils/comment/index.ts @@ -3,6 +3,8 @@ import _has from 'lodash/has' import { CommentState } from '~/gql/graphql' +import styles from './styles.module.css' + /** * Filter out comment that banned/archived and hasn't descendants * @@ -85,3 +87,26 @@ export function trimCommentContent(content: string) { ) return trimContent } + +export const highlightComment = ( + targetElement: HTMLElement, + isParentComment?: boolean, + fullSpacing?: boolean +) => { + const activeParentCommentClass = fullSpacing + ? styles.activeParentCommentFullSpacing + : styles.activeParentComment + targetElement.classList.add(styles.activeBgColor) + if (isParentComment) { + targetElement.classList.add(activeParentCommentClass) + } + + const removeHighlight = () => { + targetElement.classList.remove(styles.activeBgColor) + if (isParentComment) { + targetElement.classList.remove(activeParentCommentClass) + } + } + + setTimeout(removeHighlight, 5000) +} diff --git a/src/common/utils/comment/styles.module.css b/src/common/utils/comment/styles.module.css new file mode 100644 index 0000000000..6e0539c9d9 --- /dev/null +++ b/src/common/utils/comment/styles.module.css @@ -0,0 +1,30 @@ +.activeBgColor { + animation: comment-background-fade-out 1.6s ease-out 0s; +} + +.activeParentCommentFullSpacing { + padding: var(--sp16) !important; + margin: calc(-1 * var(--sp16)); +} + +.activeParentComment { + padding-right: var(--sp16) !important; + padding-left: var(--sp16) !important; + margin-right: calc(-1 * var(--sp16)); + margin-left: calc(-1 * var(--sp16)); +} + +@keyframes comment-background-fade-out { + 0% { + background: var(--color-green-lighter); + } + + /* 1s later */ + 62.5% { + background: var(--color-green-lighter); + } + + 100% { + background: var(--color-white); + } +} diff --git a/src/common/utils/datetime/absolute.test.ts b/src/common/utils/datetime/absolute.test.ts index 35c1dca2cd..c27ba50025 100644 --- a/src/common/utils/datetime/absolute.test.ts +++ b/src/common/utils/datetime/absolute.test.ts @@ -9,53 +9,87 @@ beforeEach(() => { describe('utils/datetime/absolute', () => { it('should parse a string date', () => { const date = '2022-01-01' - const result = toAbsoluteDate(date, 'en') + const result = toAbsoluteDate({ date: date, lang: 'en' }) expect(typeof result).toBe('string') }) it("should format this year's date correctly", () => { - const result = toAbsoluteDate(new Date('2023-01-01'), 'en') - expect(result).toBe('Jan 1') + expect(toAbsoluteDate({ date: new Date('2023-01-01'), lang: 'en' })).toBe( + 'Jan 1' + ) + + expect( + toAbsoluteDate({ + date: new Date('2023-01-01'), + lang: 'en', + optionalYear: false, + }) + ).toBe('Jan 1, 2023') }) it('should format a date in the past correctly', () => { const date = new Date('2021-01-01') - const result = toAbsoluteDate(date, 'en') + const result = toAbsoluteDate({ date: date, lang: 'en' }) expect(result).toBe('Jan 1, 2021') }) it('should format a date in the future correctly', () => { const date = new Date('2025-01-01') - const result = toAbsoluteDate(date, 'en') + const result = toAbsoluteDate({ date: date, lang: 'en' }) expect(result).toContain(date.getFullYear()) }) it('should format different types of dates correctly', () => { // string const stringDate = '2021-01-01' - const stringResult = toAbsoluteDate(stringDate, 'en') + const stringResult = toAbsoluteDate({ date: stringDate, lang: 'en' }) expect(stringResult).toBe('Jan 1, 2021') // number const numberDate = 1609459200000 - const numberResult = toAbsoluteDate(numberDate, 'en') + const numberResult = toAbsoluteDate({ date: numberDate, lang: 'en' }) expect(numberResult).toBe('Jan 1, 2021') // Date const date = new Date('2021-01-01') - const dateResult = toAbsoluteDate(date, 'en') + const dateResult = toAbsoluteDate({ date: date, lang: 'en' }) expect(dateResult).toBe('Jan 1, 2021') }) - it('should format truncated time for this year', () => { - const stringDate = '2023-07-01' - const stringResult = toAbsoluteDate(stringDate, 'en', true) - expect(stringResult).toBe('07-01') + it('should format a date in UTC+8 correctly', () => { + const date = new Date('2021-01-01T17:00:00.000Z') + + // UTC+8 + expect(toAbsoluteDate({ date: date, lang: 'en', utc8: true })).toBe( + 'Jan 2, 2021' + ) + }) +}) + +describe('utils/datetime/absolute/dateISO', () => { + it('should parse a string date', () => { + const date = '2022-01-01' + const result = toAbsoluteDate.dateISO(date) + expect(typeof result).toBe('string') + }) + + it('should format a date correctly', () => { + const date = new Date('2021-01-01') + const result = toAbsoluteDate.dateISO(date) + expect(result).toBe('2021-01-01') + }) +}) + +describe('utils/datetime/absolute/timeISO', () => { + it('should parse a string date', () => { + const date = '2022-01-01T12:34:56' + const result = toAbsoluteDate.timeISO(date) + expect(typeof result).toBe('string') }) - it('should format truncated time for previous years', () => { - const stringDate = '2021-07-01' - const stringResult = toAbsoluteDate(stringDate, 'en', true) - expect(stringResult).toBe('2021-07-01') + it('should format a date correctly', () => { + const date = new Date('2021-01-01T12:34:56') + const result = toAbsoluteDate.timeISO(date) + expect(result).toBe('12:34') }) }) diff --git a/src/common/utils/datetime/absolute.ts b/src/common/utils/datetime/absolute.ts index 2d27258fb8..4534217b22 100644 --- a/src/common/utils/datetime/absolute.ts +++ b/src/common/utils/datetime/absolute.ts @@ -1,6 +1,5 @@ -import format from 'date-fns/format' -import isThisYear from 'date-fns/isThisYear' -import parseISO from 'date-fns/parseISO' +import { format, isThisYear, parseISO } from 'date-fns' +import { formatInTimeZone } from 'date-fns-tz' const FORMATS = { zh_hant: { @@ -17,35 +16,47 @@ const FORMATS = { }, } as const -const TRUNC_FORMATS = { - absoluteTruncatedThisYear: 'MM-dd', - absoluteTruncatedFull: 'yyyy-MM-dd', -} as const +const absolute = ({ + date, + lang = 'zh_hant', + optionalYear = true, + utc8, +}: { + date: Date | string | number + lang: Language + optionalYear?: boolean + utc8?: boolean +}) => { + if (typeof date === 'string') { + date = parseISO(date) + } + + const pattern = + optionalYear && isThisYear(date) + ? FORMATS[lang].absoluteThisYear + : FORMATS[lang].absoluteFull + + if (utc8) { + return formatInTimeZone(date, 'Asia/Hong_Kong', pattern) + } -const absolute = ( - date: Date | string | number, - lang: Language = 'zh_hant', - isTruncated: boolean = false -) => { + return format(date, pattern) +} + +absolute.dateISO = (date: Date | string | number) => { if (typeof date === 'string') { date = parseISO(date) } - if (isThisYear(date)) { - return format( - date, - isTruncated - ? TRUNC_FORMATS.absoluteTruncatedThisYear - : FORMATS[lang].absoluteThisYear - ) + return format(date, 'yyyy-MM-dd') +} + +absolute.timeISO = (date: Date | string | number) => { + if (typeof date === 'string') { + date = parseISO(date) } - return format( - date, - isTruncated - ? TRUNC_FORMATS.absoluteTruncatedFull - : FORMATS[lang].absoluteFull - ) + return format(date, 'HH:mm') } export default absolute diff --git a/src/common/utils/datetime/index.ts b/src/common/utils/datetime/index.ts index edef44c25d..59968cf564 100644 --- a/src/common/utils/datetime/index.ts +++ b/src/common/utils/datetime/index.ts @@ -1,5 +1,13 @@ +import absolute from './absolute' import relative from './relative' export const datetimeFormat = { + absolute, relative, } + +export const isUTC8 = () => { + const timezoneOffset = new Date().getTimezoneOffset() + const utc8Offset = -480 + return timezoneOffset === utc8Offset +} diff --git a/src/common/utils/datetime/relative.test.ts b/src/common/utils/datetime/relative.test.ts index e9292969bb..de3162d735 100644 --- a/src/common/utils/datetime/relative.test.ts +++ b/src/common/utils/datetime/relative.test.ts @@ -57,13 +57,4 @@ describe('utils/datetime/relative', () => { const dateResult = toRelativeDate(date, 'en') expect(dateResult).toBe('Jan 1, 2021') }) - - it('should format a date more than 2 minutes but truncated', () => { - expect(toRelativeDate(new Date(2023, 6, 1, 8, 27), 'en', true)).toBe('3m') - }) - - it('should format a date within 24 hours but truncated', () => { - expect(toRelativeDate(new Date(2023, 6, 1, 7, 20), 'en', true)).toBe('1h') - expect(toRelativeDate(new Date(2023, 6, 1, 5, 20), 'en', true)).toBe('3h') - }) }) diff --git a/src/common/utils/datetime/relative.ts b/src/common/utils/datetime/relative.ts index 8a476f825b..c4d404e6d6 100644 --- a/src/common/utils/datetime/relative.ts +++ b/src/common/utils/datetime/relative.ts @@ -1,8 +1,10 @@ -import differenceInHours from 'date-fns/differenceInHours' -import differenceInMinutes from 'date-fns/differenceInMinutes' -import isThisHour from 'date-fns/isThisHour' -import isToday from 'date-fns/isToday' -import parseISO from 'date-fns/parseISO' +import { + differenceInHours, + differenceInMinutes, + isThisHour, + isToday, + parseISO, +} from 'date-fns' import absolute from './absolute' @@ -32,13 +34,11 @@ const DIFFS = { * * @param {Date|string|number} date - input date * @param {Language} lang - switch format based on language - * @param {boolean} isTruncated - `Feed needs truncated datetime * @returns {string} */ const relative = ( date: Date | string | number, - lang: Language = 'zh_hant', - isTruncated: boolean = false + lang: Language = 'zh_hant' ): string => { if (typeof date === 'string') { date = parseISO(date) @@ -51,18 +51,15 @@ const relative = ( if (isThisHour(date)) { const diffMins = differenceInMinutes(new Date(), date) - return diffMins + (isTruncated ? 'm' : DIFFS[lang]['minutesAgo']) + return diffMins + DIFFS[lang]['minutesAgo'] } if (isToday(date)) { const diffHrs = differenceInHours(new Date(), date) || 1 - return ( - diffHrs + - (isTruncated ? 'h' : DIFFS[lang][diffHrs === 1 ? 'hourAgo' : 'hoursAgo']) - ) + return diffHrs + DIFFS[lang][diffHrs === 1 ? 'hourAgo' : 'hoursAgo'] } - return absolute(date, lang, isTruncated) + return absolute({ date, lang }) } export default relative diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index 668f36f009..b06e69234a 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -30,6 +30,11 @@ interface TagArgs { // feedType?: string } +interface CampaignArgs { + id: string + shortHash: string +} + interface CommentArgs { id: string type: 'article' | 'circleDiscussion' | 'circleBroadcast' | 'moment' // comment type: article/discussion/broadcast @@ -74,6 +79,10 @@ type ToPathArgs = tag: TagArgs feedType?: string } + | { + page: 'campaignDetail' + campaign: CampaignArgs + } | { page: 'userProfile' | 'userCollections' userName: string @@ -209,6 +218,10 @@ export const toPath = ( href = `/tags/${numberId}-${name}` break } + case 'campaignDetail': { + href = `/e/${args.campaign.shortHash}` + break + } case 'userProfile': { href = `/@${args.userName}` break diff --git a/src/common/utils/text/article.ts b/src/common/utils/text/article.ts index d54bfd20ba..18be334ac4 100644 --- a/src/common/utils/text/article.ts +++ b/src/common/utils/text/article.ts @@ -37,7 +37,8 @@ export const stripHtml = ( export const makeSummary = (html: string, length = 140, buffer = 20) => { // split on sentence breaks const sections = stripHtml(html, '', ' ') - .replace(/([?!。?!]|(\.\s))\s*/g, '$1|') + .replace(/&[^;]+;/g, ' ') // remove html entities + .replace(/([?!。?!]|(\.\s))\s*/g, '$1|') // split on sentence breaks .split('|') // grow summary within buffer diff --git a/src/common/utils/validator.ts b/src/common/utils/validator.ts index 2468c4084e..54c75b00c4 100644 --- a/src/common/utils/validator.ts +++ b/src/common/utils/validator.ts @@ -53,3 +53,14 @@ export const isValidPaymentPointer = (paymentPointer: string): boolean => paymentPointer.startsWith('$') export const hasUpperCase = (str: string) => str.toLowerCase() !== str + +type NoticeNode = { + __typename?: string + id?: string +} + +export const shouldRenderNode = ( + node: NoticeNode, + renderableTypes: Set +): node is NoticeNode => + node.__typename !== undefined && renderableTypes.has(node.__typename) diff --git a/src/components/ArticleDigest/Feed/FooterActions/index.tsx b/src/components/ArticleDigest/Feed/FooterActions/index.tsx index 58f20e4db7..872eec1dca 100644 --- a/src/components/ArticleDigest/Feed/FooterActions/index.tsx +++ b/src/components/ArticleDigest/Feed/FooterActions/index.tsx @@ -37,34 +37,36 @@ const FooterActions = ({ return (
- {includesMetaData && ( -
- {hasReadTime && } +
+ {includesMetaData && ( + <> + {hasReadTime && } - {hasDonationCount && } + {hasDonationCount && } - {tag} + {tag} - {hasCircle && circle && ( - - ) : null - } - placement="left" - spacing={4} - > - - - )} -
- )} + {hasCircle && circle && ( + + ) : null + } + placement="left" + spacing={4} + > + + + )} + + )} +
{viewer.isAuthed && ( diff --git a/src/components/ArticleDigest/Feed/index.tsx b/src/components/ArticleDigest/Feed/index.tsx index ed67978486..1886ca135c 100644 --- a/src/components/ArticleDigest/Feed/index.tsx +++ b/src/components/ArticleDigest/Feed/index.tsx @@ -36,6 +36,7 @@ export type ArticleDigestFeedProps = { Partial header?: React.ReactNode collectionId?: string + excludesTimeStamp?: boolean // this is only for timestamp next to the profile } & ArticleDigestFeedControls & FooterActionsProps @@ -55,6 +56,7 @@ const BaseArticleDigestFeed = ({ hasReadTime, hasDonationCount, includesMetaData, + excludesTimeStamp, ...controls }: ArticleDigestFeedProps) => { const { author, summary } = article @@ -100,10 +102,14 @@ const BaseArticleDigestFeed = ({ hasDisplayName onClick={onClickAuthor} /> - + {!excludesTimeStamp && ( + + )}
)} - + {!excludesTimeStamp && ( + + )} )}
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 8bb3f87357..cd6d7519f9 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -101,6 +101,7 @@ export type ButtonBgColor = Extract< type ButtonBgActiveColor = Extract< ButtonColor, | 'greyLighter' + | 'greyLight' | 'greenLighter' | 'greyLighterActive' | 'green' @@ -268,8 +269,7 @@ export const Button: React.FC> = ...(testId ? { ['data-test-id']: testId } : {}), } - // content - const contentStyle = { + const sizeStyle = { width: (!isTransparent && width) || undefined, height: (!isTransparent && height) || undefined, } @@ -284,8 +284,8 @@ export const Button: React.FC> = // span if (is === 'span') { return ( -
-
+ +
{children}
@@ -296,8 +296,13 @@ export const Button: React.FC> = // anchor if (htmlHref) { return ( - -
+ +
{children}
@@ -309,8 +314,8 @@ export const Button: React.FC> = if (href) { return ( -
-
+ +
{children}
@@ -321,8 +326,8 @@ export const Button: React.FC> = // button return ( - )} - + diff --git a/src/components/Editor/Comment/index.tsx b/src/components/Editor/Comment/index.tsx index cb9949b542..85c17d3d61 100644 --- a/src/components/Editor/Comment/index.tsx +++ b/src/components/Editor/Comment/index.tsx @@ -36,13 +36,15 @@ const CommentEditor: React.FC = ({ const intl = useIntl() const { setActiveEditor, setFallbackEditor } = useCommentEditorContext() + placeholder = + placeholder || + intl.formatMessage({ + id: 'liBHHE', + defaultMessage: 'Any thoughts? Leave a kind comment~', + }) + const editor = useCommentEditor({ - placeholder: - placeholder || - intl.formatMessage({ - id: 'liBHHE', - defaultMessage: 'Any thoughts? Leave a kind comment~', - }), + placeholder, content: content || '', onUpdate: async ({ editor, transaction }) => { const content = editor.getHTML() @@ -67,6 +69,16 @@ const CommentEditor: React.FC = ({ } }, [editor]) + useEffect(() => { + if (!editor) return + + editor.extensionManager.extensions.filter( + (extension) => extension.name === 'placeholder' + )[0].options['placeholder'] = placeholder + + editor.view.dispatch(editor.state.tr) + }, [editor, placeholder]) + return (
any +} + +export const getSelectCampaign = ({ + applied, + attached, + createdAt, +}: { + applied?: EditorSelectCampaignFragment + attached: Array<{ campaign: { id: string }; stage: { id: string } }> + createdAt: string // draft or article creation time +}) => { + const { start } = applied?.writingPeriod || {} + const isCampaignStarted = !!start && new Date(createdAt) >= new Date(start) + const isCampaignActive = applied?.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 + + return { + appliedCampaign, + selectedStage, + } +} + +const SelectCampaign = ({ + appliedCampaign, + selectedStage, + editCampaign, +}: SelectCampaignProps) => { + const { lang } = useContext(LanguageContext) + const RESET_OPTION = { + name: , + value: undefined, + selected: !selectedStage, + } + const now = new Date() + const availableStages = appliedCampaign.stages.filter((s) => { + const period = s.period + + if (!period) return false + + return now >= new Date(period.start) + }) + + 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" + /> + ) +} + +SelectCampaign.fragments = gql` + fragment EditorSelectCampaign on WritingChallenge { + id + state + writingPeriod { + start + end + } + stages { + id + name + period { + start + end + } + } + } +` + +export default SelectCampaign diff --git a/src/components/Editor/SettingsDialog/List/index.tsx b/src/components/Editor/SettingsDialog/List/index.tsx index 29fb2c9be2..da4fe75eb5 100644 --- a/src/components/Editor/SettingsDialog/List/index.tsx +++ b/src/components/Editor/SettingsDialog/List/index.tsx @@ -4,6 +4,7 @@ import { Dialog } from '~/components' import { SetPublishISCNProps } from '~/components/Editor' import ListItem from '../../ListItem' +import SelectCampaign, { SelectCampaignProps } from '../../SelectCampaign' import { Step } from '../../SettingsDialog' import ToggleAccess, { ToggleAccessProps } from '../../ToggleAccess' import ToggleResponse, { ToggleResponseProps } from '../../ToggleResponse' @@ -31,7 +32,8 @@ export type SettingsListDialogProps = { } & SettingsListDialogButtons & ToggleResponseProps & ToggleAccessProps & - SetPublishISCNProps + SetPublishISCNProps & + Partial const SettingsList = ({ saving, @@ -51,6 +53,10 @@ const SettingsList = ({ collectionCount, tagsCount, + appliedCampaign, + selectedStage, + editCampaign, + canComment, toggleComment, disableChangeCanComment, @@ -102,6 +108,36 @@ const SettingsList = ({ )} + {appliedCampaign && editCampaign && ( +
+

+ +

+ +
+ )} + + } + subTitle={ + + } + hint + onClick={() => forward('cover')} + > + + + } subTitle={ @@ -123,20 +159,6 @@ const SettingsList = ({ - } - subTitle={ - - } - hint - onClick={() => forward('cover')} - > - - - diff --git a/src/components/Editor/SettingsDialog/List/styles.module.css b/src/components/Editor/SettingsDialog/List/styles.module.css index c3db032583..a19f96799d 100644 --- a/src/components/Editor/SettingsDialog/List/styles.module.css +++ b/src/components/Editor/SettingsDialog/List/styles.module.css @@ -2,6 +2,12 @@ overflow-y: auto; } +.campaign, .response { padding: var(--sp16); } + +.title { + padding-bottom: var(--sp16); + font-size: var(--font-size-base); +} diff --git a/src/components/Editor/SettingsDialog/index.tsx b/src/components/Editor/SettingsDialog/index.tsx index c516fe06cc..4b11236725 100644 --- a/src/components/Editor/SettingsDialog/index.tsx +++ b/src/components/Editor/SettingsDialog/index.tsx @@ -19,6 +19,7 @@ import { } from '~/gql/graphql' import ArticleCustomStagingArea from '../ArticleCustomStagingArea' +import { SelectCampaignProps } from '../SelectCampaign' import TagCustomStagingArea from '../TagCustomStagingArea' import SettingsList, { SettingsListDialogButtons } from './List' @@ -50,7 +51,8 @@ export type EditorSettingsDialogProps = { ToggleResponseProps & SetPublishISCNProps & SettingsListDialogButtons & - Partial + Partial & + Partial const DynamicEditorSearchSelectForm = dynamic( () => import('~/components/Forms/EditorSearchSelectForm'), @@ -111,6 +113,10 @@ const BaseEditorSettingsDialog = ({ togglePublishISCN, iscnPublishSaving, + appliedCampaign, + selectedStage, + editCampaign, + canComment, toggleComment, @@ -176,6 +182,12 @@ const BaseEditorSettingsDialog = ({ }, } + const campaignProps: Partial = { + appliedCampaign, + selectedStage, + editCampaign, + } + const responseProps: ToggleResponseProps = { canComment, toggleComment, @@ -207,6 +219,7 @@ const BaseEditorSettingsDialog = ({ collectionCount={collection.length} tagsCount={tags.length} {...accessProps} + {...campaignProps} {...responseProps} /> )} diff --git a/src/components/Editor/Sidebar/Box/index.tsx b/src/components/Editor/Sidebar/Box/index.tsx index 8adb38fdaf..1cf268ce70 100644 --- a/src/components/Editor/Sidebar/Box/index.tsx +++ b/src/components/Editor/Sidebar/Box/index.tsx @@ -12,6 +12,7 @@ interface BoxProps { onClick?: () => any disabled?: boolean footerSpacing?: boolean + borderColor?: 'freeWriteBlue' } const Box: React.FC> = ({ @@ -21,11 +22,13 @@ const Box: React.FC> = ({ onClick, disabled, footerSpacing = true, + borderColor, children, }) => { const boxClasses = classNames({ [styles.box]: true, [styles.footerSpacing]: !!footerSpacing, + [styles.freeWriteBlue]: borderColor === 'freeWriteBlue', 'u-area-disable': disabled, }) diff --git a/src/components/Editor/Sidebar/Box/styles.module.css b/src/components/Editor/Sidebar/Box/styles.module.css index 2e4938935e..d6cca9f296 100644 --- a/src/components/Editor/Sidebar/Box/styles.module.css +++ b/src/components/Editor/Sidebar/Box/styles.module.css @@ -12,6 +12,10 @@ } } +.freeWriteBlue { + border-color: var(--color-free-write-blue-border); +} + .footerSpacing { padding-bottom: var(--sp16); } diff --git a/src/components/Editor/Sidebar/Campaign/index.tsx b/src/components/Editor/Sidebar/Campaign/index.tsx new file mode 100644 index 0000000000..743d94d197 --- /dev/null +++ b/src/components/Editor/Sidebar/Campaign/index.tsx @@ -0,0 +1,30 @@ +import { FormattedMessage } from 'react-intl' + +import { ReactComponent as IconRead } from '@/public/static/icons/24px/read.svg' +import { Icon } from '~/components' + +import SelectCampaign, { SelectCampaignProps } from '../../SelectCampaign' +import Box from '../Box' +import styles from './styles.module.css' + +const SidebarCampaign: React.FC> = (props) => { + if (!props.appliedCampaign || !props.editCampaign) { + return null + } + + return ( + } + title={ + + } + borderColor="freeWriteBlue" + > +
+ +
+
+ ) +} + +export default SidebarCampaign diff --git a/src/components/Editor/Sidebar/Campaign/styles.module.css b/src/components/Editor/Sidebar/Campaign/styles.module.css new file mode 100644 index 0000000000..e8423878bb --- /dev/null +++ b/src/components/Editor/Sidebar/Campaign/styles.module.css @@ -0,0 +1,3 @@ +.container { + padding: var(--sp8) var(--sp16) 0; +} diff --git a/src/components/Editor/Sidebar/Management/index.tsx b/src/components/Editor/Sidebar/Management/index.tsx index 83a7e1c73e..fda4e99589 100644 --- a/src/components/Editor/Sidebar/Management/index.tsx +++ b/src/components/Editor/Sidebar/Management/index.tsx @@ -19,7 +19,7 @@ const SidebarManagement: React.FC = (props) => { } >
- +
) diff --git a/src/components/Editor/Sidebar/index.tsx b/src/components/Editor/Sidebar/index.tsx index 340cb7944e..a9da63416c 100644 --- a/src/components/Editor/Sidebar/index.tsx +++ b/src/components/Editor/Sidebar/index.tsx @@ -1,3 +1,4 @@ +import SidebarCampaign from './Campaign' import Collection from './Collection' import Cover from './Cover' import Management from './Management' @@ -10,6 +11,7 @@ const Sidebar = { Collection, Management, Response: SidebarArticleResponse, + Campaign: SidebarCampaign, } export default Sidebar diff --git a/src/components/Editor/ToggleAccess/SelectLicense/index.tsx b/src/components/Editor/ToggleAccess/SelectLicense/index.tsx index 241c1d7f45..7a6951c797 100644 --- a/src/components/Editor/ToggleAccess/SelectLicense/index.tsx +++ b/src/components/Editor/ToggleAccess/SelectLicense/index.tsx @@ -112,10 +112,9 @@ const SelectLicense = ({ isInCircle, license, onChange }: Props) => { const cc4link = 'https://creativecommons.org/licenses/by-nc-nd/4.0/' return ( - name="select-license" label={} - title={} onChange={(option) => onChange(option.value)} options={options.map((value) => { const extraDesc = LICENSE_TEXT[isInCircle ? 1 : 0][value].extra[lang] diff --git a/src/components/Editor/ToggleAccess/index.tsx b/src/components/Editor/ToggleAccess/index.tsx index f2eb1c4aa5..2c89a7bede 100644 --- a/src/components/Editor/ToggleAccess/index.tsx +++ b/src/components/Editor/ToggleAccess/index.tsx @@ -4,6 +4,7 @@ import { FormattedMessage, useIntl } from 'react-intl' import { ReactComponent as IconRight } from '@/public/static/icons/24px/right.svg' import { ReactComponent as IconSquareChecked } from '@/public/static/icons/square-checked.svg' +import { capitalizeFirstLetter } from '~/common/utils' import { CircleDigest, Icon, Switch, ViewerContext } from '~/components' import { ArticleAccessType, @@ -51,7 +52,7 @@ export type ToggleAccessProps = { togglePublishISCN: (iscnPublish: boolean) => void iscnPublishSaving: boolean - compact?: boolean + theme?: 'sidebar' | 'bottomBar' } const ToggleAccess: React.FC = ({ @@ -74,7 +75,7 @@ const ToggleAccess: React.FC = ({ togglePublishISCN, iscnPublishSaving, - compact, + theme, }) => { const intl = useIntl() const content = draft ? draft : article @@ -85,7 +86,7 @@ const ToggleAccess: React.FC = ({
{canToggleCircle && ( @@ -154,7 +155,7 @@ const ToggleAccess: React.FC = ({
- {compact ? ( + {theme ? ( +
+ +
+ + )} +
diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index dc6600cadb..1558e3056a 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -1,5 +1,5 @@ import { useLazyQuery } from '@apollo/react-hooks' -import formatISO from 'date-fns/formatISO' +import { formatISO } from 'date-fns' import dynamic from 'next/dynamic' import { useContext, useEffect, useState } from 'react' import { FormattedMessage } from 'react-intl' diff --git a/src/views/CampaignDetail/Apply/Button/index.tsx b/src/views/CampaignDetail/Apply/Button/index.tsx new file mode 100644 index 0000000000..34b742ce54 --- /dev/null +++ b/src/views/CampaignDetail/Apply/Button/index.tsx @@ -0,0 +1,156 @@ +import { useContext } from 'react' +import { FormattedMessage } from 'react-intl' + +import { ReactComponent as IconCheck } from '@/public/static/icons/24px/check.svg' +import { + OPEN_UNIVERSAL_AUTH_DIALOG, + UNIVERSAL_AUTH_TRIGGER, +} from '~/common/enums' +import { Button, Icon, TextIcon, ViewerContext } from '~/components' +import { + ApplyCampaignPrivateFragment, + ApplyCampaignPublicFragment, +} from '~/gql/graphql' + +type ApplyCampaignButtonProps = { + campaign: ApplyCampaignPublicFragment & Partial + size: 'lg' | 'sm' + onClick: () => void +} + +const ApplyCampaignButton = ({ + campaign, + size, + onClick, +}: ApplyCampaignButtonProps) => { + const viewer = useContext(ViewerContext) + const now = new Date() + const { start: appStart, end: appEnd } = campaign.applicationPeriod || {} + const isInApplicationPeriod = !appEnd || now < new Date(appEnd) + const applicationState = campaign.application?.state + const appliedAt = campaign.application?.createdAt + const isSucceeded = applicationState === 'succeeded' + const isPending = applicationState === 'pending' + const isRejected = applicationState === 'rejected' + const isNotApplied = !applicationState + const isAppliedDuringPeriod = + appliedAt && new Date(appliedAt) <= new Date(appEnd) + const isApplicationStarted = now >= new Date(appStart) + + /** + * Rejected + */ + if (isRejected) { + return null + } + + /** + * Succeeded + */ + if (isSucceeded) { + return ( + } + color={isAppliedDuringPeriod ? 'green' : 'black'} + > + {isAppliedDuringPeriod ? ( + + ) : ( + + )} + + ) + } + + /** + * Pending or not applied + */ + let text: React.ReactNode = '' + if (isPending) { + text = isAppliedDuringPeriod ? ( + + ) : ( + + ) + } else if (isNotApplied) { + text = isInApplicationPeriod ? ( + + ) : ( + + ) + } + + if (!viewer.isAuthed) { + onClick = () => { + window.dispatchEvent( + new CustomEvent(OPEN_UNIVERSAL_AUTH_DIALOG, { + detail: { trigger: UNIVERSAL_AUTH_TRIGGER.replyComment }, + }) + ) + } + } + + if (size === 'lg') { + return ( + + ) + } else { + return ( + + ) + } +} + +export default ApplyCampaignButton diff --git a/src/views/CampaignDetail/Apply/Dialog/index.tsx b/src/views/CampaignDetail/Apply/Dialog/index.tsx new file mode 100644 index 0000000000..5a7a1e96b2 --- /dev/null +++ b/src/views/CampaignDetail/Apply/Dialog/index.tsx @@ -0,0 +1,194 @@ +import gql from 'graphql-tag' +import { useEffect } from 'react' +import { FormattedMessage } from 'react-intl' + +import { Dialog, toast, useDialogSwitch, useMutation } from '~/components' +import { + ApplyCampaignMutation, + ApplyCampaignPrivateFragment, + ApplyCampaignPublicFragment, +} from '~/gql/graphql' + +const APPLY_CAMPAIGN = gql` + mutation ApplyCampaign($id: ID!) { + applyCampaign(input: { id: $id }) { + id + ... on WritingChallenge { + application { + state + createdAt + } + } + } + } +` + +export interface ApplyCampaignDialogProps { + campaign: ApplyCampaignPublicFragment & Partial + children: ({ openDialog }: { openDialog: () => void }) => React.ReactNode +} + +const ApplyCampaignDialog = ({ + campaign, + children, +}: ApplyCampaignDialogProps) => { + const { show, openDialog, closeDialog } = useDialogSwitch(true) + + const now = new Date() + const { end: appEnd } = campaign.applicationPeriod || {} + const isInApplicationPeriod = !appEnd || now < new Date(appEnd) + + const [applyCampaign, { loading }] = useMutation( + APPLY_CAMPAIGN, + { variables: { id: campaign.id } } + ) + + const onApplyAfterPeriod = async () => { + try { + await applyCampaign() + closeDialog() + } catch (error) { + toast.error({ + message: ( + + ), + }) + } + } + + const onApplyDuringPeriod = async () => { + try { + await applyCampaign() + } catch (error) { + toast.error({ + message: ( + + ), + }) + closeDialog() + } + } + + // auto apply + useEffect(() => { + if (!isInApplicationPeriod) return + + onApplyDuringPeriod() + }, [isInApplicationPeriod]) + + return ( + <> + {children({ openDialog })} + + + + ) : ( + + ) + } + /> + + + +

+ {isInApplicationPeriod ? ( + + ) : ( + + )} +

+
+
+ + + {!isInApplicationPeriod && ( + + } + loading={loading} + onClick={() => onApplyAfterPeriod()} + /> + )} + + } + color="greyDarker" + onClick={closeDialog} + /> + + } + smUpBtns={ + <> + {isInApplicationPeriod && ( + + } + color="greyDarker" + onClick={closeDialog} + /> + )} + {!isInApplicationPeriod && ( + + } + color="greyDarker" + onClick={closeDialog} + /> + )} + {!isInApplicationPeriod && ( + + } + loading={loading} + color="green" + onClick={() => onApplyAfterPeriod()} + /> + )} + + } + /> +
+ + ) +} + +const LazyApplyCampaignDialog = (props: ApplyCampaignDialogProps) => ( + }> + {({ openDialog }) => <>{props.children({ openDialog })}} + +) + +export default LazyApplyCampaignDialog diff --git a/src/views/CampaignDetail/Apply/index.tsx b/src/views/CampaignDetail/Apply/index.tsx new file mode 100644 index 0000000000..3ffd0c404b --- /dev/null +++ b/src/views/CampaignDetail/Apply/index.tsx @@ -0,0 +1,33 @@ +import gql from 'graphql-tag' + +import Button from './Button' +import Dialog from './Dialog' + +const fragments = { + public: gql` + fragment ApplyCampaignPublic on WritingChallenge { + id + applicationPeriod { + start + end + } + } + `, + private: gql` + fragment ApplyCampaignPrivate on WritingChallenge { + id + application { + state + createdAt + } + } + `, +} + +const Apply = { + Button, + Dialog, + fragments, +} + +export default Apply diff --git a/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts b/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts new file mode 100644 index 0000000000..0b6bd75437 --- /dev/null +++ b/src/views/CampaignDetail/ArticleFeeds/MainFeed/gql.ts @@ -0,0 +1,45 @@ +import gql from 'graphql-tag' + +import { ArticleDigestFeed } from '~/components' + +export const CAMPAIGN_ARTICLES_PUBLIC = gql` + query CampaignArticlesPublic( + $shortHash: String! + $after: String + $filter: CampaignArticlesFilter + ) { + campaign(input: { shortHash: $shortHash }) { + id + ... on WritingChallenge { + articles(input: { first: 20, after: $after, filter: $filter }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate + } + } + } + } + } + } + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} +` + +export const CAMPAIGN_ARTICLES_PRIVATE = gql` + query CampaignArticlesPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on Article { + ...ArticleDigestFeedArticlePrivate + } + } + } + ${ArticleDigestFeed.fragments.article.private} +` diff --git a/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx b/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx new file mode 100644 index 0000000000..6227078261 --- /dev/null +++ b/src/views/CampaignDetail/ArticleFeeds/MainFeed/index.tsx @@ -0,0 +1,159 @@ +import { NetworkStatus } from 'apollo-client' +import React, { useContext, useEffect, useRef } from 'react' +import { FormattedMessage } from 'react-intl' + +import { ReactComponent as IconRead } from '@/public/static/icons/24px/read.svg' +import { analytics, mergeConnections } from '~/common/utils' +import { + ArticleDigestFeed, + Empty, + Icon, + InfiniteScroll, + List, + QueryError, + SpinnerBlock, + usePublicQuery, + useRoute, + ViewerContext, +} from '~/components' +import { CampaignArticlesPublicQuery } from '~/gql/graphql' + +import { CampaignFeedType, LATEST_FEED_TYPE } from '../Tabs' +import { CAMPAIGN_ARTICLES_PRIVATE, CAMPAIGN_ARTICLES_PUBLIC } from './gql' + +interface MainFeedProps { + feedType: CampaignFeedType +} + +const MainFeed = ({ feedType }: MainFeedProps) => { + const viewer = useContext(ViewerContext) + const { getQuery } = useRoute() + const shortHash = getQuery('shortHash') + + const { data, loading, error, fetchMore, networkStatus, client } = + usePublicQuery(CAMPAIGN_ARTICLES_PUBLIC, { + variables: { + shortHash, + ...(feedType !== LATEST_FEED_TYPE + ? { filter: { stage: feedType } } + : {}), + }, + notifyOnNetworkStatusChange: true, + }) + + // pagination + const connectionPath = 'campaign.articles' + const { edges, pageInfo } = data?.campaign?.articles || {} + const isNewLoading = + [NetworkStatus.loading, NetworkStatus.setVariables].indexOf( + networkStatus + ) >= 0 + + // private data + const loadPrivate = (publicData?: CampaignArticlesPublicQuery) => { + if (!viewer.isAuthed || !publicData) { + return + } + + const publiceEdges = publicData.campaign?.articles.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: CAMPAIGN_ARTICLES_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + const fetchedPrviateTypeRef = useRef([]) + useEffect(() => { + const fetched = fetchedPrviateTypeRef.current.indexOf(feedType) >= 0 + if (loading || !edges || fetched || !viewer.isAuthed) { + return + } + + loadPrivate(data) + fetchedPrviateTypeRef.current = [...fetchedPrviateTypeRef.current, feedType] + }, [!!edges, loading, feedType, viewer.id]) + + // load next page + const loadMore = async () => { + if (loading || isNewLoading) { + return + } + + analytics.trackEvent('load_more', { + type: `campaign_detail_${feedType}` as `campaign_detail_${string}`, + location: edges?.length || 0, + }) + + const { data: newData } = await fetchMore({ + variables: { after: pageInfo?.endCursor }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + + loadPrivate(newData) + } + + if (loading && (!edges || isNewLoading)) { + return + } + + if (error) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + } + description={ + + } + /> + ) + } + + return ( + + + {edges.map(({ node }, i) => ( + + + { + analytics.trackEvent('click_feed', { + type: `campaign_detail_${feedType}` as `campaign_detail_${string}`, + contentType: 'article', + location: i, + id: node.id, + }) + }} + onClickAuthor={() => { + analytics.trackEvent('click_feed', { + type: `campaign_detail_${feedType}` as `campaign_detail_${string}`, + contentType: 'user', + location: i, + id: node.author.id, + }) + }} + /> + + + ))} + + + ) +} + +export default MainFeed diff --git a/src/views/CampaignDetail/ArticleFeeds/Tabs/index.tsx b/src/views/CampaignDetail/ArticleFeeds/Tabs/index.tsx new file mode 100644 index 0000000000..5df1109c49 --- /dev/null +++ b/src/views/CampaignDetail/ArticleFeeds/Tabs/index.tsx @@ -0,0 +1,102 @@ +import gql from 'graphql-tag' +import { useContext } from 'react' +import { useIntl } from 'react-intl' + +import { analytics } from '~/common/utils' +import { LanguageContext, SquareTabs } from '~/components' +import { ArticleFeedsTabsCampaignFragment } from '~/gql/graphql' + +import styles from './styles.module.css' + +export type CampaignFeedType = string + +export const LATEST_FEED_TYPE = 'latest' + +interface ArticleFeedsTabsProps { + feedType: CampaignFeedType + setFeedType: (type: string) => void + campaign: ArticleFeedsTabsCampaignFragment +} + +const ArticleFeedsTabs = ({ + feedType, + setFeedType, + campaign, +}: ArticleFeedsTabsProps) => { + const { lang } = useContext(LanguageContext) + const intl = useIntl() + const stages = campaign.stages || [] + + const shouldShowTab = (startedAt?: string) => { + if (!startedAt) return true + + const now = new Date() + return now >= new Date(startedAt) + } + + return ( +
+ + { + setFeedType(LATEST_FEED_TYPE) + + analytics.trackEvent('click_button', { + type: `campaign_detail_tab_${LATEST_FEED_TYPE}` as `campaign_detail_tab_${string}`, + pageType: 'campaign_detail', + }) + }} + title={intl.formatMessage({ + defaultMessage: 'Latest', + id: 'adThp5', + })} + /> + + {[...stages].reverse().map((stage) => + shouldShowTab(stage.period?.start) ? ( + { + setFeedType(stage.id) + + analytics.trackEvent('click_button', { + type: `campaign_detail_tab_${stage.id}` as `campaign_detail_tab_${string}`, + pageType: 'campaign_detail', + }) + }} + title={ + stage[ + lang === 'zh_hans' + ? 'nameZhHans' + : lang === 'zh_hant' + ? 'nameZhHant' + : 'nameEn' + ] + } + key={stage.id} + /> + ) : null + )} + +
+ ) +} + +ArticleFeedsTabs.fragments = gql` + fragment ArticleFeedsTabsCampaign on WritingChallenge { + id + stages { + id + nameZhHant: name(input: { language: zh_hant }) + nameZhHans: name(input: { language: zh_hans }) + nameEn: name(input: { language: en }) + period { + start + end + } + } + } +` + +export default ArticleFeedsTabs diff --git a/src/views/CampaignDetail/ArticleFeeds/Tabs/styles.module.css b/src/views/CampaignDetail/ArticleFeeds/Tabs/styles.module.css new file mode 100644 index 0000000000..e63daed54b --- /dev/null +++ b/src/views/CampaignDetail/ArticleFeeds/Tabs/styles.module.css @@ -0,0 +1,7 @@ +.tabs { + padding-left: var(--sp16); + + @media (--sm-up) { + padding-left: 0; + } +} diff --git a/src/views/CampaignDetail/ArticleFeeds/index.tsx b/src/views/CampaignDetail/ArticleFeeds/index.tsx new file mode 100644 index 0000000000..159f03786a --- /dev/null +++ b/src/views/CampaignDetail/ArticleFeeds/index.tsx @@ -0,0 +1,51 @@ +import gql from 'graphql-tag' +import React, { useState } from 'react' + +import { Layout, useRoute } from '~/components' +import { ArticleFeedsTabsCampaignFragment } from '~/gql/graphql' + +import MainFeed from './MainFeed' +import styles from './styles.module.css' +import ArticleFeedsTabs, { CampaignFeedType, LATEST_FEED_TYPE } from './Tabs' + +const ArticleFeeds = ({ + campaign, +}: { + campaign: ArticleFeedsTabsCampaignFragment +}) => { + const { getQuery, setQuery } = useRoute() + const qsType = getQuery('type') as CampaignFeedType + + const [feedType, setFeedType] = useState( + qsType || LATEST_FEED_TYPE + ) + + const changeFeed = (newType: CampaignFeedType) => { + setQuery('type', newType === LATEST_FEED_TYPE ? '' : newType) + setFeedType(newType) + } + + return ( +
+ + + + + +
+ ) +} + +ArticleFeeds.fragments = gql` + fragment ArticleFeedsCampaign on WritingChallenge { + id + ...ArticleFeedsTabsCampaign + } + ${ArticleFeedsTabs.fragments} +` + +export default ArticleFeeds diff --git a/src/views/CampaignDetail/ArticleFeeds/styles.module.css b/src/views/CampaignDetail/ArticleFeeds/styles.module.css new file mode 100644 index 0000000000..5460245c49 --- /dev/null +++ b/src/views/CampaignDetail/ArticleFeeds/styles.module.css @@ -0,0 +1,3 @@ +.feeds { + margin-top: var(--sp24); +} diff --git a/src/views/CampaignDetail/Description/index.tsx b/src/views/CampaignDetail/Description/index.tsx new file mode 100644 index 0000000000..4789b6e6fb --- /dev/null +++ b/src/views/CampaignDetail/Description/index.tsx @@ -0,0 +1,44 @@ +import gql from 'graphql-tag' +import { useContext } from 'react' + +import { LanguageContext, Layout } from '~/components' +import { DescriptionCampaignFragment } from '~/gql/graphql' + +import styles from './styles.module.css' + +const Description = ({ + campaign, +}: { + campaign: DescriptionCampaignFragment +}) => { + const { lang } = useContext(LanguageContext) + + return ( + +
+ + ) +} + +Description.fragments = gql` + fragment DescriptionCampaign on WritingChallenge { + id + descriptionZhHant: description(input: { language: zh_hant }) + descriptionZhHans: description(input: { language: zh_hans }) + descriptionEn: description(input: { language: en }) + } +` + +export default Description diff --git a/src/views/CampaignDetail/Description/styles.module.css b/src/views/CampaignDetail/Description/styles.module.css new file mode 100644 index 0000000000..58c3df856b --- /dev/null +++ b/src/views/CampaignDetail/Description/styles.module.css @@ -0,0 +1,15 @@ +.description { + margin-top: var(--sp24); + font-size: var(--text15); + line-height: 1.75; + color: var(--color-grey-darker); + + & p + p { + margin: var(--sp12) 0 0; + } + + @media (--md-up) { + padding-top: var(--sp24); + border-top: 1px dashed var(--color-grey-light); + } +} diff --git a/src/views/CampaignDetail/InfoHeader/Participants/Dialog/Content.tsx b/src/views/CampaignDetail/InfoHeader/Participants/Dialog/Content.tsx new file mode 100644 index 0000000000..db4ccf68a5 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/Participants/Dialog/Content.tsx @@ -0,0 +1,103 @@ +import { useQuery } from '@apollo/react-hooks' +import { useContext } from 'react' + +import { analytics, mergeConnections } from '~/common/utils' +import { + Dialog, + InfiniteScroll, + List, + QueryError, + SpinnerBlock, + useRoute, + ViewerContext, +} from '~/components' +import { UserDigest } from '~/components/UserDigest' +import { GetParticipantsQuery } from '~/gql/graphql' + +import { GET_PARTICIPANTS } from './gql' + +const ParticipantsDialogContent = () => { + const viewer = useContext(ViewerContext) + const { getQuery } = useRoute() + const shortHash = getQuery('shortHash') + + const { data, loading, error, fetchMore } = useQuery( + GET_PARTICIPANTS, + { variables: { shortHash } } + ) + + // pagination + const campaign = data?.campaign + const connectionPath = 'campaign.participants' + const { edges, pageInfo } = campaign?.participants || {} + + // load next page + const loadMore = async () => { + analytics.trackEvent('load_more', { + type: 'follower', + location: edges?.length || 0, + }) + await fetchMore({ + variables: { after: pageInfo?.endCursor }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + } + + if (loading) { + return + } + + if (error) { + return + } + + if (!campaign || !edges || edges.length <= 0 || !pageInfo) { + return null + } + + const isViewerApplySucceeded = campaign.application?.state === 'succeeded' + + return ( + + + + {isViewerApplySucceeded && ( + + + + )} + {edges + .filter((e) => e.node.id !== viewer.id) + .map(({ node, cursor }, i) => ( + + + analytics.trackEvent('click_feed', { + type: 'campaign_detail_participant', + contentType: 'user', + location: i, + id: node.id, + }) + } + /> + + ))} + + + + ) +} + +export default ParticipantsDialogContent diff --git a/src/views/CampaignDetail/InfoHeader/Participants/Dialog/gql.ts b/src/views/CampaignDetail/InfoHeader/Participants/Dialog/gql.ts new file mode 100644 index 0000000000..0aaa0d4667 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/Participants/Dialog/gql.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag' + +import { UserDigest } from '~/components/UserDigest' + +export const GET_PARTICIPANTS = gql` + query GetParticipants($shortHash: String!, $after: String) { + campaign(input: { shortHash: $shortHash }) { + id + ... on WritingChallenge { + application { + state + } + participants(input: { first: 20, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ...UserDigestRichUserPublic + } + } + } + } + } + } + ${UserDigest.Rich.fragments.user.public} +` diff --git a/src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx b/src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx new file mode 100644 index 0000000000..fd3eda65af --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/Participants/Dialog/index.tsx @@ -0,0 +1,62 @@ +import dynamic from 'next/dynamic' +import { FormattedMessage, useIntl } from 'react-intl' + +import { Dialog, SpinnerBlock, useDialogSwitch } from '~/components' +import { InfoHeaderParticipantsCampaignFragment } from '~/gql/graphql' + +interface ParticipantsDialogProps { + campaign: InfoHeaderParticipantsCampaignFragment + children: ({ openDialog }: { openDialog: () => void }) => React.ReactNode +} + +const DynamicContent = dynamic(() => import('./Content'), { + loading: () => , +}) + +const BaseParticipantsDialog = ({ + campaign, + children, +}: ParticipantsDialogProps) => { + const intl = useIntl() + const { show, openDialog, closeDialog } = useDialogSwitch(true) + + return ( + <> + {children({ openDialog })} + + + } + /> + + + + } + color="greyDarker" + onClick={closeDialog} + /> + } + /> + + + ) +} + +export const ParticipantsDialog = (props: ParticipantsDialogProps) => ( + }> + {({ openDialog }) => <>{props.children({ openDialog })}} + +) diff --git a/src/views/CampaignDetail/InfoHeader/Participants/index.tsx b/src/views/CampaignDetail/InfoHeader/Participants/index.tsx new file mode 100644 index 0000000000..24aa05fab0 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/Participants/index.tsx @@ -0,0 +1,73 @@ +import gql from 'graphql-tag' +import { useContext } from 'react' +import { FormattedMessage } from 'react-intl' + +import { Avatar, ViewerContext } from '~/components' +import { InfoHeaderParticipantsCampaignFragment } from '~/gql/graphql' + +import { ParticipantsDialog } from './Dialog' +import styles from './styles.module.css' + +const fragments = gql` + fragment InfoHeaderParticipantsCampaign on WritingChallenge { + id + application { + state + createdAt + } + participants(input: { first: 8 }) { + totalCount + edges { + node { + id + ...AvatarUser + } + } + } + } +` + +const Participants = ({ + campaign, +}: { + campaign: InfoHeaderParticipantsCampaignFragment +}) => { + const viewer = useContext(ViewerContext) + const isViewerApplySucceeded = campaign.application?.state === 'succeeded' + const edges = campaign.participants.edges?.slice( + 0, + isViewerApplySucceeded ? 7 : 8 + ) + + return ( + + {({ openDialog }) => ( +
+ + {campaign.participants.totalCount} +   + + +
+ {isViewerApplySucceeded && } + {edges?.map(({ node }, i) => ( + + ))} +
+
+ )} +
+ ) +} + +Participants.fragments = fragments + +export default Participants diff --git a/src/views/CampaignDetail/InfoHeader/Participants/styles.module.css b/src/views/CampaignDetail/InfoHeader/Participants/styles.module.css new file mode 100644 index 0000000000..9113297150 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/Participants/styles.module.css @@ -0,0 +1,88 @@ +.participants { + @mixin flex-center-start; + + font-size: var(--text14); + line-height: 1.375rem; + color: var(--color-grey); + + @media (--md-up) { + display: none; + } + + & .count { + color: var(--color-black); + } + + & .avatars { + @mixin flex-center-start; + + margin-left: var(--sp6); + + & > * { + position: relative; + margin-left: calc(var(--sp4) * -1); + box-shadow: 0 0 0 2px var(--color-white); + + &:first-child { + z-index: 15; + margin-left: 0; + } + + &:nth-child(2) { + z-index: 14; + } + + &:nth-child(3) { + z-index: 13; + } + + &:nth-child(4) { + z-index: 12; + } + + &:nth-child(5) { + z-index: 11; + } + + &:nth-child(6) { + z-index: 10; + } + + &:nth-child(7) { + z-index: 9; + } + + &:nth-child(8) { + z-index: 8; + } + + &:nth-child(9) { + z-index: 7; + } + + &:nth-child(10) { + z-index: 6; + } + + &:nth-child(11) { + z-index: 5; + } + + &:nth-child(12) { + z-index: 4; + } + + &:nth-child(13) { + z-index: 3; + } + + &:nth-child(14) { + z-index: 2; + } + + &:nth-child(15) { + z-index: 1; + } + } + } +} diff --git a/src/views/CampaignDetail/InfoHeader/gql.ts b/src/views/CampaignDetail/InfoHeader/gql.ts new file mode 100644 index 0000000000..6dafdbd104 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/gql.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +import Apply from '../Apply' +import Participants from './Participants' + +export const fragments = { + campaign: { + public: gql` + fragment InfoHeaderCampaignPublic on WritingChallenge { + id + nameZhHant: name(input: { language: zh_hant }) + nameZhHans: name(input: { language: zh_hans }) + nameEn: name(input: { language: en }) + cover + link + applicationPeriod { + start + end + } + writingPeriod { + start + end + } + ...ApplyCampaignPublic + ...InfoHeaderParticipantsCampaign + } + ${Participants.fragments} + ${Apply.fragments.public} + `, + private: gql` + fragment InfoHeaderCampaignPrivate on WritingChallenge { + id + ...ApplyCampaignPrivate + } + ${Apply.fragments.private} + `, + }, +} diff --git a/src/views/CampaignDetail/InfoHeader/index.tsx b/src/views/CampaignDetail/InfoHeader/index.tsx new file mode 100644 index 0000000000..3b0bf7d3e0 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/index.tsx @@ -0,0 +1,172 @@ +import { useContext } from 'react' +import { FormattedMessage } from 'react-intl' + +import { ReactComponent as IconRight } from '@/public/static/icons/24px/right.svg' +import { analytics, datetimeFormat, isUTC8 } from '~/common/utils' +import { + DotDivider, + Icon, + LanguageContext, + ResponsiveImage, + TextIcon, +} from '~/components' +import { + InfoHeaderCampaignPrivateFragment, + InfoHeaderCampaignPublicFragment, +} from '~/gql/graphql' + +import Apply from '../Apply' +import { fragments } from './gql' +import Participants from './Participants' +import styles from './styles.module.css' + +type InfoHeaderProps = { + campaign: InfoHeaderCampaignPublicFragment & + Partial +} + +const ViewMore = ({ link }: { link: string }) => ( + { + analytics.trackEvent('click_button', { + type: 'campaign_detail_link', + pageType: 'campaign_detail', + }) + }} + > + } + spacing={4} + placement="left" + > + + + +) + +const InfoHeader = ({ campaign }: InfoHeaderProps) => { + const { lang } = useContext(LanguageContext) + const now = new Date() + const { start: appStart, end: appEnd } = campaign.applicationPeriod || {} + const { start: writingStart, end: writingEnd } = campaign.writingPeriod || {} + const isInApplicationPeriod = !appEnd || now < new Date(appEnd) + + return ( + + {({ openDialog }) => ( +
+ {campaign.cover && ( +
+ +
+ )} + +

+ { + campaign[ + lang === 'zh_hans' + ? 'nameZhHans' + : lang === 'zh_hant' + ? 'nameZhHant' + : 'nameEn' + ] + } +

+ +
+
+ {isInApplicationPeriod && ( + + + + + {appStart + ? datetimeFormat.absolute({ + date: appStart, + lang, + utc8: true, + }) + : ''}{' '} + -{' '} + {appEnd + ? datetimeFormat.absolute({ + date: appEnd, + lang, + utc8: true, + }) + : ''} + + + )} + {!isInApplicationPeriod && ( + + + + {writingStart + ? datetimeFormat.absolute({ + date: writingStart, + lang, + utc8: true, + }) + : ''}{' '} + -{' '} + {writingEnd + ? datetimeFormat.absolute({ + date: writingEnd, + lang, + utc8: true, + }) + : ''} + + + )} + +
+ +
+ + +
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ )} +
+ ) +} + +InfoHeader.fragments = fragments + +export default InfoHeader diff --git a/src/views/CampaignDetail/InfoHeader/styles.module.css b/src/views/CampaignDetail/InfoHeader/styles.module.css new file mode 100644 index 0000000000..19797f4e39 --- /dev/null +++ b/src/views/CampaignDetail/InfoHeader/styles.module.css @@ -0,0 +1,97 @@ +.header { + padding: 0 var(--sp16); + margin-top: var(--sp20); + + @media (--sm-up) { + padding: 0; + margin-top: var(--sp32); + } +} + +.cover { + position: relative; + overflow: hidden; + border-radius: 1.25rem; + + & img { + @mixin object-fit-cover; + + background-color: var(--color-grey-lighter); + } + + &::after { + display: block; + padding-bottom: 23.26%; + content: ''; + } +} + +.dot { + margin: 0 var(--sp8); + color: var(--color-grey-darker); +} + +.name { + margin-top: var(--sp24); + font-size: var(--text24); + font-weight: var(--font-medium); + line-height: 2.25rem; +} + +.viewMore { + font-size: 0; +} + +.meta { + @mixin flex-center-space-between; + + margin-top: var(--sp8); + font-size: var(--text14); + line-height: 1.375rem; + + & .left { + @mixin flex-start-center; + } + + & .dot, + & .viewMore { + display: none; + + @media (--md-up) { + display: inline; + } + } + + & .period { + display: inline-block; + } + + & .right { + display: none; + + @media (--md-up) { + display: block; + } + } +} + +.extra { + @mixin flex-center-start; + + margin-top: var(--sp8); + + @media (--sm-up) { + & .dot, + & .viewMore { + display: none; + } + } +} + +.mobileApply { + margin-top: var(--sp24); + + @media (--md-up) { + display: none; + } +} diff --git a/src/views/CampaignDetail/SideParticipants/gql.ts b/src/views/CampaignDetail/SideParticipants/gql.ts new file mode 100644 index 0000000000..533b1d68d2 --- /dev/null +++ b/src/views/CampaignDetail/SideParticipants/gql.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag' + +import { Avatar } from '~/components' + +export const fragments = { + public: gql` + fragment SideParticipantsCampaignPublic on WritingChallenge { + id + sideParticipants: participants(input: { first: null }) { + totalCount + edges { + cursor + node { + id + displayName + userName + ...AvatarUser + } + } + } + } + ${Avatar.fragments.user} + `, + private: gql` + fragment SideParticipantsCampaignPrivate on WritingChallenge { + id + application { + state + } + } + `, +} diff --git a/src/views/CampaignDetail/SideParticipants/index.tsx b/src/views/CampaignDetail/SideParticipants/index.tsx new file mode 100644 index 0000000000..ebd63a9c17 --- /dev/null +++ b/src/views/CampaignDetail/SideParticipants/index.tsx @@ -0,0 +1,101 @@ +import { useContext } from 'react' +import { FormattedMessage } from 'react-intl' + +import { analytics, toPath } from '~/common/utils' +import { Avatar, LinkWrapper, Tooltip, ViewerContext } from '~/components' +import { + AvatarUserFragment, + SideParticipantsCampaignPrivateFragment, + SideParticipantsCampaignPublicFragment, +} from '~/gql/graphql' + +import { fragments } from './gql' +import styles from './styles.module.css' + +type SideParticipantsProps = { + campaign: SideParticipantsCampaignPublicFragment & + Partial +} + +const Participant = ({ + user, + onClick, +}: { + user: AvatarUserFragment & { + userName?: string | null + displayName?: string | null + } + onClick?: () => void +}) => { + const path = toPath({ + page: 'userProfile', + userName: user.userName || '', + }) + if (!user.displayName) { + return ( + + + + ) + } + + return ( + + + + + + + + ) +} + +const SideParticipants = ({ campaign }: SideParticipantsProps) => { + const viewer = useContext(ViewerContext) + const edges = campaign.sideParticipants.edges + const isViewerApplySucceeded = campaign.application?.state === 'succeeded' + + if (edges && edges.length <= 0) { + return null + } + + return ( +
+

+ {' '} + + {campaign.sideParticipants.totalCount} + +

+ +
+ {isViewerApplySucceeded && } + + {edges + ?.filter((u) => u.node.id !== viewer.id) + .map(({ node, cursor }, i) => ( + + analytics.trackEvent('click_feed', { + type: 'campaign_detail_participant', + contentType: 'user', + location: i, + id: node.id, + }) + } + /> + ))} +
+
+ ) +} + +SideParticipants.fragments = fragments + +export default SideParticipants diff --git a/src/views/CampaignDetail/SideParticipants/styles.module.css b/src/views/CampaignDetail/SideParticipants/styles.module.css new file mode 100644 index 0000000000..ff507dfa23 --- /dev/null +++ b/src/views/CampaignDetail/SideParticipants/styles.module.css @@ -0,0 +1,17 @@ +.participants { + & h2 { + font-size: var(--text20); + font-weight: var(--font-medium); + } + + & .count { + color: var(--color-grey); + } +} + +.avatars { + display: flex; + flex-wrap: wrap; + gap: var(--sp14); + margin-top: var(--sp16); +} diff --git a/src/views/CampaignDetail/gql.ts b/src/views/CampaignDetail/gql.ts new file mode 100644 index 0000000000..5666b21d2b --- /dev/null +++ b/src/views/CampaignDetail/gql.ts @@ -0,0 +1,29 @@ +import gql from 'graphql-tag' + +import ArticleFeeds from './ArticleFeeds' +import Description from './Description' +import InfoHeader from './InfoHeader' +import SideParticipants from './SideParticipants' + +export const CAMPAIGN_DETAIL = gql` + query CampaignDetail($shortHash: String!) { + campaign(input: { shortHash: $shortHash }) { + id + shortHash + ... on WritingChallenge { + ...InfoHeaderCampaignPublic + ...InfoHeaderCampaignPrivate + ...DescriptionCampaign + ...SideParticipantsCampaignPublic + ...SideParticipantsCampaignPrivate + ...ArticleFeedsCampaign + } + } + } + ${InfoHeader.fragments.campaign.public} + ${InfoHeader.fragments.campaign.private} + ${Description.fragments} + ${SideParticipants.fragments.public} + ${SideParticipants.fragments.private} + ${ArticleFeeds.fragments} +` diff --git a/src/views/CampaignDetail/index.tsx b/src/views/CampaignDetail/index.tsx new file mode 100644 index 0000000000..66f3b43de6 --- /dev/null +++ b/src/views/CampaignDetail/index.tsx @@ -0,0 +1,98 @@ +import { useQuery } from '@apollo/react-hooks' +import { useContext } from 'react' + +import { toPath } from '~/common/utils' +import { + EmptyLayout, + Head, + LanguageContext, + Layout, + SpinnerBlock, + Throw404, + useRoute, +} from '~/components' +import { QueryError } from '~/components/GQL' +import { CampaignDetailQuery } from '~/gql/graphql' + +import ArticleFeeds from './ArticleFeeds' +import Description from './Description' +import { CAMPAIGN_DETAIL } from './gql' +import InfoHeader from './InfoHeader' +import SideParticipants from './SideParticipants' + +const CampaignDetail = () => { + const { lang } = useContext(LanguageContext) + const { getQuery } = useRoute() + const shortHash = getQuery('shortHash') + + const { data, loading, error } = useQuery( + CAMPAIGN_DETAIL, + { variables: { shortHash } } + ) + + const campaign = data?.campaign + + if (loading) { + return ( + + + + ) + } + + if (!campaign) { + return ( + + + + ) + } + + if (error) { + return ( + + + + ) + } + + const path = toPath({ page: 'campaignDetail', campaign }) + const now = new Date() + const { end: appEnd } = campaign.applicationPeriod || {} + const isInApplicationPeriod = !appEnd || now < new Date(appEnd) + + return ( + }> + + + + + {isInApplicationPeriod && } + + {!isInApplicationPeriod && } + + ) +} + +export default CampaignDetail diff --git a/src/views/Circle/Settings/ManageInvitation/AddInvitationDialog/SelectPeriod/index.tsx b/src/views/Circle/Settings/ManageInvitation/AddInvitationDialog/SelectPeriod/index.tsx index 905fe1d869..7ea108c0aa 100644 --- a/src/views/Circle/Settings/ManageInvitation/AddInvitationDialog/SelectPeriod/index.tsx +++ b/src/views/Circle/Settings/ManageInvitation/AddInvitationDialog/SelectPeriod/index.tsx @@ -11,7 +11,7 @@ const SelectPeriod = ({ period, onChange }: Props) => { const options = [30, 90, 180, 360] return ( - name="select-period" label={ diff --git a/src/views/Circle/Settings/ManageInvitation/Invites/CircleInvitation/Period.tsx b/src/views/Circle/Settings/ManageInvitation/Invites/CircleInvitation/Period.tsx index 75f95b263f..a07bc6bcbe 100644 --- a/src/views/Circle/Settings/ManageInvitation/Invites/CircleInvitation/Period.tsx +++ b/src/views/Circle/Settings/ManageInvitation/Invites/CircleInvitation/Period.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames' -import differenceInDays from 'date-fns/differenceInDays' -import parseISO from 'date-fns/parseISO' +import { differenceInDays, parseISO } from 'date-fns' import { FormattedMessage } from 'react-intl' import { CircleInvitationFragment } from '~/gql/graphql' diff --git a/src/views/Follow/Feed/RecommendCircleActivity/gql.ts b/src/views/Follow/Feed/RecommendCircleActivity/gql.ts deleted file mode 100644 index 286706c159..0000000000 --- a/src/views/Follow/Feed/RecommendCircleActivity/gql.ts +++ /dev/null @@ -1,14 +0,0 @@ -import gql from 'graphql-tag' - -import FollowingRecommendCircle from '../FollowingRecommendCircle' - -export const fragments = gql` - fragment RecommendCircleActivity on CircleRecommendationActivity { - recommendCircles: nodes { - ...FollowingFeedRecommendCirclePublic - ...FollowingFeedRecommendCirclePrivate - } - } - ${FollowingRecommendCircle.fragments.circle.public} - ${FollowingRecommendCircle.fragments.circle.private} -` diff --git a/src/views/Follow/Feed/RecommendCircleActivity/index.tsx b/src/views/Follow/Feed/RecommendCircleActivity/index.tsx deleted file mode 100644 index 434cf59712..0000000000 --- a/src/views/Follow/Feed/RecommendCircleActivity/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { analytics } from '~/common/utils' -import { Slides } from '~/components' -import { RecommendCircleActivityFragment } from '~/gql/graphql' - -import FollowingRecommendCircle from '../FollowingRecommendCircle' -import FollowingRecommendHead from '../FollowingRecommendHead' -import { fragments } from './gql' -import styles from './styles.module.css' - -interface Props { - circles: RecommendCircleActivityFragment['recommendCircles'] | null - location: number -} - -const RecommendCircleActivity = ({ circles, location }: Props) => { - if (!circles || circles.length <= 0) { - return null - } - - return ( -
- }> - {circles.map((circle, index) => ( - { - analytics.trackEvent('click_feed', { - type: 'following', - contentType: 'CircleRecommendationActivity', - location: `${location}.${index}`, - id: circle.id, - }) - }} - > -
- -
-
- ))} -
-
- ) -} - -RecommendCircleActivity.fragments = fragments - -export default RecommendCircleActivity diff --git a/src/views/Follow/Feed/RecommendCircleActivity/styles.module.css b/src/views/Follow/Feed/RecommendCircleActivity/styles.module.css deleted file mode 100644 index a0e4d16bbe..0000000000 --- a/src/views/Follow/Feed/RecommendCircleActivity/styles.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.container { - padding-bottom: var(--sp8); -} - -.item { - @mixin border-grey; - - border-radius: var(--sp8); -} diff --git a/src/views/Follow/Feed/UserPostMomentActivity/gql.ts b/src/views/Follow/Feed/UserPostMomentActivity/gql.ts new file mode 100644 index 0000000000..56e7115ed1 --- /dev/null +++ b/src/views/Follow/Feed/UserPostMomentActivity/gql.ts @@ -0,0 +1,22 @@ +import gql from 'graphql-tag' + +import { MomentDigestFeed } from '~/components/MomentDigest/Feed' + +export const fragments = gql` + fragment UserPostMomentActivity on UserPostMomentActivity { + actor { + id + } + createdAt + nodeMoment: node { + ...MomentDigestFeedMomentPublic + ...MomentDigestFeedMomentPrivate + } + more { + ...MomentDigestFeedMomentPublic + ...MomentDigestFeedMomentPrivate + } + } + ${MomentDigestFeed.fragments.moment.public} + ${MomentDigestFeed.fragments.moment.private} +` diff --git a/src/views/Follow/Feed/UserPostMomentActivity/index.tsx b/src/views/Follow/Feed/UserPostMomentActivity/index.tsx new file mode 100644 index 0000000000..3a5a32eafc --- /dev/null +++ b/src/views/Follow/Feed/UserPostMomentActivity/index.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react' +import { FormattedMessage } from 'react-intl' + +import { ReactComponent as IconDown } from '@/public/static/icons/24px/down.svg' +import { toPath } from '~/common/utils' +import { + Button, + CardExposureTracker, + Icon, + List, + MomentDigestFeed, + TextIcon, + useRoute, +} from '~/components' +import { UserPostMomentActivityFragment } from '~/gql/graphql' + +import { fragments } from './gql' +import styles from './styles.module.css' + +const UserPostMomentActivity = ({ + actor, + nodeMoment: node, + createdAt, + location, + more, + __typename, +}: UserPostMomentActivityFragment & { location: number }) => { + const mores = more.slice(0, 2) + const [showMore, setShowMore] = useState(false) + const hasMoreThanTwo = more.length > 2 + const { router } = useRoute() + + const userProfilePath = toPath({ + page: 'userProfile', + userName: node.author.userName || '', + }) + + const gotoUserProfile = () => { + router.push(userProfilePath.href) + } + + return ( + <> + + {more.length > 0 && !showMore && ( + + )} + {showMore && ( + <> + + {mores.map((moment, index) => ( + + + + ))} + + {hasMoreThanTwo && ( + + )} + + )} + + + ) +} + +UserPostMomentActivity.fragments = fragments + +export default UserPostMomentActivity diff --git a/src/views/Follow/Feed/UserPostMomentActivity/styles.module.css b/src/views/Follow/Feed/UserPostMomentActivity/styles.module.css new file mode 100644 index 0000000000..98080e7dbc --- /dev/null +++ b/src/views/Follow/Feed/UserPostMomentActivity/styles.module.css @@ -0,0 +1,5 @@ +.showMoreButton { + height: 1.375rem; + margin-top: calc(var(--sp8) * -1); + margin-bottom: var(--sp20); +} diff --git a/src/views/Follow/Feed/gql.ts b/src/views/Follow/Feed/gql.ts index 20226260f8..093e3ce645 100644 --- a/src/views/Follow/Feed/gql.ts +++ b/src/views/Follow/Feed/gql.ts @@ -1,19 +1,24 @@ import gql from 'graphql-tag' import RecommendArticleActivity from './RecommendArticleActivity' -import RecommendCircleActivity from './RecommendCircleActivity' import RecommendUserActivity from './RecommendUserActivity' import UserAddArticleTagActivity from './UserAddArticleTagActivity' import UserBroadcastCircleActivity from './UserBroadcastCircleActivity' import UserCreateCircleActivity from './UserCreateCircleActivity' +import UserPostMomentActivity from './UserPostMomentActivity' import UserPublishArticleActivity from './UserPublishArticleActivity' export const FOLLOWING_FEED = gql` - query FollowingFeed($after: String) { + query FollowingFeed( + $after: String + $type: RecommendationFollowingFilterType + ) { viewer { id recommendation { - following(input: { first: 10, after: $after }) { + following( + input: { first: 10, after: $after, filter: { type: $type } } + ) { pageInfo { startCursor endCursor @@ -26,6 +31,9 @@ export const FOLLOWING_FEED = gql` ... on UserPublishArticleActivity { ...UserPublishArticleActivity } + ... on UserPostMomentActivity { + ...UserPostMomentActivity + } ... on UserBroadcastCircleActivity { ...UserBroadcastCircleActivity } @@ -38,9 +46,6 @@ export const FOLLOWING_FEED = gql` ... on ArticleRecommendationActivity { ...RecommendArticleActivity } - ... on CircleRecommendationActivity { - ...RecommendCircleActivity - } ... on UserRecommendationActivity { ...RecommendUserActivity } @@ -53,8 +58,8 @@ export const FOLLOWING_FEED = gql` ${UserAddArticleTagActivity.fragments} ${UserBroadcastCircleActivity.fragments} ${UserCreateCircleActivity.fragments} + ${UserPostMomentActivity.fragments} ${UserPublishArticleActivity.fragments} ${RecommendArticleActivity.fragments} - ${RecommendCircleActivity.fragments} ${RecommendUserActivity.fragments} ` diff --git a/src/views/Follow/Feed/index.tsx b/src/views/Follow/Feed/index.tsx index ae7ca870d0..1dd6fc7c08 100644 --- a/src/views/Follow/Feed/index.tsx +++ b/src/views/Follow/Feed/index.tsx @@ -4,7 +4,7 @@ import _flatten from 'lodash/flatten' import _get from 'lodash/get' import { useIntl } from 'react-intl' -import { analytics, mergeConnections } from '~/common/utils' +import { analytics, mergeConnections, shouldRenderNode } from '~/common/utils' import { EmptyWarning, Head, @@ -14,21 +14,46 @@ import { SpinnerBlock, Translate, } from '~/components' -import { FollowingFeedQuery } from '~/gql/graphql' +import { + FollowingFeedQuery, + RecommendationFollowingFilterType, +} from '~/gql/graphql' +import { TABS } from '../Tabs' import { FOLLOWING_FEED } from './gql' import RecommendArticleActivity from './RecommendArticleActivity' -import RecommendCircleActivity from './RecommendCircleActivity' import RecommendUserActivity from './RecommendUserActivity' import UserAddArticleTagActivity from './UserAddArticleTagActivity' import UserBroadcastCircleActivity from './UserBroadcastCircleActivity' import UserCreateCircleActivity from './UserCreateCircleActivity' +import UserPostMomentActivity from './UserPostMomentActivity' import UserPublishArticleActivity from './UserPublishArticleActivity' -const FollowingFeed = () => { +type FollowingFeedProps = { + tab: TABS +} + +const renderableTypes = new Set([ + 'UserPublishArticleActivity', + 'UserPostMomentActivity', + 'UserBroadcastCircleActivity', + 'UserCreateCircleActivity', + 'UserAddArticleTagActivity', + 'ArticleRecommendationActivity', + 'UserRecommendationActivity', +]) + +const FollowingFeed = ({ tab }: FollowingFeedProps) => { const intl = useIntl() - const { data, loading, error, fetchMore } = - useQuery(FOLLOWING_FEED) + const isArticleTab = tab === 'Article' + const { data, loading, error, fetchMore } = useQuery( + FOLLOWING_FEED, + { + variables: isArticleTab + ? { type: RecommendationFollowingFilterType.Article } + : {}, + } + ) if (loading) { return @@ -92,38 +117,37 @@ const FollowingFeed = () => { eof > - {edges.map(({ node }, i) => ( - - {node.__typename === 'UserPublishArticleActivity' && ( - - )} - {node.__typename === 'UserBroadcastCircleActivity' && ( - - )} - {node.__typename === 'UserCreateCircleActivity' && ( - - )} - {node.__typename === 'UserAddArticleTagActivity' && ( - - )} - {node.__typename === 'ArticleRecommendationActivity' && ( - - )} - {node.__typename === 'CircleRecommendationActivity' && ( - - )} - {node.__typename === 'UserRecommendationActivity' && ( - - )} - - ))} + {edges.map(({ node }, i) => { + return shouldRenderNode(node, renderableTypes) ? ( + + {node.__typename === 'UserPublishArticleActivity' && ( + + )} + {node.__typename === 'UserPostMomentActivity' && ( + + )} + {node.__typename === 'UserBroadcastCircleActivity' && ( + + )} + {node.__typename === 'UserCreateCircleActivity' && ( + + )} + {node.__typename === 'UserAddArticleTagActivity' && ( + + )} + {node.__typename === 'ArticleRecommendationActivity' && ( + + )} + {node.__typename === 'UserRecommendationActivity' && ( + + )} + + ) : null + })} diff --git a/src/views/Follow/Tabs/index.tsx b/src/views/Follow/Tabs/index.tsx new file mode 100644 index 0000000000..67ee453013 --- /dev/null +++ b/src/views/Follow/Tabs/index.tsx @@ -0,0 +1,40 @@ +import { useIntl } from 'react-intl' + +import { SquareTabs } from '~/components' + +import styles from './styles.module.css' + +export type TABS = 'All' | 'Article' + +type TabsProps = { + tab: TABS + setTab: React.Dispatch> +} + +export const Tabs = ({ tab, setTab }: TabsProps) => { + const intl = useIntl() + + return ( +
+ + setTab('All')} + title={intl.formatMessage({ + defaultMessage: 'All', + id: 'zQvVDJ', + })} + /> + setTab('Article')} + title={intl.formatMessage({ + defaultMessage: 'Articles', + id: 'AeIRlL', + description: 'src/views/Follow/Tabs/index.tsx', + })} + /> + +
+ ) +} diff --git a/src/views/Follow/Tabs/styles.module.css b/src/views/Follow/Tabs/styles.module.css new file mode 100644 index 0000000000..a986decf9d --- /dev/null +++ b/src/views/Follow/Tabs/styles.module.css @@ -0,0 +1,3 @@ +.tabs { + margin-top: var(--sp16); +} diff --git a/src/views/Follow/index.tsx b/src/views/Follow/index.tsx index 460f02fd0f..d11c675914 100644 --- a/src/views/Follow/index.tsx +++ b/src/views/Follow/index.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@apollo/react-hooks' import gql from 'graphql-tag' -import { useContext, useEffect } from 'react' +import { useContext, useEffect, useState } from 'react' import { FormattedMessage } from 'react-intl' import { @@ -15,6 +15,7 @@ import { updateViewerUnreadFollowing } from '~/components/GQL' import { MeFollowQuery, ReadFollowingFeedMutation } from '~/gql/graphql' import Feed from './Feed' +import { TABS, Tabs } from './Tabs' const READ_FOLLOWING = gql` mutation ReadFollowingFeed { @@ -35,7 +36,11 @@ const ME_FOLLOW = gql` } ` -const BaseFollow = () => { +type BaseFollowProps = { + tab: TABS +} + +const BaseFollow = ({ tab }: BaseFollowProps) => { const viewer = useContext(ViewerContext) const [readFollowing] = useMutation( READ_FOLLOWING, @@ -59,10 +64,11 @@ const BaseFollow = () => { return null } - return + return } const Follow = () => { + const [tab, setTab] = useState('All') return ( @@ -79,7 +85,9 @@ const Follow = () => { - + + + ) diff --git a/src/views/Home/Feed/IcymiCuratedFeed/index.tsx b/src/views/Home/Feed/IcymiCuratedFeed/index.tsx index 38c25f4e78..8157212d80 100644 --- a/src/views/Home/Feed/IcymiCuratedFeed/index.tsx +++ b/src/views/Home/Feed/IcymiCuratedFeed/index.tsx @@ -116,6 +116,7 @@ export const IcymiCuratedFeed = ({ recommendation }: IcymiCuratedFeed) => { onClickArticle('article', cardArticleNum + i, article.id) } diff --git a/src/views/Home/Feed/MainFeed/index.tsx b/src/views/Home/Feed/MainFeed/index.tsx index f29f529596..6d76bea0a1 100644 --- a/src/views/Home/Feed/MainFeed/index.tsx +++ b/src/views/Home/Feed/MainFeed/index.tsx @@ -87,7 +87,7 @@ const horizontalFeeds: FeedLocation = { const MainFeed = ({ feedSortType: sortBy }: MainFeedProps) => { const viewer = useContext(ViewerContext) const isHottestFeed = sortBy === 'hottest' - + const isIcymiFeed = sortBy === 'icymi' /** * Data Fetching */ @@ -229,7 +229,8 @@ const MainFeed = ({ feedSortType: sortBy }: MainFeedProps) => { article={edge.node} hasReadTime={true} hasDonationCount={true} - includesMetaData={sortBy !== 'icymi'} + includesMetaData={!isIcymiFeed} // only include metadata for non-icymi feeds + excludesTimeStamp={isIcymiFeed} // only exclude timestamp for icymi feed onClick={() => analytics.trackEvent('click_feed', { type: sortBy, diff --git a/src/views/Me/DraftDetail/BottomBar.tsx b/src/views/Me/DraftDetail/BottomBar.tsx index 3f4908c869..d0fe9bb5fd 100644 --- a/src/views/Me/DraftDetail/BottomBar.tsx +++ b/src/views/Me/DraftDetail/BottomBar.tsx @@ -8,14 +8,20 @@ import { ToggleAccessProps, } from '~/components/Editor' import BottomBar from '~/components/Editor/BottomBar' +import { + getSelectCampaign, + SelectCampaignProps, +} from '~/components/Editor/SelectCampaign' import SupportSettingDialog from '~/components/Editor/ToggleAccess/SupportSettingDialog' import { DigestRichCirclePublicFragment, EditMetaDraftFragment, + EditorSelectCampaignFragment, } from '~/gql/graphql' import { useEditDraftAccess, + useEditDraftCampaign, useEditDraftCanComment, useEditDraftCollection, useEditDraftCover, @@ -28,9 +34,14 @@ import { interface BottomBarProps { draft: EditMetaDraftFragment ownCircles?: DigestRichCirclePublicFragment[] + campaigns?: EditorSelectCampaignFragment[] } -const EditDraftBottomBar = ({ draft, ownCircles }: BottomBarProps) => { +const EditDraftBottomBar = ({ + draft, + ownCircles, + campaigns, +}: BottomBarProps) => { const { edit: editCollection, saving: collectionSaving } = useEditDraftCollection() const { edit: editCover, saving: coverSaving, refetch } = useEditDraftCover() @@ -50,9 +61,17 @@ const EditDraftBottomBar = ({ draft, ownCircles }: BottomBarProps) => { const { edit: editSupport, saving: supportSaving } = useEditSupportSetting() + const { edit: editCampaign, saving: campaignSaving } = useEditDraftCampaign() + const hasOwnCircle = ownCircles && ownCircles.length >= 1 const tags = (draft.tags || []).map(toDigestTagPlaceholder) + const { appliedCampaign, selectedStage } = getSelectCampaign({ + applied: campaigns && campaigns[0], + attached: draft.campaigns, + createdAt: draft.createdAt, + }) + const coverProps: SetCoverProps = { cover: draft.cover, assets: draft.assets, @@ -72,7 +91,9 @@ const EditDraftBottomBar = ({ draft, ownCircles }: BottomBarProps) => { editCollection, collectionSaving, } - const accessProps: ToggleAccessProps & SetResponseProps = { + const accessProps: ToggleAccessProps & + SetResponseProps & + Partial = { circle: draft?.access.circle, accessType: draft.access.type, license: draft.license, @@ -92,6 +113,10 @@ const EditDraftBottomBar = ({ draft, ownCircles }: BottomBarProps) => { canComment, toggleComment, + + appliedCampaign, + selectedStage, + editCampaign, } return ( @@ -108,7 +133,8 @@ const EditDraftBottomBar = ({ draft, ownCircles }: BottomBarProps) => { coverSaving || tagsSaving || accessSaving || - toggleCommentSaving + toggleCommentSaving || + campaignSaving } {...coverProps} {...tagsProps} diff --git a/src/views/Me/DraftDetail/PublishState/PublishedState.tsx b/src/views/Me/DraftDetail/PublishState/PublishedState.tsx index 080304cd5a..51e237be24 100644 --- a/src/views/Me/DraftDetail/PublishState/PublishedState.tsx +++ b/src/views/Me/DraftDetail/PublishState/PublishedState.tsx @@ -1,4 +1,3 @@ -import { useRouter } from 'next/router' import { useEffect } from 'react' import { FormattedMessage } from 'react-intl' @@ -21,8 +20,6 @@ const BasePublishedState = ({ } const PublishedState = ({ draft }: { draft: PublishStateDraftFragment }) => { - const router = useRouter() - // refetch /me/drafts on published const refetch = useImperativeQuery(ME_DRAFTS_FEED, { variables: { id: draft.id }, @@ -70,7 +67,10 @@ const PublishedState = ({ draft }: { draft: PublishStateDraftFragment }) => { description="src/views/Me/DraftDetail/PublishState/PublishedState.tsx" /> } - onClick={() => router.replace(path.href)} + // onClick={() => router.replace(path.href)} + onClick={() => { + window.location.href = path.href + }} /> } smUpBtns={ @@ -82,7 +82,10 @@ const PublishedState = ({ draft }: { draft: PublishStateDraftFragment }) => { description="src/views/Me/DraftDetail/PublishState/PublishedState.tsx" /> } - onClick={() => router.replace(path.href)} + // onClick={() => router.replace(path.href)} + onClick={() => { + window.location.href = path.href + }} /> } > diff --git a/src/views/Me/DraftDetail/SettingsButton/index.tsx b/src/views/Me/DraftDetail/SettingsButton/index.tsx index cadd08d976..ffb2589ef3 100644 --- a/src/views/Me/DraftDetail/SettingsButton/index.tsx +++ b/src/views/Me/DraftDetail/SettingsButton/index.tsx @@ -9,14 +9,20 @@ import { SetTagsProps, ToggleAccessProps, } from '~/components/Editor' +import { + getSelectCampaign, + SelectCampaignProps, +} from '~/components/Editor/SelectCampaign' import { EditorSettingsDialog } from '~/components/Editor/SettingsDialog' import { DigestRichCirclePublicFragment, EditMetaDraftFragment, + EditorSelectCampaignFragment, } from '~/gql/graphql' import { useEditDraftAccess, + useEditDraftCampaign, useEditDraftCanComment, useEditDraftCollection, useEditDraftCover, @@ -30,6 +36,7 @@ import ConfirmPublishDialogContent from './ConfirmPublishDialogContent' interface SettingsButtonProps { draft: EditMetaDraftFragment ownCircles?: DigestRichCirclePublicFragment[] + campaigns?: EditorSelectCampaignFragment[] publishable?: boolean } @@ -57,6 +64,7 @@ const ConfirmButton = ({ const SettingsButton = ({ draft, ownCircles, + campaigns, publishable, }: SettingsButtonProps) => { const { edit: editCollection, saving: collectionSaving } = @@ -70,6 +78,7 @@ const SettingsButton = ({ const { edit: editAccess, saving: accessSaving } = useEditDraftAccess( ownCircles && ownCircles[0] ) + const { edit: editCampaign } = useEditDraftCampaign() const { edit: editSupport, saving: supportSaving } = useEditSupportSetting() @@ -121,6 +130,18 @@ const SettingsButton = ({ iscnPublishSaving, } + const { appliedCampaign, selectedStage } = getSelectCampaign({ + applied: campaigns && campaigns[0], + attached: draft.campaigns, + createdAt: draft.createdAt, + }) + + const campaignProps: Partial = { + appliedCampaign, + selectedStage, + editCampaign, + } + const responseProps: SetResponseProps = { canComment, toggleComment, @@ -148,6 +169,7 @@ const SettingsButton = ({ {...collectionProps} {...accessProps} {...responseProps} + {...campaignProps} > {({ openDialog: openEditorSettingsDialog }) => ( { ) } +const EditDraftCampaign = ({ draft, campaigns }: SidebarProps) => { + const { edit } = useEditDraftCampaign() + + const { appliedCampaign, selectedStage } = getSelectCampaign({ + applied: campaigns && campaigns[0], + attached: draft.campaigns, + createdAt: draft.createdAt, + }) + + return ( + + ) +} + const EditDraftSidebar = (props: BaseSidebarProps) => { const isPending = props.draft.publishState === 'pending' const isPublished = props.draft.publishState === 'published' @@ -130,6 +152,7 @@ const EditDraftSidebar = (props: BaseSidebarProps) => { return (
+ diff --git a/src/views/Me/DraftDetail/gql.ts b/src/views/Me/DraftDetail/gql.ts index 0445522969..0ea44d7214 100644 --- a/src/views/Me/DraftDetail/gql.ts +++ b/src/views/Me/DraftDetail/gql.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag' import { ArticleDigestDropdown, CircleDigest } from '~/components' import { fragments as EditorFragments } from '~/components/Editor/fragments' +import SelectCampaign from '~/components/Editor/SelectCampaign' import assetFragment from '~/components/GQL/fragments/asset' import PublishState from './PublishState' @@ -13,6 +14,7 @@ export const editMetaFragment = gql` fragment EditMetaDraft on Draft { id publishState + createdAt cover assets { ...Asset @@ -31,6 +33,14 @@ export const editMetaFragment = gql` ...DigestRichCirclePublic } } + campaigns { + campaign { + id + } + stage { + id + } + } license requestForDonation replyToDonator @@ -46,10 +56,18 @@ export const editMetaFragment = gql` /** * Fetch draft detail or assets only */ -export const DRAFT_DETAIL_CIRCLES = gql` - query DraftDetailCirclesQuery { +export const DRAFT_DETAIL_VIEWER = gql` + query DraftDetailViewerQuery { viewer { id + campaigns(input: { first: 1 }) { + edges { + node { + id + ...EditorSelectCampaign + } + } + } ownCircles { ...DigestRichCirclePublic } @@ -58,6 +76,7 @@ export const DRAFT_DETAIL_CIRCLES = gql` } } ${CircleDigest.Rich.fragments.circle.public} + ${SelectCampaign.fragments} ` export const DRAFT_DETAIL = gql` @@ -230,3 +249,36 @@ export const SET_ACCESS = gql` } ${CircleDigest.Rich.fragments.circle.public} ` + +export const SET_CAMPAIGN = gql` + mutation SetDraftCampaign( + $id: ID! + $campaigns: [ArticleCampaignInput!] + $isReset: Boolean! + ) { + setDraftCampaign: putDraft(input: { id: $id, campaigns: $campaigns }) + @skip(if: $isReset) { + id + campaigns { + campaign { + id + } + stage { + id + } + } + } + resetDraftCampaign: putDraft(input: { id: $id, campaigns: [] }) + @include(if: $isReset) { + id + campaigns { + campaign { + id + } + stage { + id + } + } + } + } +` diff --git a/src/views/Me/DraftDetail/hooks.ts b/src/views/Me/DraftDetail/hooks.ts index d4d4c3c0a7..cee67182e9 100644 --- a/src/views/Me/DraftDetail/hooks.ts +++ b/src/views/Me/DraftDetail/hooks.ts @@ -23,6 +23,7 @@ import { import { DRAFT_ASSETS, SET_ACCESS, + SET_CAMPAIGN, SET_CAN_COMMENT, SET_COLLECTION, SET_COVER, @@ -59,6 +60,7 @@ export const useEditDraftCover = () => { } return { + // FIXME: TS any edit: async (props: any) => addRequest(() => createDraftAndEdit(props)), saving, refetch, @@ -274,3 +276,38 @@ export const useEditDraftCanComment = () => { saving, } } + +export const useEditDraftCampaign = () => { + const { addRequest, getDraftId } = useContext(DraftDetailStateContext) + const { createDraft } = useCreateDraft() + const [update, { loading: saving }] = + useMutation(SET_CAMPAIGN) + + const edit = ( + selected?: { campaign: string; stage: string }, + newId?: string + ) => + update({ + variables: { + id: newId || getDraftId(), + campaigns: selected ? [selected] : [], + isReset: !selected, + }, + }) + + const createDraftAndEdit = async (selected: { + campaign: string + stage: string + }) => { + if (getDraftId()) return edit(selected) + + return createDraft({ + onCreate: (newDraftId) => edit(selected, newDraftId), + }) + } + + return { + edit: async (props: any) => addRequest(() => createDraftAndEdit(props)), + saving: saving, + } +} diff --git a/src/views/Me/DraftDetail/index.tsx b/src/views/Me/DraftDetail/index.tsx index 03e1ec6351..fb83a43dd6 100644 --- a/src/views/Me/DraftDetail/index.tsx +++ b/src/views/Me/DraftDetail/index.tsx @@ -34,15 +34,15 @@ import { ArticleLicenseType, DirectImageUploadDoneMutation, DirectImageUploadMutation, - DraftDetailCirclesQueryQuery, DraftDetailQueryQuery, + DraftDetailViewerQueryQuery, PublishState as PublishStateType, SetDraftContentMutation, SingleFileUploadMutation, } from '~/gql/graphql' import BottomBar from './BottomBar' -import { DRAFT_DETAIL, DRAFT_DETAIL_CIRCLES, SET_CONTENT } from './gql' +import { DRAFT_DETAIL, DRAFT_DETAIL_VIEWER, SET_CONTENT } from './gql' import PublishState from './PublishState' import SaveStatus from './SaveStatus' import SettingsButton from './SettingsButton' @@ -60,6 +60,7 @@ const Editor = dynamic( const EMPTY_DRAFT: DraftDetailQueryQuery['node'] = { id: '', title: '', + createdAt: new Date().toISOString(), publishState: PublishStateType.Unpublished, content: '', summary: '', @@ -84,6 +85,7 @@ const EMPTY_DRAFT: DraftDetailQueryQuery['node'] = { sensitiveByAuthor: false, iscnPublish: null, canComment: true, + campaigns: [], } const BaseDraftDetail = () => { @@ -119,19 +121,22 @@ const BaseDraftDetail = () => { skip: isNewDraft(), } ) - const { data: circleData, loading: circleLoading } = - useQuery(DRAFT_DETAIL_CIRCLES, { + const { data: viewerData, loading: viewerLoading } = + useQuery(DRAFT_DETAIL_VIEWER, { fetchPolicy: 'network-only', }) const draft = (data?.node?.__typename === 'Draft' && data.node) || EMPTY_DRAFT - const ownCircles = circleData?.viewer?.ownCircles || undefined + const ownCircles = viewerData?.viewer?.ownCircles || undefined + const appliedCampaigns = viewerData?.viewer?.campaigns.edges?.map( + (e) => e.node + ) const [contentLength, setContentLength] = useState(0) const isOverLength = contentLength > MAX_ARTICLE_CONTENT_LENGTH useUnloadConfirm({ block: saveStatus === 'saving' && !isNewDraft() }) - if ((loading && !initNew) || circleLoading) { + if ((loading && !initNew) || viewerLoading) { return ( @@ -292,7 +297,11 @@ const BaseDraftDetail = () => { - + } > @@ -311,6 +320,7 @@ const BaseDraftDetail = () => { {draft && ( @@ -341,7 +351,11 @@ const BaseDraftDetail = () => { - + ) diff --git a/src/views/Me/Notifications/index.tsx b/src/views/Me/Notifications/index.tsx index 7a82f5a387..f9fd3af9c4 100644 --- a/src/views/Me/Notifications/index.tsx +++ b/src/views/Me/Notifications/index.tsx @@ -3,7 +3,7 @@ import gql from 'graphql-tag' import { useEffect } from 'react' import { FormattedMessage, useIntl } from 'react-intl' -import { mergeConnections } from '~/common/utils' +import { mergeConnections, shouldRenderNode } from '~/common/utils' import { EmptyNotice, Head, @@ -22,30 +22,16 @@ import { MeNotificationsQuery, } from '~/gql/graphql' -type NoticeNode = { - __typename?: string - id?: string -} - -function isSpecificNoticeType( - node: NoticeNode -): node is NoticeNode & { id: string } { - const validTypes = new Set([ - 'ArticleArticleNotice', - 'CircleNotice', - 'ArticleNotice', - 'CommentCommentNotice', - 'CommentNotice', - 'OfficialAnnouncementNotice', - 'TransactionNotice', - 'UserNotice', - ]) - return ( - node.__typename !== undefined && - validTypes.has(node.__typename) && - Boolean(node.id) - ) -} +const renderableTypes = new Set([ + 'ArticleArticleNotice', + 'CircleNotice', + 'ArticleNotice', + 'CommentCommentNotice', + 'CommentNotice', + 'OfficialAnnouncementNotice', + 'TransactionNotice', + 'UserNotice', +]) const ME_NOTIFICATIONS = gql` query MeNotifications($after: String) { @@ -120,7 +106,7 @@ const BaseNotifications = () => { {edges.map( ({ node }) => - isSpecificNoticeType(node) && ( + shouldRenderNode(node, renderableTypes) && ( diff --git a/src/views/MomentDetail/index.tsx b/src/views/MomentDetail/index.tsx new file mode 100644 index 0000000000..ceda5482d7 --- /dev/null +++ b/src/views/MomentDetail/index.tsx @@ -0,0 +1,28 @@ +import { Dialog, Media, useRoute } from '~/components' +import Content from '~/components/Dialogs/MomentDetailDialog/Content' + +const MomentDetail = () => { + const { getQuery } = useRoute() + const shortHash = getQuery('shortHash') + const onClose = () => {} + + return ( + <> + mobile moment detail page + + + + + + + ) +} + +export default MomentDetail diff --git a/src/views/TagDetail/Followers/styles.module.css b/src/views/TagDetail/Followers/styles.module.css index 88497f0ad5..54802239b0 100644 --- a/src/views/TagDetail/Followers/styles.module.css +++ b/src/views/TagDetail/Followers/styles.module.css @@ -13,32 +13,3 @@ color: var(--color-black); } } - -.avatarList { - @mixin flex-center-start; - - & > * { - position: relative; - box-shadow: 0 0 0 2px var(--color-white); - - &:nth-child(1) { - z-index: 5; - } - - &:nth-child(2) { - z-index: 4; - } - - &:nth-child(3) { - z-index: 3; - } - - &:nth-child(4) { - z-index: 2; - } - - &:nth-child(5) { - z-index: 1; - } - } -} diff --git a/src/views/User/UserProfile/AsideUserProfile/index.tsx b/src/views/User/UserProfile/AsideUserProfile/index.tsx index 508a8bd06e..9db11ca680 100644 --- a/src/views/User/UserProfile/AsideUserProfile/index.tsx +++ b/src/views/User/UserProfile/AsideUserProfile/index.tsx @@ -1,10 +1,11 @@ import dynamic from 'next/dynamic' -import { useContext, useEffect, useState } from 'react' +import { useContext, useEffect } from 'react' import { FormattedMessage } from 'react-intl' import { ReactComponent as IconCamera } from '@/public/static/icons/24px/camera.svg' import { - OPEN_SHOW_NOMAD_BADGE_DIALOG, + OPEN_GRAND_BADGE_DIALOG, + OPEN_NOMAD_BADGE_DIALOG, TEST_ID, URL_USER_PROFILE, } from '~/common/enums' @@ -23,13 +24,14 @@ import { } from '~/components' import { UserProfileUserPublicQuery } from '~/gql/graphql' +import { BadgeGrandDialog } from '../BadgeGrandDialog' import { BadgeNomadDialog } from '../BadgeNomadDialog' -import { BadgeNomadLabel } from '../BadgeNomadLabel' import { ArchitectBadge, CivicLikerBadge, GoldenMotorBadge, - // NomadBadge, + GrandBadge, + NomadBadge, SeedBadge, TraveloggersBadge, } from '../Badges' @@ -51,10 +53,6 @@ export const AsideUserProfile = () => { // public user data const userName = getQuery('name') - const showBadges = - getQuery(URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key) === - URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value - const [hasShowBadges, setHasShowBadges] = useState(false) const isInUserPage = isInPath('USER_ARTICLES') || isInPath('USER_COLLECTIONS') const isMe = !userName || viewer.userName === userName @@ -62,10 +60,6 @@ export const AsideUserProfile = () => { page: 'userProfile', userName, }) - const shareLink = - typeof window !== 'undefined' - ? `${window.location.origin}${userProfilePath.href}?${URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key}=${URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value}` - : '' const { data, loading, client } = usePublicQuery( USER_PROFILE_PUBLIC, @@ -89,10 +83,22 @@ export const AsideUserProfile = () => { }, [user?.id, viewer.id]) useEffect(() => { - if (showBadges) { - window.dispatchEvent(new CustomEvent(OPEN_SHOW_NOMAD_BADGE_DIALOG)) + const shouldOpenNomadBadgeDialog = + getQuery(URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key) === + URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value + + if (shouldOpenNomadBadgeDialog) { + window.dispatchEvent(new CustomEvent(OPEN_NOMAD_BADGE_DIALOG)) + } + + const shouldOpenGrandBadgeDialog = + getQuery(URL_USER_PROFILE.OPEN_GRAND_BADGE_DIALOG.key) === + URL_USER_PROFILE.OPEN_GRAND_BADGE_DIALOG.value + + if (shouldOpenGrandBadgeDialog) { + window.dispatchEvent(new CustomEvent(OPEN_GRAND_BADGE_DIALOG)) } - }, [showBadges]) + }, []) /** * Render @@ -118,6 +124,7 @@ export const AsideUserProfile = () => { const nomadBadgeLevel = ( hasNomadBadge ? Number.parseInt(nomadBadgeType[0].type.charAt(5)) : 1 ) as 1 | 2 | 3 | 4 + const hasGrandBadge = badges.some((b) => b.type === 'grand_slam') const userState = user.status?.state as string const isCivicLiker = user.liker.civicLiker @@ -249,28 +256,23 @@ export const AsideUserProfile = () => { user?.info.ethAddress) && (
{hasNomadBadge && ( - - {({ openDialog }) => { - if (showBadges && !hasShowBadges) { - setTimeout(() => { - openDialog() - // FIXED: infinite loop render of BadgeNomadDialog - setHasShowBadges(true) - }) - } - return ( - - ) - }} + + {({ openDialog }) => ( + + )} )} + {hasGrandBadge && ( + + {({ openDialog }) => ( + + )} + + )} {hasTraveloggersBadge && } {hasSeedBadge && } {hasGoldenMotorBadge && } diff --git a/src/views/User/UserProfile/AsideUserProfile/styles.module.css b/src/views/User/UserProfile/AsideUserProfile/styles.module.css index 1eacafa10b..a57e864fea 100644 --- a/src/views/User/UserProfile/AsideUserProfile/styles.module.css +++ b/src/views/User/UserProfile/AsideUserProfile/styles.module.css @@ -78,6 +78,7 @@ .badges { display: flex; + align-items: center; & > * + * { margin-left: var(--sp12); diff --git a/src/views/User/UserProfile/BadgeGrandDialog/Content.tsx b/src/views/User/UserProfile/BadgeGrandDialog/Content.tsx new file mode 100644 index 0000000000..8a243cea89 --- /dev/null +++ b/src/views/User/UserProfile/BadgeGrandDialog/Content.tsx @@ -0,0 +1,146 @@ +import { FormattedMessage } from 'react-intl' + +import { ReactComponent as IconLeft } from '@/public/static/icons/24px/left.svg' +import { ReactComponent as GrandBackground } from '@/public/static/images/badge-grand-background.svg' +import { URL_USER_PROFILE } from '~/common/enums' +import { toPath } from '~/common/utils' +import { Button, CopyToClipboard, Dialog, Icon, useRoute } from '~/components' + +import styles from './styles.module.css' + +type BadgeGrandDialogContentProps = { + closeDialog: () => void + goBack?: () => void +} + +const BadgeGrandDialogContent = ({ + closeDialog, + goBack, +}: BadgeGrandDialogContentProps) => { + const { getQuery } = useRoute() + const userName = getQuery('name') + const userProfilePath = toPath({ + page: 'userProfile', + userName, + }) + const shareLink = + typeof window !== 'undefined' + ? `${window.location.origin}${userProfilePath.href}?${URL_USER_PROFILE.OPEN_GRAND_BADGE_DIALOG.key}=${URL_USER_PROFILE.OPEN_GRAND_BADGE_DIALOG.value}` + : '' + const isCongrats = + getQuery(URL_USER_PROFILE.GRAND_BADGE_DIALOG_STEP.key) === + URL_USER_PROFILE.GRAND_BADGE_DIALOG_STEP.value + + return ( + <> + {goBack && ( + } + leftBtn={ + + } + /> + )} + + +
+
+ +
+ + +

+ {isCongrats ? ( + + ) : ( + + )} +

+

+ {isCongrats ? ( + + ) : ( + + )} +

+
+
+
+ + + } + > + {({ copyToClipboard }) => ( + } + color="greyDarker" + onClick={copyToClipboard} + /> + )} + + } + smUpBtns={ + <> + + ) : ( + + ) + } + color="greyDarker" + onClick={goBack || closeDialog} + /> + + + } + > + {({ copyToClipboard }) => ( + } + color="green" + onClick={copyToClipboard} + /> + )} + + + } + /> + + ) +} + +export default BadgeGrandDialogContent diff --git a/src/views/User/UserProfile/BadgeGrandDialog/index.tsx b/src/views/User/UserProfile/BadgeGrandDialog/index.tsx new file mode 100644 index 0000000000..36cb669048 --- /dev/null +++ b/src/views/User/UserProfile/BadgeGrandDialog/index.tsx @@ -0,0 +1,48 @@ +import dynamic from 'next/dynamic' + +import { OPEN_GRAND_BADGE_DIALOG } from '~/common/enums' +import { + Dialog, + SpinnerBlock, + useDialogSwitch, + useEventListener, +} from '~/components' + +type BadgeGrandProps = {} + +type BadgeGrandDialogProps = BadgeGrandProps & { + children: ({ openDialog }: { openDialog: () => void }) => React.ReactNode +} + +const DynamicContent = dynamic(() => import('./Content'), { + loading: () => , +}) + +export const BaseBadgeGrandDialog: React.FC = ({ + children, +}) => { + const { show, openDialog, closeDialog } = useDialogSwitch(true) + + return ( + <> + {children({ openDialog })} + + + + + + ) +} + +export const BadgeGrandDialog = (props: BadgeGrandDialogProps) => { + const Children = ({ openDialog }: { openDialog: () => void }) => { + useEventListener(OPEN_GRAND_BADGE_DIALOG, openDialog) + return <>{props.children && props.children({ openDialog })} + } + + return ( + }> + {({ openDialog }) => } + + ) +} diff --git a/src/views/User/UserProfile/BadgeGrandDialog/styles.module.css b/src/views/User/UserProfile/BadgeGrandDialog/styles.module.css new file mode 100644 index 0000000000..b0092ff770 --- /dev/null +++ b/src/views/User/UserProfile/BadgeGrandDialog/styles.module.css @@ -0,0 +1,19 @@ +.container { + padding: var(--sp16) 0 0; + + @media (--sm-up) { + padding: var(--sp32) 0 var(--sp40); + } +} + +.badgeIcon { + text-align: center; + + & svg { + height: 215px; + } +} + +.title { + margin: var(--sp16) 0; +} diff --git a/src/views/User/UserProfile/BadgeNomadDialog/Content.tsx b/src/views/User/UserProfile/BadgeNomadDialog/Content.tsx index ef913d1abd..44e9261751 100644 --- a/src/views/User/UserProfile/BadgeNomadDialog/Content.tsx +++ b/src/views/User/UserProfile/BadgeNomadDialog/Content.tsx @@ -5,28 +5,37 @@ import { ReactComponent as Nomad1Background } from '@/public/static/images/badge import { ReactComponent as Nomad2Background } from '@/public/static/images/badge-nomad2-background.svg' import { ReactComponent as Nomad3Background } from '@/public/static/images/badge-nomad3-background.svg' import { ReactComponent as Nomad4Background } from '@/public/static/images/badge-nomad4-background.svg' -import { Button, CopyToClipboard, Dialog, Icon } from '~/components' +import { URL_USER_PROFILE } from '~/common/enums' +import { toPath } from '~/common/utils' +import { Button, CopyToClipboard, Dialog, Icon, useRoute } from '~/components' import styles from './styles.module.css' type BadgeNomadDialogContentProps = { - closeDialog: () => void nomadBadgeLevel: 1 | 2 | 3 | 4 - shareLink: string - isNested?: boolean + closeDialog: () => void goBack?: () => void } const BadgeNomadDialogContent = ({ closeDialog, nomadBadgeLevel, - shareLink, - isNested, goBack, }: BadgeNomadDialogContentProps) => { + const { getQuery } = useRoute() + const userName = getQuery('name') + const userProfilePath = toPath({ + page: 'userProfile', + userName, + }) + const shareLink = + typeof window !== 'undefined' + ? `${window.location.origin}${userProfilePath.href}?${URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key}=${URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value}` + : '' + return ( <> - {isNested && goBack && ( + {goBack && ( } leftBtn={ @@ -125,14 +134,14 @@ const BadgeNomadDialogContent = ({ <> ) : ( ) } color="greyDarker" - onClick={isNested ? goBack : closeDialog} + onClick={goBack || closeDialog} /> import('./Content'), { export const BaseBadgeNomadDialog: React.FC = ({ children, nomadBadgeLevel, - shareLink, }) => { const { show, openDialog, closeDialog } = useDialogSwitch(true) @@ -30,15 +34,21 @@ export const BaseBadgeNomadDialog: React.FC = ({ ) } -export const BadgeNomadDialog = (props: BadgeNomadDialogProps) => ( - }> - {({ openDialog }) => <>{props.children({ openDialog })}} - -) +export const BadgeNomadDialog = (props: BadgeNomadDialogProps) => { + const Children = ({ openDialog }: { openDialog: () => void }) => { + useEventListener(OPEN_NOMAD_BADGE_DIALOG, openDialog) + return <>{props.children && props.children({ openDialog })} + } + + return ( + }> + {({ openDialog }) => } + + ) +} diff --git a/src/views/User/UserProfile/BadgeNomadLabel/index.tsx b/src/views/User/UserProfile/BadgeNomadLabel/index.tsx deleted file mode 100644 index 72436f5456..0000000000 --- a/src/views/User/UserProfile/BadgeNomadLabel/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { FormattedMessage } from 'react-intl' - -import { Tooltip } from '~/components' - -import { NomadBadge } from '../Badges' - -type BadgeNomadLabelProps = { - hasTooltip?: boolean - nomadBadgeLevel: 1 | 2 | 3 | 4 - onClick?: () => void -} - -export const BadgeNomadLabel: React.FC = ({ - hasTooltip, - nomadBadgeLevel, - onClick, -}) => { - const Content = ( - - ) - - return ( - <> - {hasTooltip && ( - - ) : nomadBadgeLevel === 3 ? ( - - ) : nomadBadgeLevel === 2 ? ( - - ) : ( - - ) - } - placement="top" - > - {Content} - - )} - - {!hasTooltip && Content} - - ) -} diff --git a/src/views/User/UserProfile/Badges/index.tsx b/src/views/User/UserProfile/Badges/index.tsx index 04c6db3a49..4fd68cba35 100644 --- a/src/views/User/UserProfile/Badges/index.tsx +++ b/src/views/User/UserProfile/Badges/index.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames' import { FormattedMessage } from 'react-intl' +import { ReactComponent as IconGrand } from '@/public/static/icons/24px/badge-grand.svg' import { ReactComponent as IconNomad1Badge } from '@/public/static/icons/24px/badge-nomad1-moon.svg' import { ReactComponent as IconNomad2Badge } from '@/public/static/icons/24px/badge-nomad2-star.svg' import { ReactComponent as IconNomad3Badge } from '@/public/static/icons/24px/badge-nomad3-light.svg' @@ -11,210 +12,115 @@ import { ReactComponent as IconArchitectBadge } from '@/public/static/icons/24px import { ReactComponent as IconRight } from '@/public/static/icons/24px/right.svg' import { ReactComponent as IconSeedBadge } from '@/public/static/icons/24px/seed-user.svg' import { ReactComponent as IconTraveloggersBadge } from '@/public/static/icons/24px/traveloggers.svg' -import { Icon, Tooltip, Translate, withIcon } from '~/components' +import { ReactComponent as IconGrand48 } from '@/public/static/icons/48px/badge-grand.svg' +import { Icon, Tooltip, WrappedIcon } from '~/components' -// import { BadgeNomadDialog } from '../BadgeNomadLabel' import styles from './styles.module.css' -type badgePros = { +type BadgePros = { isInDialog?: boolean hasTooltip?: boolean + onClick?: () => void } -export const SeedBadge = ({ isInDialog, hasTooltip }: badgePros) => { - const copy = ( - - ) - if (isInDialog) { - return ( -
- <>{withIcon(IconSeedBadge)({ size: 48 })} -
{copy}
-
- ) - } - - if (hasTooltip) { - return ( - - - {withIcon(IconSeedBadge)({ size: 20 })} - - - ) - } - - return ( - - {withIcon(IconSeedBadge)({ size: 20 })} - - ) -} - -export const GoldenMotorBadge = ({ isInDialog, hasTooltip }: badgePros) => { - const copy = ( - - ) - if (isInDialog) { - return ( -
- <>{withIcon(IconGoldenMotorBadge)({ size: 48 })} -
{copy}
-
- ) - } - - if (hasTooltip) { - return ( - - - {withIcon(IconGoldenMotorBadge)({ size: 20 })} - - - ) - } - - return ( - - {withIcon(IconGoldenMotorBadge)({ size: 20 })} - - ) -} +const withBadge = ({ + icon, + name, +}: { + icon: WrappedIcon + name: string | React.ReactNode +}) => { + const Badge = ({ isInDialog, hasTooltip }: BadgePros) => { + if (isInDialog) { + return ( +
+ +
{name}
+
+ ) + } -export const ArchitectBadge = ({ isInDialog, hasTooltip }: badgePros) => { - const copy = ( - - ) - if (isInDialog) { - return ( -
- <>{withIcon(IconArchitectBadge)({ size: 48 })} -
{copy}
-
- ) - } + if (hasTooltip) { + return ( + + + + + + ) + } - if (hasTooltip) { return ( - - - {withIcon(IconArchitectBadge)({ size: 20 })} - - + + + ) } - return ( - - {withIcon(IconArchitectBadge)({ size: 20 })} - - ) + return Badge } -export const CivicLikerBadge = ({ isInDialog, hasTooltip }: badgePros) => { - const copy = ( - - ) - if (isInDialog) { - return ( -
- <>{withIcon(IconCivicLikerBadge)({ size: 48 })} -
{copy}
-
- ) - } +export const SeedBadge = withBadge({ + icon: IconSeedBadge, + name: , +}) - if (hasTooltip) { - return ( - - - {withIcon(IconCivicLikerBadge)({ size: 20 })} - - - ) - } +export const GoldenMotorBadge = withBadge({ + icon: IconGoldenMotorBadge, + name: ( + + ), +}) - return ( - - {withIcon(IconCivicLikerBadge)({ size: 20 })} - - ) -} +export const ArchitectBadge = withBadge({ + icon: IconArchitectBadge, + name: , +}) -export const TraveloggersBadge = ({ isInDialog, hasTooltip }: badgePros) => { - const copy = ( - - ) - if (isInDialog) { - return ( -
- <>{withIcon(IconTraveloggersBadge)({ size: 48 })} -
{copy}
-
- ) - } - if (hasTooltip) { - return ( - - - {withIcon(IconTraveloggersBadge)({ size: 20 })} - - - ) - } +export const CivicLikerBadge = withBadge({ + icon: IconCivicLikerBadge, + name: , +}) - return ( - - {withIcon(IconTraveloggersBadge)({ size: 20 })} - - ) -} +export const TraveloggersBadge = withBadge({ + icon: IconTraveloggersBadge, + name: 'Traveloggers', +}) export const NomadBadge = ({ isInDialog, hasTooltip, + onClick, level, gotoNomadBadge, -}: badgePros & { +}: BadgePros & { level: 1 | 2 | 3 | 4 gotoNomadBadge?: () => void }) => { - const copy = ( - - ) - - let withIconComp = withIcon(IconNomad1Badge) + let icon = IconNomad1Badge switch (level) { case 2: - withIconComp = withIcon(IconNomad2Badge) + icon = IconNomad2Badge break case 3: - withIconComp = withIcon(IconNomad3Badge) + icon = IconNomad3Badge break case 4: - withIconComp = withIcon(IconNomad4Badge) + icon = IconNomad4Badge break } + // make height auto since the icon has a border outside + const overwriteStyle = { height: 'auto' } + if (isInDialog) { return (
- <>{withIconComp({ size: 48 })} +
{level === 4 ? ( @@ -242,45 +148,121 @@ export const NomadBadge = ({ if (hasTooltip) { return ( - - - {withIconComp({ size: 20 })} - + + ) : level === 3 ? ( + + ) : level === 2 ? ( + + ) : ( + + ) + } + placement="top" + > + ) } return ( - {withIconComp({ size: 20 })} + + + ) +} + +export const GrandBadge = ({ + isInDialog, + hasTooltip, + onClick, + gotoGrandBadge, +}: BadgePros & { gotoGrandBadge?: () => void }) => { + if (isInDialog) { + return ( +
+ +
+
+ +
+
+ +
+
+ +
+ ) + } + + if (hasTooltip) { + return ( + + } + placement="top" + > + + + ) + } + + return ( + + ) } export interface BadgesOptions { + isInDialog?: boolean + hasNomadBadge?: boolean nomadBadgeLevel?: 1 | 2 | 3 | 4 + gotoNomadBadge?: () => void + + hasGrandBadge?: boolean + gotoGrandBadge?: () => void + hasTraveloggersBadge?: boolean hasSeedBadge?: boolean hasGoldenMotorBadge?: boolean hasArchitectBadge?: boolean isCivicLiker?: boolean - gotoNomadBadge?: () => void - - isInDialog?: boolean - shareLink: string } export const Badges = ({ isInDialog, hasNomadBadge, nomadBadgeLevel, + gotoNomadBadge, + hasGrandBadge, + gotoGrandBadge, hasTraveloggersBadge, hasSeedBadge, hasGoldenMotorBadge, hasArchitectBadge, isCivicLiker, - gotoNomadBadge, }: BadgesOptions) => isInDialog ? ( @@ -293,6 +275,11 @@ export const Badges = ({ />
)} + {hasGrandBadge && ( +
+ +
+ )} {(hasTraveloggersBadge || hasSeedBadge || hasGoldenMotorBadge || @@ -310,6 +297,7 @@ export const Badges = ({ ) : ( {hasNomadBadge && } + {hasGrandBadge && } {hasTraveloggersBadge && } {hasSeedBadge && } {hasGoldenMotorBadge && } diff --git a/src/views/User/UserProfile/Badges/styles.module.css b/src/views/User/UserProfile/Badges/styles.module.css index 65285078fc..b9cb949a5e 100644 --- a/src/views/User/UserProfile/Badges/styles.module.css +++ b/src/views/User/UserProfile/Badges/styles.module.css @@ -1,9 +1,6 @@ .badge { font-size: 0; - - &.nomad { - border: none !important; - } + border: none !important; } .item { diff --git a/src/views/User/UserProfile/BadgesDialog/index.tsx b/src/views/User/UserProfile/BadgesDialog/index.tsx index 1d4a2008e8..3542f7d77b 100644 --- a/src/views/User/UserProfile/BadgesDialog/index.tsx +++ b/src/views/User/UserProfile/BadgesDialog/index.tsx @@ -2,12 +2,24 @@ import { useState } from 'react' import { FormattedMessage } from 'react-intl' import { ReactComponent as IconTimes } from '@/public/static/icons/24px/times.svg' -import { Button, Dialog, Icon, useDialogSwitch } from '~/components' +import { + OPEN_GRAND_BADGE_DIALOG, + OPEN_NOMAD_BADGE_DIALOG, +} from '~/common/enums' +import { + Button, + Dialog, + Icon, + useDialogSwitch, + useEventListener, +} from '~/components' +import BadgeGrandContent from '../BadgeGrandDialog/Content' import BadgeNomadDialogContent from '../BadgeNomadDialog/Content' import { Badges, BadgesOptions } from '../Badges' -type Step = 'badges' | 'nomad' +type Step = 'badges' | 'nomad' | 'grand' + interface BadgesDialogProps extends BadgesOptions { children: ({ openDialog, @@ -22,17 +34,18 @@ export const BaseBadgesDialog = ({ hasNomadBadge, nomadBadgeLevel, hasTraveloggersBadge, + hasGrandBadge, hasSeedBadge, hasGoldenMotorBadge, hasArchitectBadge, isCivicLiker, - shareLink, step: initStep = 'badges', }: BadgesDialogProps) => { const { show, openDialog, closeDialog } = useDialogSwitch(true) const [step, setStep] = useState(initStep) const isInBadgesStep = step === 'badges' const isInNomadStep = step === 'nomad' + const isInGrandStep = step === 'grand' const openStepDialog = (step?: Step) => { if (step) { @@ -72,13 +85,14 @@ export const BaseBadgesDialog = ({ isInDialog hasNomadBadge={hasNomadBadge} nomadBadgeLevel={nomadBadgeLevel} + hasGrandBadge={hasGrandBadge} hasTraveloggersBadge={hasTraveloggersBadge} hasSeedBadge={hasSeedBadge} hasGoldenMotorBadge={hasGoldenMotorBadge} hasArchitectBadge={hasArchitectBadge} isCivicLiker={isCivicLiker} - shareLink={shareLink} gotoNomadBadge={() => setStep('nomad')} + gotoGrandBadge={() => setStep('grand')} /> @@ -100,10 +114,14 @@ export const BaseBadgesDialog = ({ )} {isInNomadStep && !!nomadBadgeLevel && ( setStep('badges')} + /> + )} + {isInGrandStep && ( + setStep('badges')} /> )} @@ -113,10 +131,11 @@ export const BaseBadgesDialog = ({ } export const BadgesDialog = (props: BadgesDialogProps) => { - const Children = ({ openDialog }: { openDialog: (step?: Step) => void }) => { - return <>{props?.children({ openDialog })} + const Children = ({ openDialog }: { openDialog: () => void }) => { + useEventListener(OPEN_NOMAD_BADGE_DIALOG, openDialog) + useEventListener(OPEN_GRAND_BADGE_DIALOG, openDialog) + return <>{props.children && props.children({ openDialog })} } - return ( }> {({ openDialog }) => } diff --git a/src/views/User/UserProfile/index.tsx b/src/views/User/UserProfile/index.tsx index 731ffd3cdb..4b30a4b047 100644 --- a/src/views/User/UserProfile/index.tsx +++ b/src/views/User/UserProfile/index.tsx @@ -1,10 +1,15 @@ import classNames from 'classnames' import dynamic from 'next/dynamic' -import { useContext, useEffect, useState } from 'react' +import { useContext, useEffect } from 'react' import { FormattedMessage } from 'react-intl' -import { TEST_ID, URL_USER_PROFILE } from '~/common/enums' -import { numAbbr, toPath } from '~/common/utils' +import { + OPEN_GRAND_BADGE_DIALOG, + OPEN_NOMAD_BADGE_DIALOG, + TEST_ID, + URL_USER_PROFILE, +} from '~/common/enums' +import { numAbbr } from '~/common/utils' import { Avatar, Button, @@ -42,16 +47,10 @@ export const UserProfile = () => { // public user data const userName = getQuery('name') - const showBadges = - getQuery(URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key) === - URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value - const [hasShowBadges, setHasShowBadges] = useState(false) const isMe = !userName || viewer.userName === userName const { data, loading, client } = usePublicQuery( USER_PROFILE_PUBLIC, - { - variables: { userName }, - } + { variables: { userName } } ) const user = data?.user @@ -68,6 +67,24 @@ export const UserProfile = () => { }) }, [user?.id, viewer.id]) + useEffect(() => { + const shouldOpenNomadBadgeDialog = + getQuery(URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key) === + URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value + + if (shouldOpenNomadBadgeDialog) { + window.dispatchEvent(new CustomEvent(OPEN_NOMAD_BADGE_DIALOG)) + } + + const shouldOpenGrandBadgeDialog = + getQuery(URL_USER_PROFILE.OPEN_GRAND_BADGE_DIALOG.key) === + URL_USER_PROFILE.OPEN_GRAND_BADGE_DIALOG.value + + if (shouldOpenGrandBadgeDialog) { + window.dispatchEvent(new CustomEvent(OPEN_GRAND_BADGE_DIALOG)) + } + }, []) + /** * Render */ @@ -84,15 +101,6 @@ export const UserProfile = () => { return } - const userProfilePath = toPath({ - page: 'userProfile', - userName, - }) - const shareLink = - typeof window !== 'undefined' - ? `${window.location.origin}${userProfilePath.href}?${URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.key}=${URL_USER_PROFILE.OPEN_NOMAD_BADGE_DIALOG.value}` - : '' - const badges = user.info.badges || [] const circles = user.ownCircles || [] const hasSeedBadge = badges.some((b) => b.type === 'seed') @@ -106,6 +114,7 @@ export const UserProfile = () => { const nomadBadgeLevel = ( hasNomadBadge ? Number.parseInt(nomadBadgeType[0].type.charAt(5)) : 1 ) as 1 | 2 | 3 | 4 + const hasGrandBadge = badges.some((b) => b.type === 'grand_slam') const profileCover = user.info.profileCover const userState = user.status?.state as string @@ -198,43 +207,33 @@ export const UserProfile = () => { {user.displayName} - {({ openDialog }) => { - if (showBadges && hasNomadBadge && !hasShowBadges) { - setTimeout(() => { - openDialog('nomad') - // FIXED: infinite loop render of BadgesDialog - setHasShowBadges(true) - }) - } - return ( -
openDialog()} - > - -
- ) - }} + {({ openDialog }) => ( +
openDialog()} + > + +
+ )}
{user?.info.ethAddress && ( diff --git a/src/views/User/UserProfile/styles.module.css b/src/views/User/UserProfile/styles.module.css index d8ea9d56c6..154e0ab56b 100644 --- a/src/views/User/UserProfile/styles.module.css +++ b/src/views/User/UserProfile/styles.module.css @@ -72,9 +72,7 @@ & > span > * { position: relative; display: inline-block; - margin-left: -4.5px; - border: 1px solid var(--color-white); - border-radius: 50%; + margin-left: -4px; &:nth-child(1) { z-index: 9; @@ -95,6 +93,10 @@ &:nth-child(5) { z-index: 5; } + + &:nth-child(6) { + z-index: 4; + } } } }