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 = (
<>
@@ -21,10 +31,15 @@ const TailwindFrame = ({children, onResize, style, title}) => {
);
};
+type ResizableFrameProps = FrameProps & {
+ style: React.CSSProperties,
+ title: string
+};
+
/**
* This iframe has the same height as it contents and mimics a shadow DOM component
*/
-const ResizableFrame = ({children, style, title}) => {
+const ResizableFrame: React.FC = ({children, style, title}) => {
const [iframeStyle, setIframeStyle] = useState(style);
const onResize = useCallback((iframeRoot) => {
setIframeStyle((current) => {
@@ -42,7 +57,7 @@ const ResizableFrame = ({children, style, title}) => {
);
};
-export const CommentsFrame = ({children}) => {
+export const CommentsFrame: React.FC = ({children}) => {
const style = {
width: '100%',
height: '400px'
@@ -54,7 +69,11 @@ export const CommentsFrame = ({children}) => {
);
};
-export const PopupFrame = ({children, title}) => {
+type PopupFrameProps = FrameProps & {
+ title: string
+};
+
+export const PopupFrame: React.FC = ({children, title}) => {
const style = {
zIndex: '3999999',
position: 'fixed',
diff --git a/apps/comments-ui/src/components/IFrame.js b/apps/comments-ui/src/components/IFrame.tsx
similarity index 78%
rename from apps/comments-ui/src/components/IFrame.js
rename to apps/comments-ui/src/components/IFrame.tsx
index 096eb2eeaf..d22e44412a 100644
--- a/apps/comments-ui/src/components/IFrame.js
+++ b/apps/comments-ui/src/components/IFrame.tsx
@@ -1,13 +1,21 @@
-import React, {Component} from 'react';
+import {Component} from 'react';
import {createPortal} from 'react-dom';
-export default class IFrame extends Component {
- constructor() {
- super();
+/**
+ * This is still a class component because it causes issues with the behaviour (DOM recreation and layout glitches) if we switch to a functional component. Feel free to refactor.
+ */
+export default class IFrame extends Component {
+ node: any;
+ iframeHtml: any;
+ iframeHead: any;
+ iframeRoot: any;
+
+ constructor(props: {onResize?: (el: HTMLElement) => void, children: any}) {
+ super(props);
this.setNode = this.setNode.bind(this);
this.node = null;
}
-
+
componentDidMount() {
this.node.addEventListener('load', this.handleLoad);
}
@@ -35,7 +43,7 @@ export default class IFrame extends Component {
// because when we want to listen for keydown events, those are only send in the window of iframe that is focused
// To get around this, we pass down the keydown events to the main window
// No need to detach, because the iframe would get removed
- this.node.contentWindow.addEventListener('keydown', (e) => {
+ this.node.contentWindow.addEventListener('keydown', (e: KeyboardEvent | undefined) => {
// dispatch a new event
window.dispatchEvent(
new KeyboardEvent('keydown', e)
@@ -44,8 +52,8 @@ export default class IFrame extends Component {
}
}
- setNode(node) {
- this.node = node;
+ setNode(node: any) {
+ this.node = node;
}
render() {
diff --git a/apps/comments-ui/src/components/PopupBox.js b/apps/comments-ui/src/components/PopupBox.tsx
similarity index 83%
rename from apps/comments-ui/src/components/PopupBox.js
rename to apps/comments-ui/src/components/PopupBox.tsx
index aa99522b1c..1768e0cbb2 100644
--- a/apps/comments-ui/src/components/PopupBox.js
+++ b/apps/comments-ui/src/components/PopupBox.tsx
@@ -1,10 +1,11 @@
-import AppContext from '../AppContext';
import GenericPopup from './popups/GenericPopup';
-import Pages from '../pages';
-import {useContext, useEffect, useState} from 'react';
+import {Pages} from '../pages';
+import {useAppContext} from '../AppContext';
+import {useEffect, useState} from 'react';
-export default function PopupBox() {
- const {popup} = useContext(AppContext);
+type Props = {};
+const PopupBox: React.FC = () => {
+ const {popup} = useAppContext();
// To make sure we can properly animate a popup that goes away, we keep a state of the last visible popup
// This way, when the popup context is set to null, we still can show the popup while we transition it away
@@ -47,8 +48,10 @@ export default function PopupBox() {
return (
<>
-
+
>
);
-}
+};
+
+export default PopupBox;
diff --git a/apps/comments-ui/src/components/content/Avatar.js b/apps/comments-ui/src/components/content/Avatar.tsx
similarity index 84%
rename from apps/comments-ui/src/components/content/Avatar.js
rename to apps/comments-ui/src/components/content/Avatar.tsx
index 54f1813dc3..2e41a72707 100644
--- a/apps/comments-ui/src/components/content/Avatar.js
+++ b/apps/comments-ui/src/components/content/Avatar.tsx
@@ -1,6 +1,5 @@
-import AppContext from '../../AppContext';
-import React, {useContext} from 'react';
import {ReactComponent as AvatarIcon} from '../../images/icons/avatar.svg';
+import {Comment, useAppContext} from '../../AppContext';
import {getInitials} from '../../utils/helpers';
function getDimensionClasses() {
@@ -18,13 +17,16 @@ export const BlankAvatar = () => {
);
};
-export const Avatar = ({comment}) => {
- const {member, avatarSaturation} = useContext(AppContext);
+type AvatarProps = {
+ comment?: Comment;
+};
+export const Avatar: React.FC = ({comment}) => {
+ const {member, avatarSaturation} = useAppContext();
const dimensionClasses = getDimensionClasses();
const memberName = member?.name ?? comment?.member?.name;
- const getHashOfString = (str) => {
+ const getHashOfString = (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
@@ -33,18 +35,18 @@ export const Avatar = ({comment}) => {
return hash;
};
- const normalizeHash = (hash, min, max) => {
+ const normalizeHash = (hash: number, min: number, max: number) => {
return Math.floor((hash % (max - min)) + min);
};
- const generateHSL = () => {
+ const generateHSL = (): [number, number, number] => {
let commentMember = (comment ? comment.member : member);
if (!commentMember || !commentMember.name) {
return [0,0,10];
}
- const saturation = isNaN(avatarSaturation) ? 50 : avatarSaturation;
+ const saturation = avatarSaturation === undefined || isNaN(avatarSaturation) ? 50 : avatarSaturation;
const hRange = [0, 360];
const lRangeTop = Math.round(saturation / (100 / 30)) + 30;
@@ -54,11 +56,11 @@ export const Avatar = ({comment}) => {
const hash = getHashOfString(commentMember.name);
const h = normalizeHash(hash, hRange[0], hRange[1]);
const l = normalizeHash(hash, lRange[0], lRange[1]);
-
+
return [h, saturation, l];
};
- const HSLtoString = (hsl) => {
+ const HSLtoString = (hsl: [number, number, number]) => {
return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
};
@@ -66,7 +68,7 @@ export const Avatar = ({comment}) => {
if (comment && !comment.member) {
return getInitials('Deleted member');
}
-
+
let commentMember = (comment ? comment.member : member);
if (!commentMember || !commentMember.name) {
diff --git a/apps/comments-ui/src/components/content/CTABox.js b/apps/comments-ui/src/components/content/CTABox.tsx
similarity index 89%
rename from apps/comments-ui/src/components/content/CTABox.js
rename to apps/comments-ui/src/components/content/CTABox.tsx
index 4d394edc07..bfd37492dc 100644
--- a/apps/comments-ui/src/components/content/CTABox.js
+++ b/apps/comments-ui/src/components/content/CTABox.tsx
@@ -1,8 +1,11 @@
-import AppContext from '../../AppContext';
-import {useContext} from 'react';
+import {useAppContext} from '../../AppContext';
-const CTABox = ({isFirst, isPaid}) => {
- const {accentColor, publication, member} = useContext(AppContext);
+type Props = {
+ isFirst: boolean,
+ isPaid: boolean
+};
+const CTABox: React.FC = ({isFirst, isPaid}) => {
+ const {accentColor, publication, member} = useAppContext();
const buttonStyle = {
backgroundColor: accentColor
diff --git a/apps/comments-ui/src/components/content/Comment.js b/apps/comments-ui/src/components/content/Comment.tsx
similarity index 75%
rename from apps/comments-ui/src/components/content/Comment.js
rename to apps/comments-ui/src/components/content/Comment.tsx
index 37ea12438f..20798d3b03 100644
--- a/apps/comments-ui/src/components/content/Comment.js
+++ b/apps/comments-ui/src/components/content/Comment.tsx
@@ -1,17 +1,22 @@
-import AppContext from '../../AppContext';
import EditForm from './forms/EditForm';
import LikeButton from './buttons/LikeButton';
import MoreButton from './buttons/MoreButton';
-import React, {useContext, useState} from 'react';
import Replies from './Replies';
import ReplyButton from './buttons/ReplyButton';
import ReplyForm from './forms/ReplyForm';
import {Avatar, BlankAvatar} from './Avatar';
+import {Comment, useAppContext} from '../../AppContext';
import {Transition} from '@headlessui/react';
import {formatExplicitTime, isCommentPublished} from '../../utils/helpers';
import {useRelativeTime} from '../../utils/hooks';
+import {useState} from 'react';
-function AnimatedComment({comment, parent}) {
+type AnimatedCommentProps = {
+ comment: Comment;
+ parent?: Comment;
+};
+
+const AnimatedComment: React.FC = ({comment, parent}) => {
return (
);
-}
+};
-function EditableComment({comment, parent}) {
+type EditableCommentProps = AnimatedCommentProps;
+const EditableComment: React.FC = ({comment, parent}) => {
const [isInEditMode, setIsInEditMode] = useState(false);
const closeEditMode = () => {
@@ -44,22 +50,25 @@ function EditableComment({comment, parent}) {
);
} else {
- return ();
+ return ();
}
-}
+};
-function Comment({comment, parent, openEditMode}) {
+type CommentProps = AnimatedCommentProps & {
+ openEditMode: () => void;
+};
+const CommentComponent: React.FC = ({comment, parent, openEditMode}) => {
const isPublished = isCommentPublished(comment);
if (isPublished) {
return ();
}
return ();
-}
+};
-function PublishedComment({comment, parent, openEditMode}) {
+const PublishedComment: React.FC = ({comment, parent, openEditMode}) => {
const [isInReplyMode, setIsInReplyMode] = useState(false);
- const {dispatchAction} = useContext(AppContext);
+ const {dispatchAction} = useAppContext();
const toggleReplyMode = async () => {
if (!isInReplyMode) {
@@ -86,10 +95,14 @@ function PublishedComment({comment, parent, openEditMode}) {
);
-}
+};
-function UnpublishedComment({comment, openEditMode}) {
- const {admin} = useContext(AppContext);
+type UnpublishedCommentProps = {
+ comment: Comment;
+ openEditMode: () => void;
+}
+const UnpublishedComment: React.FC = ({comment, openEditMode}) => {
+ const {admin} = useAppContext();
let notPublishedMessage;
if (admin && comment.status === 'hidden') {
@@ -114,12 +127,12 @@ function UnpublishedComment({comment, openEditMode}) {
);
-}
+};
// Helper components
-function MemberExpertise({comment}) {
- const {member} = useContext(AppContext);
+const MemberExpertise: React.FC<{comment: Comment}> = ({comment}) => {
+ const {member} = useAppContext();
const memberExpertise = member && comment.member && comment.member.uuid === member.uuid ? member.expertise : comment?.member?.expertise;
if (!memberExpertise) {
@@ -129,9 +142,9 @@ function MemberExpertise({comment}) {
return (
{memberExpertise}·
);
-}
+};
-function EditedInfo({comment}) {
+const EditedInfo: React.FC<{comment: Comment}> = ({comment}) => {
if (!comment.edited_at) {
return null;
}
@@ -140,9 +153,9 @@ function EditedInfo({comment}) {
·Edited
);
-}
+};
-function RepliesContainer({comment}) {
+const RepliesContainer: React.FC<{comment: Comment}> = ({comment}) => {
const hasReplies = comment.replies && comment.replies.length > 0;
if (!hasReplies) {
@@ -154,9 +167,14 @@ function RepliesContainer({comment}) {
);
-}
+};
-function ReplyFormBox({comment, isInReplyMode, closeReplyMode}) {
+type ReplyFormBoxProps = {
+ comment: Comment;
+ isInReplyMode: boolean;
+ closeReplyMode: () => void;
+};
+const ReplyFormBox: React.FC = ({comment, isInReplyMode, closeReplyMode}) => {
if (!isInReplyMode) {
return null;
}
@@ -166,23 +184,23 @@ function ReplyFormBox({comment, isInReplyMode, closeReplyMode}) {
);
-}
+};
//
// -- Published comment components --
//
// TODO: move name detection to helper
-function AuthorName({comment}) {
+const AuthorName: React.FC<{comment: Comment}> = ({comment}) => {
const name = !comment.member ? 'Deleted member' : (comment.member.name ? comment.member.name : 'Anonymous');
return (
{name}
);
-}
+};
-function CommentHeader({comment}) {
+const CommentHeader: React.FC<{comment: Comment}> = ({comment}) => {
const createdAtRelative = useRelativeTime(comment.created_at);
return (
@@ -199,21 +217,28 @@ function CommentHeader({comment}) {
);
-}
+};
-function CommentBody({html}) {
+const CommentBody: React.FC<{html: string}> = ({html}) => {
const dangerouslySetInnerHTML = {__html: html};
return (
);
-}
+};
-function CommentMenu({comment, toggleReplyMode, isInReplyMode, openEditMode, parent}) {
+type CommentMenuProps = {
+ comment: Comment;
+ toggleReplyMode: () => void;
+ isInReplyMode: boolean;
+ openEditMode: () => void;
+ parent?: Comment;
+};
+const CommentMenu: React.FC = ({comment, toggleReplyMode, isInReplyMode, openEditMode, parent}) => {
// If this comment is from the current member, always override member
// with the member from the context, so we update the expertise in existing comments when we change it
- const {member, commentsEnabled} = useContext(AppContext);
+ const {member, commentsEnabled} = useAppContext();
const paidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
@@ -222,25 +247,30 @@ function CommentMenu({comment, toggleReplyMode, isInReplyMode, openEditMode, par
return (
{}
- {(canReply && )}
+ {(canReply && )}
{}
);
-}
+};
//
// -- Layout --
//
-function RepliesLine({hasReplies}) {
+const RepliesLine: React.FC<{hasReplies: boolean}> = ({hasReplies}) => {
if (!hasReplies) {
return null;
}
return ();
-}
+};
-function CommentLayout({children, avatar, hasReplies}) {
+type CommentLayoutProps = {
+ children: React.ReactNode;
+ avatar: React.ReactNode;
+ hasReplies: boolean;
+}
+const CommentLayout: React.FC = ({children, avatar, hasReplies}) => {
return (
@@ -254,7 +284,7 @@ function CommentLayout({children, avatar, hasReplies}) {
);
-}
+};
//
// -- Default --
diff --git a/apps/comments-ui/src/components/content/Content.js b/apps/comments-ui/src/components/content/Content.tsx
similarity index 92%
rename from apps/comments-ui/src/components/content/Content.js
rename to apps/comments-ui/src/components/content/Content.tsx
index bfd5ef53ca..ce665a1a55 100644
--- a/apps/comments-ui/src/components/content/Content.js
+++ b/apps/comments-ui/src/components/content/Content.tsx
@@ -1,14 +1,14 @@
-import AppContext from '../../AppContext';
import CTABox from './CTABox';
import Comment from './Comment';
import ContentTitle from './ContentTitle';
import MainForm from './forms/MainForm';
import Pagination from './Pagination';
-import React, {useContext, useEffect} from 'react';
import {ROOT_DIV_ID} from '../../utils/constants';
+import {useAppContext} from '../../AppContext';
+import {useEffect} from 'react';
const Content = () => {
- const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, secundaryFormCount} = useContext(AppContext);
+ const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, secundaryFormCount} = useAppContext();
const commentsElements = comments.slice().reverse().map(comment => );
const paidOnly = commentsEnabled === 'paid';
diff --git a/apps/comments-ui/src/components/content/ContentTitle.js b/apps/comments-ui/src/components/content/ContentTitle.tsx
similarity index 75%
rename from apps/comments-ui/src/components/content/ContentTitle.js
rename to apps/comments-ui/src/components/content/ContentTitle.tsx
index 184c48bc9b..fb95fa18ab 100644
--- a/apps/comments-ui/src/components/content/ContentTitle.js
+++ b/apps/comments-ui/src/components/content/ContentTitle.tsx
@@ -1,6 +1,10 @@
import {formatNumber} from '../../utils/helpers';
-const Count = ({showCount, count}) => {
+type CountProps = {
+ showCount: boolean,
+ count: number
+};
+const Count: React.FC = ({showCount, count}) => {
if (!showCount) {
return null;
}
@@ -16,17 +20,22 @@ const Count = ({showCount, count}) => {
);
};
-const Title = ({title}) => {
+const Title: React.FC<{title: string | null}> = ({title}) => {
if (title === null) {
return (
<>Member discussion>
);
}
- return title;
+ return <>{title}>;
};
-const ContentTitle = ({title, showCount, count}) => {
+type ContentTitleProps = {
+ title: string | null,
+ showCount: boolean,
+ count: number
+};
+const ContentTitle: React.FC = ({title, showCount, count}) => {
// We have to check for null for title because null means default, wheras empty string means empty
if (!title && !showCount && title !== null) {
return null;
diff --git a/apps/comments-ui/src/components/content/Loading.js b/apps/comments-ui/src/components/content/Loading.tsx
similarity index 92%
rename from apps/comments-ui/src/components/content/Loading.js
rename to apps/comments-ui/src/components/content/Loading.tsx
index fc5148422e..e8fa1512a3 100644
--- a/apps/comments-ui/src/components/content/Loading.js
+++ b/apps/comments-ui/src/components/content/Loading.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {ReactComponent as SpinnerIcon} from '../../images/icons/spinner.svg';
function Loading() {
diff --git a/apps/comments-ui/src/components/content/Pagination.js b/apps/comments-ui/src/components/content/Pagination.tsx
similarity index 84%
rename from apps/comments-ui/src/components/content/Pagination.js
rename to apps/comments-ui/src/components/content/Pagination.tsx
index 80eb963075..2ba187a4c8 100644
--- a/apps/comments-ui/src/components/content/Pagination.js
+++ b/apps/comments-ui/src/components/content/Pagination.tsx
@@ -1,12 +1,11 @@
-import AppContext from '../../AppContext';
-import React, {useContext} from 'react';
import {formatNumber} from '../../utils/helpers';
+import {useAppContext} from '../../AppContext';
const Pagination = () => {
- const {pagination, dispatchAction} = useContext(AppContext);
+ const {pagination, dispatchAction} = useAppContext();
const loadMore = () => {
- dispatchAction('loadMoreComments');
+ dispatchAction('loadMoreComments', {});
};
if (!pagination) {
diff --git a/apps/comments-ui/src/components/content/Replies.js b/apps/comments-ui/src/components/content/Replies.tsx
similarity index 52%
rename from apps/comments-ui/src/components/content/Replies.js
rename to apps/comments-ui/src/components/content/Replies.tsx
index 03eac35f3d..5d8d231bba 100644
--- a/apps/comments-ui/src/components/content/Replies.js
+++ b/apps/comments-ui/src/components/content/Replies.tsx
@@ -1,10 +1,12 @@
-import AppContext from '../../AppContext';
-import Comment from './Comment';
+import CommentComponent from './Comment';
import RepliesPagination from './RepliesPagination';
-import {useContext} from 'react';
+import {Comment, useAppContext} from '../../AppContext';
-const Replies = ({comment}) => {
- const {dispatchAction} = useContext(AppContext);
+type Props = {
+ comment: Comment
+};
+const Replies: React.FC = ({comment}) => {
+ const {dispatchAction} = useAppContext();
const repliesLeft = comment.count.replies - comment.replies.length;
@@ -14,7 +16,7 @@ const Replies = ({comment}) => {
return (
- {comment.replies.map((reply => ))}
+ {comment.replies.map((reply => ))}
{repliesLeft > 0 && }
);
diff --git a/apps/comments-ui/src/components/content/RepliesPagination.js b/apps/comments-ui/src/components/content/RepliesPagination.tsx
similarity index 87%
rename from apps/comments-ui/src/components/content/RepliesPagination.js
rename to apps/comments-ui/src/components/content/RepliesPagination.tsx
index 123ba59cc0..bf86c8ed12 100644
--- a/apps/comments-ui/src/components/content/RepliesPagination.js
+++ b/apps/comments-ui/src/components/content/RepliesPagination.tsx
@@ -1,7 +1,11 @@
import React from 'react';
import {formatNumber} from '../../utils/helpers';
-const RepliesPagination = ({loadMore, count}) => {
+type Props = {
+ loadMore: () => void;
+ count: number;
+};
+const RepliesPagination: React.FC = ({loadMore, count}) => {
return (