From 331533d724ca0261b552c269626ec76dbeffdc87 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Tue, 27 Jun 2023 14:51:37 +0200 Subject: [PATCH] Migrated Comments-UI to TypeScript (#17129) refs https://github.com/TryGhost/Team/issues/3504 This migrates comments-ui to TypeScript. Only `App.js` is left to migrate, but since this isn't using hooks yet, it will need a bigger rewrite so this will need to happen in a separate PR. --- .github/dev.js | 16 +- apps/comments-ui/.eslintrc.js | 5 +- apps/comments-ui/package.json | 15 +- .../{postcss.config.js => postcss.config.cjs} | 0 apps/comments-ui/src/{App.js => App.jsx} | 2 +- .../src/{App.test.js => App.test.jsx} | 24 +- apps/comments-ui/src/AppContext.js | 6 - apps/comments-ui/src/AppContext.ts | 80 +++++ .../src/{actions.js => actions.ts} | 58 ++-- .../{ContentBox.js => ContentBox.tsx} | 27 +- .../src/components/{Frame.js => Frame.tsx} | 27 +- .../src/components/{IFrame.js => IFrame.tsx} | 24 +- .../components/{PopupBox.js => PopupBox.tsx} | 17 +- .../content/{Avatar.js => Avatar.tsx} | 24 +- .../content/{CTABox.js => CTABox.tsx} | 11 +- .../content/{Comment.js => Comment.tsx} | 106 ++++--- .../content/{Content.js => Content.tsx} | 6 +- .../{ContentTitle.js => ContentTitle.tsx} | 17 +- .../content/{Loading.js => Loading.tsx} | 1 - .../content/{Pagination.js => Pagination.tsx} | 7 +- .../content/{Replies.js => Replies.tsx} | 14 +- ...iesPagination.js => RepliesPagination.tsx} | 6 +- .../buttons/{LikeButton.js => LikeButton.tsx} | 13 +- .../buttons/{MoreButton.js => MoreButton.tsx} | 13 +- .../{ReplyButton.js => ReplyButton.tsx} | 14 +- ...minContextMenu.js => AdminContextMenu.tsx} | 17 +- ...orContextMenu.js => AuthorContextMenu.tsx} | 15 +- ...tContextMenu.js => CommentContextMenu.tsx} | 25 +- ...ontextMenu.js => NotAuthorContextMenu.tsx} | 12 +- .../forms/{EditForm.js => EditForm.tsx} | 16 +- .../content/forms/{Form.js => Form.tsx} | 68 ++++- .../forms/{MainForm.js => MainForm.tsx} | 21 +- .../forms/{ReplyForm.js => ReplyForm.tsx} | 16 +- .../{SecundaryForm.js => SecundaryForm.tsx} | 23 +- ...AddDetailsPopup.js => AddDetailsPopup.tsx} | 49 +-- .../{CloseButton.js => CloseButton.tsx} | 7 +- .../{GenericPopup.js => GenericPopup.tsx} | 22 +- .../{ReportPopup.js => ReportPopup.tsx} | 21 +- apps/comments-ui/src/{index.js => index.tsx} | 57 ++-- apps/comments-ui/src/pages.js | 11 - apps/comments-ui/src/pages.ts | 25 ++ apps/comments-ui/src/setupTests.js | 15 - apps/comments-ui/src/setupTests.ts | 11 + apps/comments-ui/src/typings.d.ts | 6 + apps/comments-ui/src/utils/api.js | 279 ----------------- .../src/utils/{api.test.js => api.test.ts} | 29 +- apps/comments-ui/src/utils/api.ts | 287 ++++++++++++++++++ .../utils/{check-mode.js => check-mode.ts} | 2 +- .../src/utils/{constants.js => constants.ts} | 0 .../src/utils/{editor.js => editor.ts} | 3 +- .../{helpers.test.js => helpers.test.ts} | 4 +- .../src/utils/{helpers.js => helpers.ts} | 31 +- .../src/utils/{hooks.js => hooks.ts} | 28 +- apps/comments-ui/src/utils/test-utils.js | 50 --- apps/comments-ui/src/vite-env.d.ts | 1 + apps/comments-ui/test/utils/MockedApi.ts | 2 +- apps/comments-ui/tsconfig.json | 32 ++ apps/comments-ui/tsconfig.node.json | 10 + apps/comments-ui/vite.config.js | 82 ----- apps/comments-ui/vite.config.ts | 72 +++++ yarn.lock | 173 +++-------- 61 files changed, 1148 insertions(+), 907 deletions(-) rename apps/comments-ui/{postcss.config.js => postcss.config.cjs} (100%) rename apps/comments-ui/src/{App.js => App.jsx} (99%) rename apps/comments-ui/src/{App.test.js => App.test.jsx} (94%) delete mode 100644 apps/comments-ui/src/AppContext.js create mode 100644 apps/comments-ui/src/AppContext.ts rename apps/comments-ui/src/{actions.js => actions.ts} (79%) rename apps/comments-ui/src/components/{ContentBox.js => ContentBox.tsx} (66%) rename apps/comments-ui/src/components/{Frame.js => Frame.tsx} (72%) rename apps/comments-ui/src/components/{IFrame.js => IFrame.tsx} (78%) rename apps/comments-ui/src/components/{PopupBox.js => PopupBox.tsx} (83%) rename apps/comments-ui/src/components/content/{Avatar.js => Avatar.tsx} (84%) rename apps/comments-ui/src/components/content/{CTABox.js => CTABox.tsx} (89%) rename apps/comments-ui/src/components/content/{Comment.js => Comment.tsx} (75%) rename apps/comments-ui/src/components/content/{Content.js => Content.tsx} (92%) rename apps/comments-ui/src/components/content/{ContentTitle.js => ContentTitle.tsx} (75%) rename apps/comments-ui/src/components/content/{Loading.js => Loading.tsx} (92%) rename apps/comments-ui/src/components/content/{Pagination.js => Pagination.tsx} (84%) rename apps/comments-ui/src/components/content/{Replies.js => Replies.tsx} (52%) rename apps/comments-ui/src/components/content/{RepliesPagination.js => RepliesPagination.tsx} (87%) rename apps/comments-ui/src/components/content/buttons/{LikeButton.js => LikeButton.tsx} (87%) rename apps/comments-ui/src/components/content/buttons/{MoreButton.js => MoreButton.tsx} (82%) rename apps/comments-ui/src/components/content/buttons/{ReplyButton.js => ReplyButton.tsx} (77%) rename apps/comments-ui/src/components/content/context-menus/{AdminContextMenu.js => AdminContextMenu.tsx} (72%) rename apps/comments-ui/src/components/content/context-menus/{AuthorContextMenu.js => AuthorContextMenu.tsx} (60%) rename apps/comments-ui/src/components/content/context-menus/{CommentContextMenu.js => CommentContextMenu.tsx} (82%) rename apps/comments-ui/src/components/content/context-menus/{NotAuthorContextMenu.js => NotAuthorContextMenu.tsx} (65%) rename apps/comments-ui/src/components/content/forms/{EditForm.js => EditForm.tsx} (84%) rename apps/comments-ui/src/components/content/forms/{Form.js => Form.tsx} (82%) rename apps/comments-ui/src/components/content/forms/{MainForm.js => MainForm.tsx} (81%) rename apps/comments-ui/src/components/content/forms/{ReplyForm.js => ReplyForm.tsx} (80%) rename apps/comments-ui/src/components/content/forms/{SecundaryForm.js => SecundaryForm.tsx} (58%) rename apps/comments-ui/src/components/popups/{AddDetailsPopup.js => AddDetailsPopup.tsx} (84%) rename apps/comments-ui/src/components/popups/{CloseButton.js => CloseButton.tsx} (72%) rename apps/comments-ui/src/components/popups/{GenericPopup.js => GenericPopup.tsx} (81%) rename apps/comments-ui/src/components/popups/{ReportPopup.js => ReportPopup.tsx} (83%) rename apps/comments-ui/src/{index.js => index.tsx} (64%) delete mode 100644 apps/comments-ui/src/pages.js create mode 100644 apps/comments-ui/src/pages.ts delete mode 100644 apps/comments-ui/src/setupTests.js create mode 100644 apps/comments-ui/src/setupTests.ts create mode 100644 apps/comments-ui/src/typings.d.ts delete mode 100644 apps/comments-ui/src/utils/api.js rename apps/comments-ui/src/utils/{api.test.js => api.test.ts} (60%) create mode 100644 apps/comments-ui/src/utils/api.ts rename apps/comments-ui/src/utils/{check-mode.js => check-mode.ts} (83%) rename apps/comments-ui/src/utils/{constants.js => constants.ts} (100%) rename apps/comments-ui/src/utils/{editor.js => editor.ts} (89%) rename apps/comments-ui/src/utils/{helpers.test.js => helpers.test.ts} (76%) rename apps/comments-ui/src/utils/{helpers.js => helpers.ts} (86%) rename apps/comments-ui/src/utils/{hooks.js => hooks.ts} (72%) delete mode 100644 apps/comments-ui/src/utils/test-utils.js create mode 100644 apps/comments-ui/src/vite-env.d.ts create mode 100644 apps/comments-ui/tsconfig.json create mode 100644 apps/comments-ui/tsconfig.node.json delete mode 100644 apps/comments-ui/vite.config.js create mode 100644 apps/comments-ui/vite.config.ts diff --git a/.github/dev.js b/.github/dev.js index 689d9a4f5c..6510b417cc 100644 --- a/.github/dev.js +++ b/.github/dev.js @@ -149,7 +149,7 @@ if (DASH_DASH_ARGS.includes('lexical')) { // To make this work, you'll need a CADDY server running in front // Note the port is different because of this extra layer. Use the following Caddyfile: // https://localhost:4174 { - // reverse_proxy 127.0.0.1:4173 + // reverse_proxy http://127.0.0.1:4173 // } COMMAND_GHOST.env['editor__url'] = 'https://localhost:4174/koenig-lexical.umd.js'; @@ -159,6 +159,18 @@ if (DASH_DASH_ARGS.includes('lexical')) { } if (DASH_DASH_ARGS.includes('comments') || DASH_DASH_ARGS.includes('all')) { + if (DASH_DASH_ARGS.includes('https')) { + // Safari needs HTTPS for it to work + // To make this work, you'll need a CADDY server running in front + // Note the port is different because of this extra layer. Use the following Caddyfile: + // https://localhost:7174 { + // reverse_proxy http://127.0.0.1:7173 + // } + COMMAND_GHOST.env['comments__url'] = 'https://localhost:7174/comments-ui.min.js'; + } else { + COMMAND_GHOST.env['comments__url'] = 'http://localhost:7173/comments-ui.min.js'; + } + commands.push({ name: 'comments', command: 'yarn dev', @@ -166,8 +178,6 @@ if (DASH_DASH_ARGS.includes('comments') || DASH_DASH_ARGS.includes('all')) { prefixColor: '#E55137', env: {} }); - - COMMAND_GHOST.env['comments__url'] = 'http://localhost:7174/comments-ui.min.js'; } async function handleStripe() { diff --git a/apps/comments-ui/.eslintrc.js b/apps/comments-ui/.eslintrc.js index 0f6166bbd5..996726ec69 100644 --- a/apps/comments-ui/.eslintrc.js +++ b/apps/comments-ui/.eslintrc.js @@ -36,6 +36,9 @@ module.exports = { 'tailwindcss/migration-from-tailwind-2': ['warn', {config: 'tailwind.config.cjs'}], 'tailwindcss/no-arbitrary-value': 'off', 'tailwindcss/no-custom-classname': 'off', - 'tailwindcss/no-contradicting-classname': ['error', {config: 'tailwind.config.cjs'}] + 'tailwindcss/no-contradicting-classname': ['error', {config: 'tailwind.config.cjs'}], + + // This rule doesn't work correctly with TypeScript, and TypeScript has its own better version + 'no-undef': 'off' } }; diff --git a/apps/comments-ui/package.json b/apps/comments-ui/package.json index 964a89315d..5e344bbe80 100644 --- a/apps/comments-ui/package.json +++ b/apps/comments-ui/package.json @@ -15,7 +15,7 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "dev": "concurrently \"yarn preview -l silent\" \"yarn build:watch\"", + "dev": "concurrently \"yarn preview --host -l silent\" \"yarn build:watch\"", "dev:test": "vite build && vite preview --port 7175", "build": "vite build", "build:watch": "vite build --watch", @@ -24,7 +24,7 @@ "test:e2e": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test", "test:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=1000 yarn test:e2e --headed", "test:e2e:full": "ALL_BROWSERS=1 yarn test:e2e", - "lint": "eslint src --ext .js --cache", + "lint": "eslint src --ext .js,.ts,.jsx,.tsx --cache", "preship": "yarn lint", "ship": "STATUS=$(git status --porcelain); echo $STATUS; if [ -z \"$STATUS\" ]; then yarn version; fi", "postship": "git push ${GHOST_UPSTREAM:-origin} --follow-tags && npm publish", @@ -61,15 +61,18 @@ "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.5", "@testing-library/user-event": "14.4.3", + "@typescript-eslint/eslint-plugin": "5.57.1", + "@typescript-eslint/parser": "5.57.1", "@vitejs/plugin-react": "4.0.1", "@vitest/coverage-v8": "0.32.2", "autoprefixer": "10.4.14", "bson-objectid": "2.0.4", "concurrently": "8.2.0", - "eslint": "8.43.0", - "eslint-config-react-app": "7.0.1", - "eslint-plugin-ghost": "2.12.0", - "eslint-plugin-tailwindcss": "^3.6.0", + "eslint": "8.38.0", + "eslint-plugin-ghost": "3.2.0", + "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-react-refresh": "0.3.4", + "eslint-plugin-tailwindcss": "3.11.0", "jsdom": "22.1.0", "postcss": "8.4.24", "tailwindcss": "3.3.2", diff --git a/apps/comments-ui/postcss.config.js b/apps/comments-ui/postcss.config.cjs similarity index 100% rename from apps/comments-ui/postcss.config.js rename to apps/comments-ui/postcss.config.cjs diff --git a/apps/comments-ui/src/App.js b/apps/comments-ui/src/App.jsx similarity index 99% rename from apps/comments-ui/src/App.js rename to apps/comments-ui/src/App.jsx index 630e7022d0..ef7f82c5e6 100644 --- a/apps/comments-ui/src/App.js +++ b/apps/comments-ui/src/App.jsx @@ -1,9 +1,9 @@ -import AppContext from './AppContext'; import ContentBox from './components/ContentBox'; import PopupBox from './components/PopupBox'; import React from 'react'; import setupGhostApi from './utils/api'; import {ActionHandler, SyncActionHandler, isSyncAction} from './actions'; +import {AppContext} from './AppContext'; import {CommentsFrame} from './components/Frame'; import {createPopupNotification} from './utils/helpers'; import {hasMode} from './utils/check-mode'; diff --git a/apps/comments-ui/src/App.test.js b/apps/comments-ui/src/App.test.jsx similarity index 94% rename from apps/comments-ui/src/App.test.js rename to apps/comments-ui/src/App.test.jsx index 26b4b59b59..b5fd86c8f1 100644 --- a/apps/comments-ui/src/App.test.js +++ b/apps/comments-ui/src/App.test.jsx @@ -2,7 +2,7 @@ import App from './App'; import userEvent from '@testing-library/user-event'; import {ROOT_DIV_ID} from './utils/constants'; import {act, fireEvent, render, waitFor, within} from '@testing-library/react'; -import {buildComment, buildMember} from './utils/test-utils'; +import {buildComment, buildMember} from '../test/utils/fixtures'; function renderApp({member = null, documentStyles = {}, props = {}} = {}) { const postId = 'my-post'; @@ -80,7 +80,7 @@ function renderApp({member = null, documentStyles = {}, props = {}} = {}) { } beforeEach(() => { - window.scrollTo = jest.fn(); + window.scrollTo = vi.fn(); Range.prototype.getClientRects = function getClientRects() { return [ { @@ -98,7 +98,7 @@ beforeEach(() => { }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); describe('Auth frame', () => { @@ -148,7 +148,7 @@ describe('Dark mode', () => { describe('Comments', () => { it('renders comments', async () => { const {api, iframeDocument} = renderApp(); - jest.spyOn(api.comments, 'browse').mockImplementation(() => { + vi.spyOn(api.comments, 'browse').mockImplementation(() => { return { comments: [ buildComment({html: '

This is a comment body

'}) @@ -174,7 +174,7 @@ describe('Comments', () => { const limit = 5; const {api, iframeDocument} = renderApp(); - jest.spyOn(api.comments, 'browse').mockImplementation(({page}) => { + vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => { if (page === 2) { return { comments: new Array(1).fill({}).map(() => buildComment({html: '

This is a paginated comment

'})), @@ -216,7 +216,7 @@ describe('Comments', () => { const limit = 5; const {api, iframeDocument} = renderApp(); - jest.spyOn(api.comments, 'browse').mockImplementation(({page}) => { + vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => { return { comments: new Array(limit).fill({}).map(() => buildComment({html: '

This is a comment body

', member: null})), meta: { @@ -239,7 +239,7 @@ describe('Comments', () => { const limit = 5; const {api, iframeDocument} = renderApp(); - jest.spyOn(api.comments, 'browse').mockImplementation(({page}) => { + vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => { if (page === 2) { throw new Error('Not requested'); } @@ -281,7 +281,7 @@ describe('Likes', () => { member }); - jest.spyOn(api.comments, 'browse').mockImplementation(({page}) => { + vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => { if (page === 2) { throw new Error('Not requested'); } @@ -299,8 +299,8 @@ describe('Likes', () => { }; }); - const likeSpy = jest.spyOn(api.comments, 'like'); - const unlikeSpy = jest.spyOn(api.comments, 'unlike'); + const likeSpy = vi.spyOn(api.comments, 'like'); + const unlikeSpy = vi.spyOn(api.comments, 'unlike'); const comment = await within(iframeDocument).findByTestId('comment-component'); @@ -346,7 +346,7 @@ describe('Replies', () => { member }); - jest.spyOn(api.comments, 'browse').mockImplementation(({page}) => { + vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => { if (page === 2) { throw new Error('Not requested'); } @@ -364,7 +364,7 @@ describe('Replies', () => { }; }); - const repliesSpy = jest.spyOn(api.comments, 'replies'); + const repliesSpy = vi.spyOn(api.comments, 'replies'); const comments = await within(iframeDocument).findAllByTestId('comment-component'); expect(comments).toHaveLength(limit); diff --git a/apps/comments-ui/src/AppContext.js b/apps/comments-ui/src/AppContext.js deleted file mode 100644 index 08f9e23987..0000000000 --- a/apps/comments-ui/src/AppContext.js +++ /dev/null @@ -1,6 +0,0 @@ -// Ref: https://reactjs.org/docs/context.html -import React from 'react'; - -const AppContext = React.createContext({}); - -export default AppContext; diff --git a/apps/comments-ui/src/AppContext.ts b/apps/comments-ui/src/AppContext.ts new file mode 100644 index 0000000000..33aa3fd8dd --- /dev/null +++ b/apps/comments-ui/src/AppContext.ts @@ -0,0 +1,80 @@ +// Ref: https://reactjs.org/docs/context.html +import React, {useContext} from 'react'; +import {ActionType, Actions, SyncActionType, SyncActions} from './actions'; +import {Page} from './pages'; + +export type PopupNotification = { + type: string, + status: string, + autoHide: boolean, + closeable: boolean, + duration: number, + meta: any, + message: string, + count: number +} + +export type Member = { + id: string, + uuid: string, + name: string, + avatar: string, + expertise: string +} + +export type Comment = { + id: string, + post_id: string, + replies: Comment[], + status: string, + liked: boolean, + count: { + replies: number, + likes: number, + }, + member: Member, + edited_at: string, + created_at: string, + html: string +} + +export type AddComment = { + post_id: string, + status: string, + html: string +} + +export type AppContextType = { + action: string, + popupNotification: PopupNotification | null, + customSiteUrl: string | undefined, + member: null | any, + admin: null | any, + comments: Comment[], + pagination: { + page: number, + limit: number, + pages: number, + total: number + } | null, + commentCount: number, + postId: string, + title: string, + showCount: boolean, + colorScheme: string | undefined, + avatarSaturation: number | undefined, + accentColor: string | undefined, + commentsEnabled: string | undefined, + publication: string, + secundaryFormCount: number, + popup: Page | null, + + // This part makes sure we can add automatic data and return types to the actions when using context.dispatchAction('actionName', data) + dispatchAction: (action: T, data: Parameters<(typeof Actions & typeof SyncActions)[T]>[0] extends {data: any} ? Parameters<(typeof Actions & typeof SyncActions)[T]>[0]['data'] : {}) => T extends ActionType ? Promise : void +} + +export const AppContext = React.createContext({} as any); + +export const AppContextProvider = AppContext.Provider; + +export const useAppContext = () => useContext(AppContext); diff --git a/apps/comments-ui/src/actions.js b/apps/comments-ui/src/actions.ts similarity index 79% rename from apps/comments-ui/src/actions.js rename to apps/comments-ui/src/actions.ts index a9ab9f77af..a9e6d02cb7 100644 --- a/apps/comments-ui/src/actions.js +++ b/apps/comments-ui/src/actions.ts @@ -1,4 +1,8 @@ -async function loadMoreComments({state, api}) { +import {AddComment, AppContextType, Comment} from './AppContext'; +import {GhostApi} from './utils/api'; +import {Page} from './pages'; + +async function loadMoreComments({state, api}: {state: AppContextType, api: GhostApi}): Promise> { let page = 1; if (state.pagination && state.pagination.page) { page = state.pagination.page + 1; @@ -12,7 +16,7 @@ async function loadMoreComments({state, api}) { }; } -async function loadMoreReplies({state, api, data: {comment, limit}}) { +async function loadMoreReplies({state, api, data: {comment, limit}}: {state: AppContextType, api: GhostApi, data: {comment: any, limit?: number | 'all'}}): Promise> { const data = await api.comments.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit}); // Note: we store the comments from new to old, and show them in reverse order @@ -29,7 +33,7 @@ async function loadMoreReplies({state, api, data: {comment, limit}}) { }; } -async function addComment({state, api, data: comment}) { +async function addComment({state, api, data: comment}: {state: AppContextType, api: GhostApi, data: AddComment}) { const data = await api.comments.add({comment}); comment = data.comments[0]; @@ -39,7 +43,7 @@ async function addComment({state, api, data: comment}) { }; } -async function addReply({state, api, data: {reply, parent}}) { +async function addReply({state, api, data: {reply, parent}}: {state: AppContextType, api: GhostApi, data: {reply: any, parent: any}}) { let comment = reply; comment.parent_id = parent.id; @@ -69,7 +73,7 @@ async function addReply({state, api, data: {reply, parent}}) { }; } -async function hideComment({state, adminApi, data: comment}) { +async function hideComment({state, adminApi, data: comment}: {state: AppContextType, adminApi: any, data: {id: string}}) { await adminApi.hideComment(comment.id); return { @@ -102,7 +106,7 @@ async function hideComment({state, adminApi, data: comment}) { }; } -async function showComment({state, api, adminApi, data: comment}) { +async function showComment({state, api, adminApi, data: comment}: {state: AppContextType, api: GhostApi, adminApi: any, data: {id: string}}) { await adminApi.showComment(comment.id); // We need to refetch the comment, to make sure we have an up to date HTML content @@ -133,7 +137,7 @@ async function showComment({state, api, adminApi, data: comment}) { }; } -async function likeComment({state, api, data: comment}) { +async function likeComment({state, api, data: comment}: {state: AppContextType, api: GhostApi, data: {id: string}}) { await api.comments.like({comment}); return { @@ -173,13 +177,13 @@ async function likeComment({state, api, data: comment}) { }; } -async function reportComment({state, api, data: comment}) { +async function reportComment({state, api, data: comment}: {state: AppContextType, api: GhostApi, data: {id: string}}) { await api.comments.report({comment}); return {}; } -async function unlikeComment({state, api, data: comment}) { +async function unlikeComment({state, api, data: comment}: {state: AppContextType, api: GhostApi, data: {id: string}}) { await api.comments.unlike({comment}); return { @@ -218,7 +222,7 @@ async function unlikeComment({state, api, data: comment}) { }; } -async function deleteComment({state, api, data: comment}) { +async function deleteComment({state, api, data: comment}: {state: AppContextType, api: GhostApi, data: {id: string}}) { await api.comments.edit({ comment: { id: comment.id, @@ -256,7 +260,7 @@ async function deleteComment({state, api, data: comment}) { }; } -async function editComment({state, api, data: {comment, parent}}) { +async function editComment({state, api, data: {comment, parent}}: {state: AppContextType, api: GhostApi, data: {comment: Partial & {id: string}, parent?: Comment}}) { const data = await api.comments.edit({ comment }); @@ -284,10 +288,10 @@ async function editComment({state, api, data: {comment, parent}}) { }; } -async function updateMember({data, state, api}) { +async function updateMember({data, state, api}: {data: {name: string, expertise: string}, state: AppContextType, api: GhostApi}) { const {name, expertise} = data; - const patchData = {}; - + const patchData: {name?: string, expertise?: string} = {}; + const originalName = state?.member?.name; if (name && originalName !== name) { @@ -320,7 +324,7 @@ async function updateMember({data, state, api}) { return null; } -function openPopup({data}) { +function openPopup({data}: {data: Page}) { return { popup: data }; @@ -332,27 +336,29 @@ function closePopup() { }; } -function increaseSecundaryFormCount({state}) { +function increaseSecundaryFormCount({state}: {state: AppContextType}) { return { secundaryFormCount: state.secundaryFormCount + 1 }; } -function decreaseSecundaryFormCount({state}) { +function decreaseSecundaryFormCount({state}: {state: AppContextType}) { return { secundaryFormCount: state.secundaryFormCount - 1 }; } // Sync actions make use of setState((currentState) => newState), to avoid 'race' conditions -const SyncActions = { +export const SyncActions = { openPopup, closePopup, increaseSecundaryFormCount, decreaseSecundaryFormCount }; -const Actions = { +export type SyncActionType = keyof typeof SyncActions; + +export const Actions = { // Put your actions here addComment, editComment, @@ -368,25 +374,27 @@ const Actions = { updateMember }; -export function isSyncAction(action) { - return !!SyncActions[action]; +export type ActionType = keyof typeof Actions; + +export function isSyncAction(action: string): action is SyncActionType { + return !!(SyncActions as any)[action]; } /** Handle actions in the App, returns updated state */ -export async function ActionHandler({action, data, state, api, adminApi}) { +export async function ActionHandler({action, data, state, api, adminApi}: {action: ActionType, data: any, state: AppContextType, api: GhostApi, adminApi: any}) { const handler = Actions[action]; if (handler) { - return await handler({data, state, api, adminApi}) || {}; + return await handler({data, state, api, adminApi} as any) || {}; } return {}; } /** Handle actions in the App, returns updated state */ -export function SyncActionHandler({action, data, state, api, adminApi}) { +export function SyncActionHandler({action, data, state, api, adminApi}: {action: SyncActionType, data: any, state: AppContextType, api: GhostApi, adminApi: any}) { const handler = SyncActions[action]; if (handler) { // Do not await here - return handler({data, state, api, adminApi}) || {}; + return handler({data, state, api, adminApi} as any) || {}; } return {}; } diff --git a/apps/comments-ui/src/components/ContentBox.js b/apps/comments-ui/src/components/ContentBox.tsx similarity index 66% rename from apps/comments-ui/src/components/ContentBox.js rename to apps/comments-ui/src/components/ContentBox.tsx index 20a6998d5a..e2885eae5b 100644 --- a/apps/comments-ui/src/components/ContentBox.js +++ b/apps/comments-ui/src/components/ContentBox.tsx @@ -1,11 +1,14 @@ -import AppContext from '../AppContext'; import Content from './content/Content'; import Loading from './content/Loading'; -import React, {useContext} from 'react'; +import React from 'react'; import {ROOT_DIV_ID} from '../utils/constants'; +import {useAppContext} from '../AppContext'; -const ContentBox = ({done}) => { - const luminance = (r, g, b) => { +type Props = { + done: boolean +}; +const ContentBox: React.FC = ({done}) => { + const luminance = (r: number, g: number, b: number) => { var a = [r, g, b].map(function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); @@ -13,14 +16,14 @@ const ContentBox = ({done}) => { return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; }; - const contrast = (rgb1, rgb2) => { + const contrast = (rgb1: [number, number, number], rgb2: [number, number, number]) => { var lum1 = luminance(rgb1[0], rgb1[1], rgb1[2]); var lum2 = luminance(rgb2[0], rgb2[1], rgb2[2]); var brightest = Math.max(lum1, lum2); var darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); }; - const {accentColor, colorScheme} = useContext(AppContext); + const {accentColor, colorScheme} = useAppContext(); const darkMode = () => { if (colorScheme === 'light') { @@ -28,12 +31,16 @@ const ContentBox = ({done}) => { } else if (colorScheme === 'dark') { return true; } else { - const containerColor = getComputedStyle(document.getElementById(ROOT_DIV_ID).parentNode).getPropertyValue('color'); + const el = document.getElementById(ROOT_DIV_ID); + if (!el || !el.parentElement) { + return false; + } + const containerColor = getComputedStyle(el.parentElement).getPropertyValue('color'); const colorsOnly = containerColor.substring(containerColor.indexOf('(') + 1, containerColor.lastIndexOf(')')).split(/,\s*/); - const red = colorsOnly[0]; - const green = colorsOnly[1]; - const blue = colorsOnly[2]; + const red = parseInt(colorsOnly[0]); + const green = parseInt(colorsOnly[1]); + const blue = parseInt(colorsOnly[2]); return contrast([255, 255, 255], [red, green, blue]) < 5; } diff --git a/apps/comments-ui/src/components/Frame.js b/apps/comments-ui/src/components/Frame.tsx similarity index 72% rename from apps/comments-ui/src/components/Frame.js rename to apps/comments-ui/src/components/Frame.tsx index 401fa83c02..cb692c37d6 100644 --- a/apps/comments-ui/src/components/Frame.js +++ b/apps/comments-ui/src/components/Frame.tsx @@ -2,10 +2,20 @@ import IFrame from './IFrame'; import React, {useCallback, useState} from 'react'; import styles from '../styles/iframe.css?inline'; +type FrameProps = { + children: React.ReactNode +}; + +type TailwindFrameProps = FrameProps & { + style: React.CSSProperties, + title: string, + onResize: (iframeRoot: HTMLElement) => void +}; + /** * Loads all the CSS styles inside an iFrame. Only shows the visible content as soon as the CSS file with the tailwind classes has loaded. */ -const TailwindFrame = ({children, onResize, style, title}) => { +const TailwindFrame: React.FC = ({children, onResize, style, title}) => { const head = ( <>