final migration over to react_on_rails / react18
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useState } from "react";
|
||||
import style from "./HelloWorld.module.css";
|
||||
|
||||
const HelloWorld = (props) => {
|
||||
const [name, setName] = useState(props.name);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Hello, {name}!</h3>
|
||||
<hr />
|
||||
<form>
|
||||
<label className={style.bright} htmlFor="name">
|
||||
Say hello to:
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HelloWorld.propTypes = {
|
||||
name: PropTypes.string.isRequired, // this is passed from the Rails view
|
||||
};
|
||||
|
||||
export default HelloWorld;
|
||||
@@ -1,4 +0,0 @@
|
||||
.bright {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import HelloWorld from './HelloWorld';
|
||||
// This could be specialized for server rendering
|
||||
// For example, if using React-Router, we'd have the SSR setup here.
|
||||
|
||||
export default HelloWorld;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { debounce, isEmpty } from "lodash";
|
||||
import * as React from "react";
|
||||
import ListItem from "./ListItem";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Icon from "./Icon";
|
||||
import ListItem from "./ListItem";
|
||||
|
||||
// staging
|
||||
const HOST = "http://scraper.local:3001";
|
||||
// const HOST = "";
|
||||
// const HOST = "http://scraper.local:3001";
|
||||
const HOST = "";
|
||||
const COMMON_LIST_ELEM_CLASSES = `
|
||||
w-full p-2 pl-8
|
||||
text-xl font-light
|
||||
@@ -26,17 +26,27 @@ const INPUT_ELEM_CLASSES = `
|
||||
placeholder:font-extralight
|
||||
`;
|
||||
|
||||
interface PropTypes {}
|
||||
export default function UserSearchBar({}: PropTypes) {
|
||||
interface PropTypes {
|
||||
isServerRendered?: boolean;
|
||||
}
|
||||
interface ServerResponse {
|
||||
users: { name: string }[];
|
||||
}
|
||||
export default function UserSearchBar({ isServerRendered }: PropTypes) {
|
||||
isServerRendered = !!isServerRendered;
|
||||
const [userName, setUserName] = useState("");
|
||||
const [items, setUserList] = useState([]);
|
||||
const [selectedIdx, setSelectedIdx] = useState(null);
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [pendingRequest, setPendingRequest] = useState(null);
|
||||
const [items, setUserList] = useState<ServerResponse["users"]>([]);
|
||||
const [selectedIdx, setSelectedIdx] = useState<number | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [pendingRequest, setPendingRequest] = useState<AbortController | null>(
|
||||
null
|
||||
);
|
||||
const [typingSettled, setTypingSettled] = useState(true);
|
||||
const inputRef = useRef(null);
|
||||
const inputHasFocus = document.activeElement == inputRef.current;
|
||||
|
||||
const [isFocused, setIsFocused] = useState(
|
||||
isServerRendered ? false : document.activeElement == inputRef.current
|
||||
);
|
||||
|
||||
const clearResults = useCallback(() => {
|
||||
setUserList([]);
|
||||
@@ -81,22 +91,26 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
|
||||
const controller = new AbortController();
|
||||
setPendingRequest(controller);
|
||||
setShowSpinner(true);
|
||||
fetch(`${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(handleServerResponse)
|
||||
.catch((err) => {
|
||||
if (err.code != err.ABORT_ERR) {
|
||||
clearResults();
|
||||
setErrorMessage(err.toString());
|
||||
async function doFetch() {
|
||||
try {
|
||||
let response = await fetch(
|
||||
`${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`,
|
||||
{
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
await handleServerResponse(response);
|
||||
} catch (error) {
|
||||
if (error.name == "AbortError") {
|
||||
return;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setShowSpinner(false);
|
||||
clearResults();
|
||||
setErrorMessage(error.toString());
|
||||
} finally {
|
||||
setPendingRequest(null);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
doFetch();
|
||||
return () => {
|
||||
cancelPendingRequest();
|
||||
};
|
||||
@@ -104,9 +118,9 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
|
||||
const handleServerResponse = useCallback(async (response) => {
|
||||
clearResults();
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
const contentType: string = response.headers.get("Content-Type");
|
||||
if (response.status == 200) {
|
||||
const { users } = await response.json();
|
||||
const { users }: ServerResponse = await response.json();
|
||||
setUserList(users);
|
||||
} else if (
|
||||
!isEmpty(contentType) &&
|
||||
@@ -136,6 +150,11 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
case "ArrowUp":
|
||||
selectPrevListElem();
|
||||
break;
|
||||
case "Enter":
|
||||
if (selectedIdx != null) {
|
||||
setUserName(items[selectedIdx].name);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -164,18 +183,15 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
setSelectedIdx(newIdx);
|
||||
}
|
||||
|
||||
const errorShown = isFocused && !isEmpty(errorMessage);
|
||||
const infoShown =
|
||||
inputHasFocus &&
|
||||
isFocused &&
|
||||
!isEmpty(userName) &&
|
||||
pendingRequest == null &&
|
||||
typingSettled &&
|
||||
items.length == 0;
|
||||
const errorShown = !isEmpty(errorMessage);
|
||||
const itemsShown =
|
||||
inputHasFocus &&
|
||||
items.length > 0 &&
|
||||
typingSettled &&
|
||||
pendingRequest == null;
|
||||
isFocused && items.length > 0 && typingSettled && pendingRequest == null;
|
||||
const anyShown = infoShown || errorShown || itemsShown;
|
||||
|
||||
function UserSearchBarItems() {
|
||||
@@ -184,7 +200,7 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
{errorShown ? (
|
||||
<ListItem
|
||||
key="error"
|
||||
isLast={items.length == 0}
|
||||
isLast={!infoShown && items.length == 0}
|
||||
selected={false}
|
||||
style="error"
|
||||
value={errorMessage}
|
||||
@@ -193,7 +209,7 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
{infoShown ? (
|
||||
<ListItem
|
||||
key="info"
|
||||
isLast={true}
|
||||
isLast={!itemsShown}
|
||||
selected={false}
|
||||
style="info"
|
||||
value="No users found"
|
||||
@@ -202,7 +218,7 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
{itemsShown
|
||||
? items.map(({ name }, idx) => (
|
||||
<ListItem
|
||||
key={name}
|
||||
key={"name-" + name}
|
||||
isLast={idx == items.length - 1}
|
||||
selected={idx == selectedIdx}
|
||||
style="item"
|
||||
@@ -227,7 +243,7 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
type="magnifying-glass"
|
||||
className={`ml-2 ${FOCUSABLE_SVG_ELEM_CLASSES}`}
|
||||
/>
|
||||
{showSpinner ? (
|
||||
{pendingRequest != null ? (
|
||||
<Icon type="spinner" className={`right-2 ${SVG_ELEM_CLASSES}`} />
|
||||
) : null}
|
||||
<input
|
||||
@@ -243,6 +259,8 @@ export default function UserSearchBar({}: PropTypes) {
|
||||
defaultValue={userName}
|
||||
onChange={(v) => searchForUser(v.target.value)}
|
||||
onKeyDown={(v) => onSearchInputKeyDown(v)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</label>
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from "react";
|
||||
import UserSearchBar from "./UserSearchBar";
|
||||
|
||||
export default function (props) {
|
||||
return <UserSearchBar {...props} isServerRendered={true} />;
|
||||
}
|
||||
7
app/javascript/packs/application-bundle.js
Normal file
7
app/javascript/packs/application-bundle.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import ReactOnRails from "react-on-rails";
|
||||
|
||||
import UserSearchBar from "../bundles/Main/components/UserSearchBar";
|
||||
// This is how react_on_rails can see the UserSearchBar in the browser.
|
||||
ReactOnRails.register({
|
||||
UserSearchBar,
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
// Support component names relative to this directory:
|
||||
var componentRequireContext = require.context("components", true);
|
||||
var ReactRailsUJS = require("react_ujs");
|
||||
ReactRailsUJS.useContext(componentRequireContext);
|
||||
@@ -1,8 +0,0 @@
|
||||
import ReactOnRails from 'react-on-rails';
|
||||
|
||||
import HelloWorld from '../bundles/HelloWorld/components/HelloWorld';
|
||||
|
||||
// This is how react_on_rails can see the HelloWorld in the browser.
|
||||
ReactOnRails.register({
|
||||
HelloWorld,
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import ReactOnRails from 'react-on-rails';
|
||||
import ReactOnRails from "react-on-rails";
|
||||
|
||||
import HelloWorld from '../bundles/HelloWorld/components/HelloWorldServer';
|
||||
import UserSearchBar from "../bundles/Main/components/UserSearchBarServer";
|
||||
|
||||
// This is how react_on_rails can see the HelloWorld in the browser.
|
||||
// This is how react_on_rails can see the UserSearchBar in the browser.
|
||||
ReactOnRails.register({
|
||||
HelloWorld,
|
||||
UserSearchBar,
|
||||
});
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// By default, this pack is loaded for server-side rendering.
|
||||
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
|
||||
var componentRequireContext = require.context("components", true);
|
||||
var ReactRailsUJS = require("react_ujs");
|
||||
ReactRailsUJS.useContext(componentRequireContext);
|
||||
@@ -1,2 +0,0 @@
|
||||
<h1>Hello World</h1>
|
||||
<%= react_component("HelloWorld", props: @hello_world_props, prerender: false) %>
|
||||
@@ -3,6 +3,15 @@
|
||||
<head>
|
||||
<title><%= @site_title %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<% if Object.const_defined?('Rack::MiniProfiler') %>
|
||||
<%# needed so miniprofiler doens't screw with the fetch() api %>
|
||||
<script type='text/javascript'>
|
||||
if(!window.MiniProfiler) {
|
||||
window.MiniProfiler = {};
|
||||
}
|
||||
window.MiniProfiler.patchesApplied = true;
|
||||
</script>
|
||||
<% end %>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ReactOnRailsWithShakapacker</title>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= javascript_pack_tag 'hello-world-bundle' %>
|
||||
<%= stylesheet_pack_tag 'hello-world-bundle' %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +1,6 @@
|
||||
<% content_for :head do %>
|
||||
<%#= javascript_pack_tag "application" %>
|
||||
<%= javascript_pack_tag 'hello-world-bundle' %>
|
||||
<%= javascript_pack_tag 'application-bundle' %>
|
||||
<% end %>
|
||||
<div class='w-full mt-2 sm:mt-6'>
|
||||
<%#= react_component("UserSearchBar", {}, prerender: true) %>
|
||||
<%= react_component("HelloWorld", props: {}, prerender: true) %>
|
||||
<%= react_component("UserSearchBar", props: {}, prerender: true) %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Rails.application.routes.draw do
|
||||
get 'hello_world', to: 'hello_world#index'
|
||||
root to: "pages#root"
|
||||
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
10
config/webpack/staging.js
Normal file
10
config/webpack/staging.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// The source code including full typescript support is available at:
|
||||
// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/production.js
|
||||
|
||||
const webpackConfig = require('./webpackConfig');
|
||||
|
||||
const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig) => {
|
||||
// place any code here that is for production only
|
||||
};
|
||||
|
||||
module.exports = webpackConfig(productionEnvOnly);
|
||||
@@ -37,13 +37,13 @@ development:
|
||||
# port: 8080
|
||||
compress: true
|
||||
# Note that apps that do not check the host are vulnerable to DNS rebinding attacks
|
||||
allowed_hosts: [ 'localhost' ]
|
||||
allowed_hosts: ["localhost"]
|
||||
pretty: true
|
||||
headers:
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
static:
|
||||
watch:
|
||||
ignored: '**/node_modules/**'
|
||||
ignored: "**/node_modules/**"
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
@@ -52,11 +52,12 @@ test:
|
||||
# Compile test packs to a separate directory
|
||||
public_output_path: packs-test
|
||||
|
||||
production:
|
||||
production: &production
|
||||
<<: *default
|
||||
|
||||
# Production depends on precompilation of packs prior to booting for performance.
|
||||
compile: false
|
||||
|
||||
# Cache manifest.json for performance
|
||||
cache_manifest: true
|
||||
|
||||
staging:
|
||||
<<: *production
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/react": "^18.0.33",
|
||||
"react-refresh": "^0.14.0"
|
||||
}
|
||||
|
||||
@@ -1218,6 +1218,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/lodash@^4.14.192":
|
||||
version "4.14.192"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285"
|
||||
integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==
|
||||
|
||||
"@types/mime@*":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
|
||||
|
||||
Reference in New Issue
Block a user