diff --git a/client/package-lock.json b/client/package-lock.json index a11531b..4738066 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "chota": "^0.9.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-mentions": "^4.4.7", "react-modal": "^3.16.1", "react-router-dom": "^6.10.0", "socket.io-client": "^4.6.1" @@ -330,6 +331,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", + "dependencies": { + "regenerator-runtime": "^0.13.2" + } + }, "node_modules/@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", @@ -1109,6 +1118,14 @@ "node": ">=4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", @@ -1298,6 +1315,21 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-mentions": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.7.tgz", + "integrity": "sha512-VNriu2h/uOB+RS0mwZgPG2Vf+UtdDvRh5zbXa2TNc1WqacKuNDgTdhlbo9LEOZRBxRzIeTUYQmYJ7p9M9rDHqQ==", + "dependencies": { + "@babel/runtime": "7.4.5", + "invariant": "^2.2.4", + "prop-types": "^15.5.8", + "substyle": "^9.1.0" + }, + "peerDependencies": { + "react": ">=16.8.3", + "react-dom": ">=16.8.3" + } + }, "node_modules/react-modal": { "version": "3.16.1", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", @@ -1355,6 +1387,11 @@ "react-dom": ">=16.8" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -1440,6 +1477,18 @@ "node": ">=0.10.0" } }, + "node_modules/substyle": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz", + "integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==", + "dependencies": { + "@babel/runtime": "^7.3.4", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": ">=16.8.3" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/client/package.json b/client/package.json index fc5a173..dd29d51 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "chota": "^0.9.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-mentions": "^4.4.7", "react-modal": "^3.16.1", "react-router-dom": "^6.10.0", "socket.io-client": "^4.6.1" diff --git a/client/src/components/timerHeader.tsx b/client/src/components/timerHeader.tsx index 99f93e9..5f4e679 100644 --- a/client/src/components/timerHeader.tsx +++ b/client/src/components/timerHeader.tsx @@ -1,36 +1,55 @@ import Modal from "react-modal"; import { useEffect, useState } from "react"; +import { Mention, MentionsInput } from "react-mentions"; import { useAuthContext } from "../context/authContext"; - -const customStyles = { - content: { - top: "50%", - left: "50%", - right: "auto", - bottom: "auto", - marginRight: "-50%", - transform: "translate(-50%, -50%)", - width: "40vw", - maxWidth: "800px", - }, -}; +import mentionStyles from "../styles/mention"; +import modalStyles from "../styles/modal"; Modal.setAppElement("#root"); export default function TimerHeader({ friends, selected, onSelect }) { const [modalOpen, setModalOpen] = useState(false); + const [newTimerName, setNewTimerName] = useState(""); + const [errors, setErrors] = useState([]); const { friendName, setSignedIn } = useAuthContext(); const logout = () => { fetch("/api/auth/logout").then(() => setSignedIn(false)); }; + const createTimer = (e) => { + e.preventDefault(); + + fetch("/api/timers", { + method: "POST", + body: JSON.stringify({ + name: newTimerName.replaceAll( + /\[@[\w\d\s]+\]\((\@<\d+>)\)/g, + (_match, atId) => atId + ), + }), + headers: { + "Content-Type": "application/json", + }, + }) + .then((r) => r.json()) + .then((r) => { + if (r.message) { + setErrors([r.message]); + return; + } + setNewTimerName(""); + setErrors([]); + setModalOpen(false); + }); + }; + return ( <> setModalOpen(false)} - style={customStyles} + style={modalStyles} >
@@ -42,13 +61,35 @@ export default function TimerHeader({ friends, selected, onSelect }) { marginBottom: "1rem", }} > - New Timer +

New Timer

+ setModalOpen(false)} className="button outline"> ×
-
+ + setNewTimerName(e.target.value)} + > + ({ + id: `@<${id}>`, + display: `@${name}`, + }))} + /> + + {errors.length ? ( + errors.map((error, i) => ( +
+ {error} +
+ )) + ) : ( + <> + )}
diff --git a/client/src/routes/login.tsx b/client/src/routes/login.tsx index 8446348..c466196 100644 --- a/client/src/routes/login.tsx +++ b/client/src/routes/login.tsx @@ -31,9 +31,11 @@ export default function Login() { const getTokenFormSubmission = async (e) => { e.preventDefault(); - const { error, token } = await requestTokenSubmit(e.target.name.value); - if (error) { - setErrors([error]); + const { error, message, token } = await requestTokenSubmit( + e.target.name.value + ); + if (error && message) { + setErrors([message]); return; } setErrors([]); @@ -42,9 +44,8 @@ export default function Login() { const signTokenFormSubmission = async (e) => { e.preventDefault(); - const { error, token, expiration, friend } = await submitSignedToken( - e.target.signature.value - ); + const { error, message, token, expiration, friend } = + await submitSignedToken(e.target.signature.value); if (token) { setSignedIn(true); @@ -55,7 +56,9 @@ export default function Login() { return; } - setErrors([error]); + if (error & message) { + setErrors([message]); + } }; if (signedIn) { diff --git a/client/src/styles/mention.ts b/client/src/styles/mention.ts new file mode 100644 index 0000000..f7b0f9e --- /dev/null +++ b/client/src/styles/mention.ts @@ -0,0 +1,47 @@ +export default { + control: { + backgroundColor: "#fff", + fontSize: 16, + // fontWeight: 'normal', + }, + "&multiLine": { + control: { + fontFamily: "monospace", + minHeight: 63, + }, + highlighter: { + padding: 9, + border: "1px solid transparent", + }, + input: { + padding: 9, + border: "1px solid silver", + }, + }, + "&singleLine": { + display: "inline-block", + width: 180, + highlighter: { + padding: 1, + border: "2px inset transparent", + }, + input: { + padding: 1, + border: "2px inset", + }, + }, + suggestions: { + list: { + backgroundColor: "white", + border: "1px solid rgba(0,0,0,0.15)", + fontSize: 16, + }, + item: { + padding: "5px 15px", + borderBottom: "1px solid rgba(0,0,0,0.15)", + "&focused": { + backgroundColor: "#cee4e5", + }, + }, + }, +}; diff --git a/client/src/styles/modal.ts b/client/src/styles/modal.ts new file mode 100644 index 0000000..09c383c --- /dev/null +++ b/client/src/styles/modal.ts @@ -0,0 +1,12 @@ +export default { + content: { + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + marginRight: "-50%", + transform: "translate(-50%, -50%)", + width: "40vw", + maxWidth: "800px", + }, +};