Link to friend in card

This commit is contained in:
Elizabeth Hunt 2023-04-04 13:27:33 -06:00
parent 6a1a270c94
commit ea279b7295
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
6 changed files with 191 additions and 59 deletions

View File

@ -11,6 +11,7 @@
"chota": "^0.9.2", "chota": "^0.9.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-modal": "^3.16.1",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"socket.io-client": "^4.6.1" "socket.io-client": "^4.6.1"
}, },
@ -1044,6 +1045,11 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@ -1205,6 +1211,14 @@
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
"dev": true "dev": true
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@ -1241,6 +1255,16 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -1264,6 +1288,34 @@
"react": "^18.2.0" "react": "^18.2.0"
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-modal": {
"version": "3.16.1",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
"integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
"dependencies": {
"exenv": "^1.2.0",
"prop-types": "^15.7.2",
"react-lifecycles-compat": "^3.0.0",
"warning": "^4.0.3"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -1509,6 +1561,14 @@
} }
} }
}, },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/ws": { "node_modules/ws": {
"version": "8.11.0", "version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",

View File

@ -12,6 +12,7 @@
"chota": "^0.9.2", "chota": "^0.9.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-modal": "^3.16.1",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"socket.io-client": "^4.6.1" "socket.io-client": "^4.6.1"
}, },

View File

@ -1,19 +1,24 @@
import { ago } from "../utils/ago"; import { ago } from "../utils/ago";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const replaceReferencedFriendsInName = (name, referencedFriends) => { const replaceReferencedFriendsInName = (name, referencedFriends, onSelect) => {
const friendIdToFriend = referencedFriends.reduce((friendMap, friend) => { const friendIdToFriend = referencedFriends.reduce((friendMap, friend) => {
friendMap[friend.id] = friend; friendMap[friend.id] = friend;
return friendMap; return friendMap;
}, {}); }, {});
return name.replaceAll( return name.split(/(@\<\d+\>)/g).map((s) => {
/@\<(\d+)\>/g, const matches = /@\<(\d+)\>/g.exec(s);
(_match, id) => friendIdToFriend[id].name if (matches) {
); const [_match, id] = matches;
}; const name = friendIdToFriend[id].name;
// name.replaceAll(
export default function TimerCard({ timer }) { return <a onClick={() => onSelect({ friendId: id })}>{name}</a>;
}
return s;
});
};
export default function TimerCard({ timer, onSelect }) {
const [since, setSince] = useState(ago(timer.start)); const [since, setSince] = useState(ago(timer.start));
useEffect(() => { useEffect(() => {
@ -33,7 +38,13 @@ export default function TimerCard({ timer }) {
return ( return (
<h1> <h1>
<code>{since}</code>{" "} <code>{since}</code>{" "}
{replaceReferencedFriendsInName(timer.name, timer.referenced_friends)} {replaceReferencedFriendsInName(
timer.name,
timer.referenced_friends,
onSelect
).map((s, i) => (
<span key={i}>{s}</span>
))}
</h1> </h1>
); );
} }

View File

@ -1,28 +1,66 @@
import Modal from "react-modal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useAuthContext } from "../context/authContext"; import { useAuthContext } from "../context/authContext";
export default function TimerHeader({ onSelect }) { const customStyles = {
const [friends, setFriends] = useState([]); content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
width: "40vw",
maxWidth: "800px",
},
};
Modal.setAppElement("#root");
export default function TimerHeader({ friends, selected, onSelect }) {
const [modalOpen, setModalOpen] = useState(false);
const { friendName, setSignedIn } = useAuthContext(); const { friendName, setSignedIn } = useAuthContext();
const [selected, setSelected] = useState();
const logout = () => { const logout = () => {
fetch("/api/auth/logout").then(() => setSignedIn(false)); fetch("/api/auth/logout").then(() => setSignedIn(false));
}; };
useEffect(() => {
fetch("/api/auth/friends")
.then((r) => r.json())
.then((friends) => setFriends(friends));
}, []);
return ( return (
<>
<Modal
isOpen={modalOpen}
onRequestClose={() => setModalOpen(false)}
style={customStyles}
>
<div id="createTimerModal">
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
}}
>
<span>New Timer</span>
<a onClick={() => setModalOpen(false)} className="button outline">
&times;
</a>
</div>
<div>
<form>
<button type="submit">Add</button>
</form>
</div>
</div>
</div>
</Modal>
<nav className="nav"> <nav className="nav">
<div className="nav-left"> <div className="nav-left">
<div className="tabs"> <div className="tabs">
<a <a
onClick={() => { onClick={() => {
setSelected(undefined);
onSelect(undefined); onSelect(undefined);
}} }}
className={selected ? "" : "active"} className={selected ? "" : "active"}
@ -33,10 +71,9 @@ export default function TimerHeader({ onSelect }) {
<a <a
key={friend.id} key={friend.id}
onClick={() => { onClick={() => {
setSelected(friend.id);
onSelect({ friendId: friend.id }); onSelect({ friendId: friend.id });
}} }}
className={selected === friend.id ? "active" : ""} className={selected?.friendId == friend.id ? "active" : ""}
> >
{friend.name} {friend.name}
</a> </a>
@ -44,6 +81,13 @@ export default function TimerHeader({ onSelect }) {
</div> </div>
</div> </div>
<div className="nav-right"> <div className="nav-right">
<a
onClick={() => setModalOpen(true)}
style={{ marginTop: "1rem" }}
className="button outline"
>
+
</a>
<details className="dropdown"> <details className="dropdown">
<summary style={{ marginTop: "1rem" }} className="button outline"> <summary style={{ marginTop: "1rem" }} className="button outline">
{friendName} {friendName}
@ -54,5 +98,6 @@ export default function TimerHeader({ onSelect }) {
</details> </details>
</div> </div>
</nav> </nav>
</>
); );
} }

View File

@ -57,7 +57,11 @@ export default function Login() {
setErrors([error]); setErrors([error]);
}; };
if (signedIn) return <Navigate to="/" />;
if (signedIn) {
return <Navigate to="/" />;
}
if (!token) if (!token)
return ( return (
<div className="body-centered"> <div className="body-centered">

View File

@ -42,6 +42,20 @@ export default function Timers() {
namespace: "/events/timers", namespace: "/events/timers",
query: {}, query: {},
}); });
const [friends, setFriends] = useState([]);
const [selected, setSelected] = useState();
useEffect(() => {
fetch("/api/auth/friends")
.then((r) => r.json())
.then((friends) => setFriends(friends));
}, []);
const onSelect = (selected: TimersFilter) => {
setSelected(selected);
setEndpoint(makeEndpoint(selected));
setQuery(selected);
};
useEffect(() => { useEffect(() => {
socket?.on("refreshed", (newTimer: TimerResponse) => { socket?.on("refreshed", (newTimer: TimerResponse) => {
@ -60,12 +74,7 @@ export default function Timers() {
return ( return (
<div className="container"> <div className="container">
<TimerHeader <TimerHeader friends={friends} selected={selected} onSelect={onSelect} />
onSelect={(selected: TimersFilter) => {
setEndpoint(makeEndpoint(selected));
setQuery(selected);
}}
/>
{timers ? ( {timers ? (
timers timers
.map((timer) => ({ .map((timer) => ({
@ -73,7 +82,9 @@ export default function Timers() {
start: new Date(timer.start), start: new Date(timer.start),
})) }))
.sort(({ start: startA }, { start: startB }) => startB - startA) .sort(({ start: startA }, { start: startB }) => startB - startA)
.map((timer) => <TimerCard timer={timer} key={timer.id} />) .map((timer) => (
<TimerCard onSelect={onSelect} timer={timer} key={timer.id} />
))
) : ( ) : (
<></> <></>
)} )}