diff --git a/.eslintrc.js b/.eslintrc.js index 1af7ec0b4..8c696b251 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,7 @@ module.exports = { jsx: "never", mjs: "never", ts: "never", + json: "always", }, ], diff --git a/package.json b/package.json index 778cc65b9..52979aa9b 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,15 @@ "passport-google-oauth20": "^2.0.0", "passport-local": "^1.0.0", "pg": "^8.9.0", + "preact": "^10.15.1", + "preact-render-to-string": "^6.1.0", + "preact-ssr-prepass": "^1.2.0", "pretty-error": "^3.0.4", "prop-types": "^15.8.1", - "react": "^18.2.0", + "react": "npm:@preact/compat@*", "react-autosuggest": "^10.0.2", "react-bootstrap": "^2.7.0", - "react-dom": "^18.2.0", + "react-dom": "npm:@preact/compat@*", "react-flip-toolkit": "^7.0.17", "react-geosuggest": "^2.14.1", "react-icons": "^4.7.1", diff --git a/src/client.tsx b/src/client.tsx index d1bcc7a46..d2e45dd46 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -7,8 +7,8 @@ * LICENSE.txt file in the root directory of this source tree. */ +import { hydrate, render } from "preact"; import React, { useEffect } from "react"; -import { createRoot, hydrateRoot } from "react-dom/client"; import { Action, createPath, Location } from "history"; import App from "./components/App"; import configureStore from "./store/configureStore"; @@ -91,8 +91,6 @@ if (subdomain) { const router = routerCreator(routes); -const root = createRoot(container!); - // Re-render the app when window.location changes const onLocationChange = async ({ action, @@ -197,9 +195,9 @@ const onLocationChange = async ({ }; if (isInitialRender) { - hydrateRoot(container!, ); + hydrate(, container!); } else { - root.render(); + render(, container!); } } catch (error) { if (__DEV__) { diff --git a/src/components/AddUserForm/AddUserForm.tsx b/src/components/AddUserForm/AddUserForm.tsx index 42c212d0e..d7ebbd407 100644 --- a/src/components/AddUserForm/AddUserForm.tsx +++ b/src/components/AddUserForm/AddUserForm.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, Component, FormEvent } from "react"; +import React, { ChangeEvent, Component, TargetedEvent } from "react"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; import Form from "react-bootstrap/Form"; @@ -38,9 +38,9 @@ class AddUserForm extends Component { HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement > ) => - this.setState({ [field]: event.target.value }); + this.setState({ [field]: event.currentTarget.value }); - handleSubmit = (event: FormEvent) => { + handleSubmit = (event: TargetedEvent) => { event.preventDefault(); this.props.addUserToTeam(this.state); this.setState({ ...AddUserForm.defaultState }); diff --git a/src/components/App.tsx b/src/components/App.tsx index cba74cd8d..5ed4cab62 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -9,7 +9,8 @@ import StyleContext, { InsertCSS } from "isomorphic-style-loader/StyleContext"; import PropTypes from "prop-types"; -import React, { Children, ReactNode } from "react"; +import React from "react"; +import { ComponentChildren } from "preact"; import { Provider as ReduxProvider } from "react-redux"; import { Libraries, Loader } from "@googlemaps/js-api-loader"; import { AppContext } from "../interfaces"; @@ -26,7 +27,7 @@ const ContextType = { }; interface AppProps { - children: ReactNode; + children?: ComponentChildren; context: AppContext; } @@ -63,6 +64,10 @@ class App extends React.PureComponent { static childContextTypes = ContextType; + static defaultProps = { + children: null, + }; + constructor(props: AppProps) { super(props); @@ -88,7 +93,7 @@ class App extends React.PureComponent { - {Children.only(this.props.children)} + {this.props.children} diff --git a/src/components/ChangeTeamURLModal/ChangeTeamURLModal.tsx b/src/components/ChangeTeamURLModal/ChangeTeamURLModal.tsx index 400bcbc55..d75641904 100644 --- a/src/components/ChangeTeamURLModal/ChangeTeamURLModal.tsx +++ b/src/components/ChangeTeamURLModal/ChangeTeamURLModal.tsx @@ -40,9 +40,9 @@ class ChangeTeamURLModal extends Component< handleChange = (field: keyof ChangeTeamURLModalState) => - (event: ChangeEvent) => + (event: ChangeEvent) => this.setState({ - [field]: event.target.value, + [field]: event.currentTarget.value, }); handleSubmit = () => { diff --git a/src/components/ConfirmModal/ConfirmModal.tsx b/src/components/ConfirmModal/ConfirmModal.tsx index 370d1ff79..b657e7f89 100644 --- a/src/components/ConfirmModal/ConfirmModal.tsx +++ b/src/components/ConfirmModal/ConfirmModal.tsx @@ -1,4 +1,5 @@ -import React, { ReactNode } from "react"; +import React from "react"; +import { ComponentChildren } from "preact"; import Modal from "react-bootstrap/Modal"; import ModalBody from "react-bootstrap/ModalBody"; import ModalFooter from "react-bootstrap/ModalFooter"; @@ -8,7 +9,7 @@ export interface ConfirmModalProps { actionLabel: string; shown: boolean; hideModal: () => void; - body: ReactNode; + body: ComponentChildren; handleSubmit: () => void; } diff --git a/src/components/DeleteTeamModal/DeleteTeamModal.tsx b/src/components/DeleteTeamModal/DeleteTeamModal.tsx index bfddad417..24f79d92f 100644 --- a/src/components/DeleteTeamModal/DeleteTeamModal.tsx +++ b/src/components/DeleteTeamModal/DeleteTeamModal.tsx @@ -36,9 +36,11 @@ class DeleteTeamModal extends Component< }; } - handleChange = (event: ChangeEvent) => { + handleChange = ( + event: ChangeEvent + ) => { this.setState({ - confirmSlug: event.target.value, + confirmSlug: event.currentTarget.value, }); }; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 7904e00f3..1b94ae575 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -7,7 +7,8 @@ * LICENSE.txt file in the root directory of this source tree. */ -import React, { Component, ReactNode } from "react"; +import React, { Component } from "react"; +import { VNode } from "preact"; import PropTypes from "prop-types"; import { InsertCSS } from "isomorphic-style-loader/StyleContext"; // eslint-disable-next-line css-modules/no-unused-class, no-unused-vars @@ -22,7 +23,7 @@ import s from "./Layout.scss"; const emptyFunction = () => undefined; export interface LayoutProps { - children: ReactNode; + children: VNode; isHome?: boolean; path: string; shouldScrollToTop: boolean; diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx index 185b43c2d..11844d62a 100644 --- a/src/components/Link/Link.tsx +++ b/src/components/Link/Link.tsx @@ -7,7 +7,8 @@ * LICENSE.txt file in the root directory of this source tree. */ -import React, { Component, MouseEvent, ReactNode } from "react"; +import React, { Component, HTMLAttributes } from "react"; +import { ComponentChildren } from "preact"; import history from "../../history"; function isLeftClickEvent(event: MouseEvent) { @@ -18,9 +19,9 @@ function isModifiedEvent(event: MouseEvent) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); } -interface LinkProps extends React.HTMLProps { +interface LinkProps extends HTMLAttributes { to: string; - children: ReactNode; + children: ComponentChildren; onClick?: (event: MouseEvent) => void; } diff --git a/src/components/NameFilterForm/NameFilterForm.tsx b/src/components/NameFilterForm/NameFilterForm.tsx index 1e86b568e..07f94a812 100644 --- a/src/components/NameFilterForm/NameFilterForm.tsx +++ b/src/components/NameFilterForm/NameFilterForm.tsx @@ -58,7 +58,7 @@ class NameFilterForm extends Component< }; setNameFilterValue = (event: ChangeEvent) => { - this.props.setNameFilter(event.target.value); + this.props.setNameFilter(event.currentTarget.value); }; hideForm = () => { diff --git a/src/components/NotificationList/NotificationList.tsx b/src/components/NotificationList/NotificationList.tsx index 414ac2cba..482706d27 100644 --- a/src/components/NotificationList/NotificationList.tsx +++ b/src/components/NotificationList/NotificationList.tsx @@ -12,6 +12,7 @@ interface NotificationListProps { const NotificationList = ({ notifications }: NotificationListProps) => (
    + {/* @ts-expect-error: expects component instead of children */} {notifications.map((notification) => ( ) => this.setState({ - daysAgo: Number(event.target.value), + daysAgo: Number(event.currentTarget.value), }); handleSubmit = () => { diff --git a/src/components/RestaurantAddForm/RestaurantAddForm.tsx b/src/components/RestaurantAddForm/RestaurantAddForm.tsx index 197f62742..69142cded 100644 --- a/src/components/RestaurantAddForm/RestaurantAddForm.tsx +++ b/src/components/RestaurantAddForm/RestaurantAddForm.tsx @@ -74,7 +74,7 @@ class RestaurantAddForm extends Component { return (
    {this.maps ? ( - + { if (method === "up" || method === "down") { @@ -65,7 +65,7 @@ export class _RestaurantAddTagForm extends Component< })); }; - handleSubmit = (event: FormEvent) => { + handleSubmit = (event: TargetedEvent) => { event.preventDefault(); this.props.addNewTagToRestaurant(this.state.autosuggestValue); this.setState(() => ({ @@ -74,7 +74,7 @@ export class _RestaurantAddTagForm extends Component< }; handleSuggestionSelected = ( - event: FormEvent, + event: TargetedEvent, { suggestion, method }: SuggestionSelectedEventData ) => { if (method === "enter") { diff --git a/src/components/RestaurantMap/RestaurantMap.tsx b/src/components/RestaurantMap/RestaurantMap.tsx index 7b733fed8..6f89fb5b4 100644 --- a/src/components/RestaurantMap/RestaurantMap.tsx +++ b/src/components/RestaurantMap/RestaurantMap.tsx @@ -1,7 +1,8 @@ /* eslint-disable max-classes-per-file */ import PropTypes from "prop-types"; -import React, { Component, ReactNode, Suspense, lazy } from "react"; +import React, { Component, Suspense, lazy } from "react"; +import { VNode } from "preact"; import withStyles from "isomorphic-style-loader/withStyles"; import { GOOGLE_MAP_ZOOM } from "../../constants"; import googleMapOptions from "../../helpers/googleMapOptions"; @@ -94,12 +95,12 @@ class RestaurantMap extends Component { tempMarker, } = this.props; - let tempMarkerComponent: ReactNode; + let tempMarkerComponent: VNode; if (tempMarker !== undefined) { tempMarkerComponent = ; } - let googleInfoWindow: ReactNode; + let googleInfoWindow: VNode; if ("placeId" in infoWindow && infoWindow.placeId && infoWindow.latLng) { googleInfoWindow = ( { {({ loader }) => loader ? ( - + { const mapUi = getMapUi(state); diff --git a/src/components/RestaurantMapSettings/RestaurantMapSettingsContainer.ts b/src/components/RestaurantMapSettings/RestaurantMapSettingsContainer.ts index dd2d33b12..22664d6af 100644 --- a/src/components/RestaurantMapSettings/RestaurantMapSettingsContainer.ts +++ b/src/components/RestaurantMapSettings/RestaurantMapSettingsContainer.ts @@ -20,9 +20,9 @@ const mapDispatchToProps = ( dispatch(flashSuccess("Default zoom level set for team.")) ), setShowUnvoted: (event: ChangeEvent) => - dispatch(setShowUnvoted(event.target.checked)), + dispatch(setShowUnvoted(event.currentTarget.checked)), setShowPOIs: (event: ChangeEvent) => - dispatch(setShowPOIs(event.target.checked)), + dispatch(setShowPOIs(event.currentTarget.checked)), }); export default connect( diff --git a/src/components/RestaurantMarker/RestaurantMarkerContainer.ts b/src/components/RestaurantMarker/RestaurantMarkerContainer.ts index 75d564a1d..e469eb9b2 100644 --- a/src/components/RestaurantMarker/RestaurantMarkerContainer.ts +++ b/src/components/RestaurantMarker/RestaurantMarkerContainer.ts @@ -1,4 +1,3 @@ -import { MouseEvent } from "react"; import { connect } from "react-redux"; import { Dispatch, State } from "../../interfaces"; import { getRestaurantById } from "../../selectors/restaurants"; diff --git a/src/components/RestaurantNameForm/RestaurantNameForm.tsx b/src/components/RestaurantNameForm/RestaurantNameForm.tsx index c852e07ef..ecf0d0981 100644 --- a/src/components/RestaurantNameForm/RestaurantNameForm.tsx +++ b/src/components/RestaurantNameForm/RestaurantNameForm.tsx @@ -1,10 +1,4 @@ -import React, { - ChangeEvent, - Component, - FormEvent, - RefObject, - createRef, -} from "react"; +import React, { ChangeEvent, TargetedEvent, useEffect, useRef } from "react"; import withStyles from "isomorphic-style-loader/withStyles"; import Button from "react-bootstrap/Button"; import s from "./RestaurantNameForm.scss"; @@ -12,53 +6,50 @@ import s from "./RestaurantNameForm.scss"; interface RestaurantNameFormProps { editNameFormValue: string; setEditNameFormValue: (e: ChangeEvent) => void; - changeRestaurantName: (e: FormEvent) => void; + changeRestaurantName: (e: TargetedEvent) => void; hideEditNameForm: () => void; } -class RestaurantNameForm extends Component { - input: RefObject; +const RestaurantNameForm = (props: RestaurantNameFormProps) => { + const input = useRef(null); - componentDidMount() { + useEffect(() => { // React Bootstrap steals focus, grab it back - this.input = createRef(); setTimeout(() => { - this.input.current?.focus(); + input.current?.focus(); }, 1); - } + }, []); - render() { - return ( - - - - - - - - ); - } -} + return ( +
    + + + + + +
    + ); +}; export default withStyles(s)(RestaurantNameForm); diff --git a/src/components/RestaurantNameForm/RestaurantNameFormContainer.ts b/src/components/RestaurantNameForm/RestaurantNameFormContainer.ts index 42dabcb9f..6caf8a3cf 100644 --- a/src/components/RestaurantNameForm/RestaurantNameFormContainer.ts +++ b/src/components/RestaurantNameForm/RestaurantNameFormContainer.ts @@ -1,4 +1,4 @@ -import { ChangeEvent, FormEvent } from "react"; +import { ChangeEvent, TargetedEvent } from "react"; import { connect } from "react-redux"; import { changeRestaurantName } from "../../actions/restaurants"; import { hideEditNameForm, setEditNameFormValue } from "../../actions/listUi"; @@ -23,7 +23,7 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => ({ dispatch(hideEditNameForm(ownProps.id)); }, setEditNameFormValue: (event: ChangeEvent) => { - dispatch(setEditNameFormValue(ownProps.id, event.target.value)); + dispatch(setEditNameFormValue(ownProps.id, event.currentTarget.value)); }, dispatch, }); @@ -35,7 +35,7 @@ const mergeProps = ( ) => ({ ...stateProps, ...dispatchProps, - changeRestaurantName: (event: FormEvent) => { + changeRestaurantName: (event: TargetedEvent) => { event.preventDefault(); dispatchProps.dispatch( changeRestaurantName(ownProps.id, stateProps.editNameFormValue) diff --git a/src/components/TagFilterForm/TagFilterForm.tsx b/src/components/TagFilterForm/TagFilterForm.tsx index 13161e727..7d4b893f6 100644 --- a/src/components/TagFilterForm/TagFilterForm.tsx +++ b/src/components/TagFilterForm/TagFilterForm.tsx @@ -1,4 +1,4 @@ -import React, { Component, FormEvent, RefObject, createRef } from "react"; +import React, { Component, RefObject, TargetedEvent, createRef } from "react"; import Autosuggest, { ChangeEvent, SuggestionSelectedEventData, @@ -24,7 +24,7 @@ const returnTrue = () => true; export interface TagFilterFormProps { exclude?: boolean; - addByName: (autosuggestValue: string) => (event: FormEvent) => void; + addByName: (autosuggestValue: string) => (event: TargetedEvent) => void; addTag: (id: number) => void; allTags: Tag[]; clearTags: () => void; @@ -68,7 +68,7 @@ class TagFilterForm extends Component { } setAutosuggestValue = ( - event: FormEvent, + event: TargetedEvent, { newValue, method }: ChangeEvent ) => { if (method === "up" || method === "down") { @@ -80,7 +80,7 @@ class TagFilterForm extends Component { }; handleSuggestionSelected = ( - event: FormEvent, + event: TargetedEvent, { suggestion, method }: SuggestionSelectedEventData ) => { if (method === "enter") { diff --git a/src/components/TagFilterForm/TagFilterFormContainer.ts b/src/components/TagFilterForm/TagFilterFormContainer.ts index b9347b3a8..b54419ef9 100644 --- a/src/components/TagFilterForm/TagFilterFormContainer.ts +++ b/src/components/TagFilterForm/TagFilterFormContainer.ts @@ -1,4 +1,4 @@ -import { FormEvent } from "react"; +import { TargetedEvent } from "react"; import { connect } from "react-redux"; import { addTagFilter, @@ -69,7 +69,7 @@ const mergeProps = ( ...stateProps, ...dispatchProps, addByName(autosuggestValue: string) { - return (event: FormEvent) => { + return (event: TargetedEvent) => { event.preventDefault(); const tag = stateProps.allTags.find((t) => t.name === autosuggestValue); if (tag !== undefined) { diff --git a/src/components/TeamForm/TeamForm.tsx b/src/components/TeamForm/TeamForm.tsx index 9dacf7095..451ba007b 100644 --- a/src/components/TeamForm/TeamForm.tsx +++ b/src/components/TeamForm/TeamForm.tsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import React, { ChangeEvent, Component, FormEvent } from "react"; +import React, { ChangeEvent, Component, TargetedEvent } from "react"; import { FaRegQuestionCircle } from "react-icons/fa"; import { BsPrefixRefForwardingComponent } from "react-bootstrap/esm/helpers"; import Button, { ButtonPropsWithXsSize } from "react-bootstrap/Button"; @@ -67,10 +67,11 @@ class TeamForm extends Component { handleGeosuggestChange = (value: string) => this.setState({ address: value }); handleChange = - (field: keyof TeamFormState) => (event: ChangeEvent) => - this.setState({ [field]: event.target.value }); + (field: keyof TeamFormState) => + (event: ChangeEvent) => + this.setState({ [field]: event.currentTarget.value }); - handleSubmit = (event: FormEvent) => { + handleSubmit = (event: TargetedEvent) => { event.preventDefault(); const typedsortDuration = Number(this.state.sortDuration); if (typedsortDuration > 0) { diff --git a/src/components/TeamGeosuggest/TeamGeosuggest.tsx b/src/components/TeamGeosuggest/TeamGeosuggest.tsx index e877ec4bc..1f2a506f0 100644 --- a/src/components/TeamGeosuggest/TeamGeosuggest.tsx +++ b/src/components/TeamGeosuggest/TeamGeosuggest.tsx @@ -68,7 +68,7 @@ class TeamGeosuggest extends Component { const { id, initialValue } = this.props; return this.maps ? ( - + { {({ loader }) => loader ? ( - + { } handleChange = - (field: keyof LoginState) => (event: ChangeEvent) => + (field: keyof LoginState) => + (event: ChangeEvent) => this.setState({ - [field]: event.target.value, + [field]: event.currentTarget.value, }); render() { diff --git a/src/routes/main/account/Account.tsx b/src/routes/main/account/Account.tsx index 86fa306c8..e11561c52 100644 --- a/src/routes/main/account/Account.tsx +++ b/src/routes/main/account/Account.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, Component, FormEvent } from "react"; +import React, { ChangeEvent, Component, TargetedEvent } from "react"; import withStyles from "isomorphic-style-loader/withStyles"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; @@ -34,10 +34,11 @@ class Account extends Component { } handleChange = - (field: keyof AccountState) => (event: ChangeEvent) => - this.setState({ [field]: event.target.value }); + (field: keyof AccountState) => + (event: ChangeEvent) => + this.setState({ [field]: event.currentTarget.value }); - handleSubmit = (event: FormEvent) => { + handleSubmit = (event: TargetedEvent) => { event.preventDefault(); this.props .updateCurrentUser(this.state) diff --git a/src/routes/main/new-team/NewTeam.tsx b/src/routes/main/new-team/NewTeam.tsx index d91b48ae5..e4ff248ad 100644 --- a/src/routes/main/new-team/NewTeam.tsx +++ b/src/routes/main/new-team/NewTeam.tsx @@ -1,5 +1,4 @@ -import PropTypes from "prop-types"; -import React, { ChangeEvent, Component, FormEvent } from "react"; +import React, { ChangeEvent, Component, TargetedEvent } from "react"; import withStyles from "isomorphic-style-loader/withStyles"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; @@ -44,16 +43,19 @@ class NewTeam extends Component { handleGeosuggestChange = (value: string) => this.setState({ address: value }); handleChange = - (field: keyof NewTeamState) => (event: ChangeEvent) => - this.setState({ [field]: event.target.value }); + (field: keyof NewTeamState) => + (event: ChangeEvent) => + this.setState({ [field]: event.currentTarget.value }); - handleSlugChange = (event: ChangeEvent) => { + handleSlugChange = ( + event: ChangeEvent + ) => { this.setState({ - slug: event.target.value.toLowerCase(), + slug: event.currentTarget.value.toLowerCase(), }); }; - handleSubmit = (event: FormEvent) => { + handleSubmit = (event: TargetedEvent) => { const { center, createTeam } = this.props; event.preventDefault(); diff --git a/src/routes/main/teams/Teams.tsx b/src/routes/main/teams/Teams.tsx index 9efa8901e..516b86dc7 100644 --- a/src/routes/main/teams/Teams.tsx +++ b/src/routes/main/teams/Teams.tsx @@ -1,4 +1,4 @@ -import React, { Component, MouseEvent } from "react"; +import React, { Component, TargetedEvent } from "react"; import withStyles from "isomorphic-style-loader/withStyles"; import Button from "react-bootstrap/Button"; import ListGroup from "react-bootstrap/ListGroup"; @@ -16,7 +16,7 @@ interface TeamsProps { } class Teams extends Component { - confirmLeave = (user: User, team: Team) => (event: MouseEvent) => { + confirmLeave = (user: User, team: Team) => (event: TargetedEvent) => { event.preventDefault(); this.props.confirm({ actionLabel: "Leave", diff --git a/src/routes/main/welcome/Welcome.tsx b/src/routes/main/welcome/Welcome.tsx index b255a3623..6df72094a 100644 --- a/src/routes/main/welcome/Welcome.tsx +++ b/src/routes/main/welcome/Welcome.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, Component, FormEvent } from "react"; +import React, { ChangeEvent, Component, TargetedEvent } from "react"; import withStyles from "isomorphic-style-loader/withStyles"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; @@ -28,10 +28,10 @@ class Welcome extends Component { }; } - handleChange = (event: ChangeEvent) => - this.setState({ name: event.target.value }); + handleChange = (event: ChangeEvent) => + this.setState({ name: event.currentTarget.value }); - handleSubmit = (event: FormEvent) => { + handleSubmit = (event: TargetedEvent) => { event.preventDefault(); this.props.updateCurrentUser(this.state); }; diff --git a/src/routes/team/team/Team.tsx b/src/routes/team/team/Team.tsx index 98714b92a..0f8c5fd31 100644 --- a/src/routes/team/team/Team.tsx +++ b/src/routes/team/team/Team.tsx @@ -60,10 +60,10 @@ class Team extends React.Component { (user: User) => (event: ChangeEvent) => { const { currentUser, team } = this.props; - const newRole = event.target.value as RoleType; + const newRole = event.currentTarget.value as RoleType; if ( - event.target.value === "member" && + event.currentTarget.value === "member" && getRole(currentUser, team)?.type === "member" ) { this.props.confirm({ @@ -104,7 +104,7 @@ class Team extends React.Component { if (canChangeUser(currentUser, user, team, users)) { return ( - {hasGuestRole && } {hasMemberRole && } {hasOwnerRole && } diff --git a/src/server.tsx b/src/server.tsx index e39465fae..1a470fb40 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -34,8 +34,10 @@ import React from "react"; import ReactDOM from "react-dom/server"; import expressWs from "express-ws"; import Honeybadger from "@honeybadger-io/js"; +import prepass from "preact-ssr-prepass"; import PrettyError from "pretty-error"; -import AppComponent from "./components/App"; +import { InsertCSS } from "isomorphic-style-loader/StyleContext"; +import App from "./components/App"; import Html, { HtmlProps } from "./components/Html"; import { ErrorPageWithoutStyle } from "./components/ErrorPage/ErrorPage"; import errorPageStyle from "./components/ErrorPage/ErrorPage.scss"; @@ -45,6 +47,7 @@ import teamRoutes from "./routes/team"; import mainRoutes from "./routes/main"; import passport from "./passport"; import routerCreator from "./router"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import chunks from "./chunk-manifest.json"; // eslint-disable-line import/no-unresolved import configureStore from "./store/configureStore"; @@ -56,8 +59,7 @@ import passwordMiddleware from "./middlewares/password"; import usersMiddleware from "./middlewares/users"; import api from "./api"; import { sequelize, Team, User } from "./db"; -import { App, AppContext, ExtWebSocket, Flash, StateData } from "./interfaces"; -import { InsertCSS } from "isomorphic-style-loader/StyleContext"; +import { AppContext, ExtWebSocket, Flash, StateData } from "./interfaces"; process.on("unhandledRejection", (reason, p) => { console.error("Unhandled Rejection at:", p, "reason:", reason); @@ -104,6 +106,7 @@ export const wsServer = internalWsServer; // user agent is not known. // ----------------------------------------------------------------------------- globalThis.navigator = globalThis.navigator || {}; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore globalThis.navigator.userAgent = globalThis.navigator.userAgent || "all"; @@ -283,6 +286,7 @@ const wsInstance = expressWs(app, wsServer); export const wss = wsInstance.getWss(); app.use((req, res, next) => { + // eslint-disable-next-line no-param-reassign req.broadcast = (teamId, data) => { wss.clients?.forEach((client: ExtWebSocket) => { if (client.teamId === teamId) { @@ -401,9 +405,11 @@ const render: RequestHandler = async (req, res, next) => { root: generateUrl(req, req.get("host")!), }; - data.children = ReactDOM.renderToString( - {route.component} - ); + const vdom = {route.component}; + + await prepass(vdom); + + data.children = ReactDOM.renderToString(vdom); data.styles = [{ id: "css", cssText: [...css].join("") }]; const scripts = new Set(); @@ -429,6 +435,7 @@ const render: RequestHandler = async (req, res, next) => { res.status(route.status || 200); res.send(`${html}`); } catch (err) { + console.log("HERES THE ERROR YOU WNATED", err); next(err); } }; diff --git a/test/test-utils.tsx b/test/test-utils.tsx index 9ee25a186..59ea7a8cc 100644 --- a/test/test-utils.tsx +++ b/test/test-utils.tsx @@ -1,12 +1,10 @@ -import { ReactElement } from "react"; +import { VNode } from "preact"; import { render, RenderOptions } from "@testing-library/react"; -const AllTheProviders = ({ children }: { children: ReactElement }) => children; +const AllTheProviders = ({ children }: { children: VNode }) => children; -const customRender = ( - ui: ReactElement, - options?: Omit -) => render(ui, { wrapper: AllTheProviders, ...options }); +const customRender = (ui: VNode, options?: Omit) => + render(ui, { wrapper: AllTheProviders, ...options }); export * from "@testing-library/react"; export { customRender as render }; diff --git a/tsconfig.json b/tsconfig.json index 2d4e9f7d4..f65863d77 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,10 @@ "jsx": "react", "allowJs": true, "moduleResolution": "node", + "paths": { + "react": ["./node_modules/preact/compat/"], + "react-dom": ["./node_modules/preact/compat/"] + }, "resolveJsonModule": true, "strictPropertyInitialization": false, "typeRoots": ["./typings", "node_modules/@types"] diff --git a/yarn.lock b/yarn.lock index 585aaab22..bd8bbb3da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9159,7 +9159,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -9338,16 +9338,19 @@ __metadata: pg: ^8.9.0 postcss: ^8.4.21 postcss-loader: ^7.0.2 + preact: ^10.15.1 + preact-render-to-string: ^6.1.0 + preact-ssr-prepass: ^1.2.0 prettier: 2.8.8 pretty-error: ^3.0.4 prop-types: ^15.8.1 proxyquire: ^1.7.11 raw-loader: ^0.5.1 - react: ^18.2.0 + react: "npm:@preact/compat@*" react-autosuggest: ^10.0.2 react-bootstrap: ^2.7.0 react-dev-utils: ^12.0.1 - react-dom: ^18.2.0 + react-dom: "npm:@preact/compat@*" react-error-overlay: ^4.0.1 react-flip-toolkit: ^7.0.17 react-geosuggest: ^2.14.1 @@ -11156,6 +11159,33 @@ __metadata: languageName: node linkType: hard +"preact-render-to-string@npm:^6.1.0": + version: 6.1.0 + resolution: "preact-render-to-string@npm:6.1.0" + dependencies: + pretty-format: ^3.8.0 + peerDependencies: + preact: ">=10" + checksum: 2bf62e90d14b391bf20a16442ceaec252c94cf9dd22ec27e9aa7ce92ff4339c299a025ae804d5237867c398c6609aeee282bbc269a15ecf28be4923491fb9f55 + languageName: node + linkType: hard + +"preact-ssr-prepass@npm:^1.2.0": + version: 1.2.0 + resolution: "preact-ssr-prepass@npm:1.2.0" + peerDependencies: + preact: ">=10 || ^10.0.0-beta.0 || ^10.0.0-alpha.0" + checksum: 5c27122cb5e0838d25ffc8be4e63641c9ce7637a9e210d32852be609bee74cb68f79601089270b08812e45f779f81a316fb8a7e6ebb27a3b1493b17c36df41f6 + languageName: node + linkType: hard + +"preact@npm:^10.15.1": + version: 10.15.1 + resolution: "preact@npm:10.15.1" + checksum: dabad11843b19b40b11846a25ff0b1fc4bc58268909e01a7faddb341a40983d24fbe7d44ad6e2c5d35e43091963af616800552fec9af44dd0a2f0f698d1bba1f + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -11211,6 +11241,13 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^3.8.0": + version: 3.8.0 + resolution: "pretty-format@npm:3.8.0" + checksum: 21a114d43ef06978f8f7f6212be4649b0b094f05d9b30e14e37550bf35c8ca24d8adbca9e5adc4cc15d9eaf7a1e7a30478a4dc37b30982bfdf0292a5b385484c + languageName: node + linkType: hard + "process-on-spawn@npm:^1.0.0": version: 1.0.0 resolution: "process-on-spawn@npm:1.0.0" @@ -11510,15 +11547,12 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.2.0": - version: 18.2.0 - resolution: "react-dom@npm:18.2.0" - dependencies: - loose-envify: ^1.1.0 - scheduler: ^0.23.0 +"react-dom@npm:@preact/compat@*, react@npm:@preact/compat@*": + version: 17.1.2 + resolution: "@preact/compat@npm:17.1.2" peerDependencies: - react: ^18.2.0 - checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc + preact: "*" + checksum: 512b0f149fc11e36c5980c4173c696d17b2b4e78f500ca44fde9c688a1d58b835b70c91161d6d5d903e5d46b4649fc73b28151f43d7cdf91ec2825b24e8cdf7b languageName: node linkType: hard @@ -11675,15 +11709,6 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.2.0": - version: 18.2.0 - resolution: "react@npm:18.2.0" - dependencies: - loose-envify: ^1.1.0 - checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b - languageName: node - linkType: hard - "read-pkg-up@npm:^7.0.1": version: 7.0.1 resolution: "read-pkg-up@npm:7.0.1" @@ -12314,15 +12339,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0": - version: 0.23.0 - resolution: "scheduler@npm:0.23.0" - dependencies: - loose-envify: ^1.1.0 - checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a - languageName: node - linkType: hard - "schema-utils@npm:2.7.0": version: 2.7.0 resolution: "schema-utils@npm:2.7.0"