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",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-modal": "^3.16.1",
"react-router-dom": "^6.10.0",
"socket.io-client": "^4.6.1"
},
@ -1044,6 +1045,11 @@
"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": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@ -1205,6 +1211,14 @@
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
"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": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@ -1241,6 +1255,16 @@
"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": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -1264,6 +1288,34 @@
"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": {
"version": "0.14.0",
"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": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",

View File

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

View File

@ -1,19 +1,24 @@
import { ago } from "../utils/ago";
import { useEffect, useState } from "react";
const replaceReferencedFriendsInName = (name, referencedFriends) => {
const replaceReferencedFriendsInName = (name, referencedFriends, onSelect) => {
const friendIdToFriend = referencedFriends.reduce((friendMap, friend) => {
friendMap[friend.id] = friend;
return friendMap;
}, {});
return name.replaceAll(
/@\<(\d+)\>/g,
(_match, id) => friendIdToFriend[id].name
);
};
// name.replaceAll(
return name.split(/(@\<\d+\>)/g).map((s) => {
const matches = /@\<(\d+)\>/g.exec(s);
if (matches) {
const [_match, id] = matches;
const name = friendIdToFriend[id].name;
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));
useEffect(() => {
@ -33,7 +38,13 @@ export default function TimerCard({ timer }) {
return (
<h1>
<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>
);
}

View File

@ -1,58 +1,103 @@
import Modal from "react-modal";
import { useEffect, useState } from "react";
import { useAuthContext } from "../context/authContext";
export default function TimerHeader({ onSelect }) {
const [friends, setFriends] = useState([]);
const customStyles = {
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 [selected, setSelected] = useState();
const logout = () => {
fetch("/api/auth/logout").then(() => setSignedIn(false));
};
useEffect(() => {
fetch("/api/auth/friends")
.then((r) => r.json())
.then((friends) => setFriends(friends));
}, []);
return (
<nav className="nav">
<div className="nav-left">
<div className="tabs">
<a
onClick={() => {
setSelected(undefined);
onSelect(undefined);
}}
className={selected ? "" : "active"}
>
all
</a>
{friends.map((friend) => (
<a
key={friend.id}
onClick={() => {
setSelected(friend.id);
onSelect({ friendId: friend.id });
<>
<Modal
isOpen={modalOpen}
onRequestClose={() => setModalOpen(false)}
style={customStyles}
>
<div id="createTimerModal">
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
}}
className={selected === friend.id ? "active" : ""}
>
{friend.name}
</a>
))}
<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>
</div>
<div className="nav-right">
<details className="dropdown">
<summary style={{ marginTop: "1rem" }} className="button outline">
{friendName}
</summary>
<a className="button outline text-error" onClick={logout}>
Logout
</Modal>
<nav className="nav">
<div className="nav-left">
<div className="tabs">
<a
onClick={() => {
onSelect(undefined);
}}
className={selected ? "" : "active"}
>
all
</a>
{friends.map((friend) => (
<a
key={friend.id}
onClick={() => {
onSelect({ friendId: friend.id });
}}
className={selected?.friendId == friend.id ? "active" : ""}
>
{friend.name}
</a>
))}
</div>
</div>
<div className="nav-right">
<a
onClick={() => setModalOpen(true)}
style={{ marginTop: "1rem" }}
className="button outline"
>
+
</a>
</details>
</div>
</nav>
<details className="dropdown">
<summary style={{ marginTop: "1rem" }} className="button outline">
{friendName}
</summary>
<a className="button outline text-error" onClick={logout}>
Logout
</a>
</details>
</div>
</nav>
</>
);
}

View File

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

View File

@ -42,6 +42,20 @@ export default function Timers() {
namespace: "/events/timers",
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(() => {
socket?.on("refreshed", (newTimer: TimerResponse) => {
@ -60,12 +74,7 @@ export default function Timers() {
return (
<div className="container">
<TimerHeader
onSelect={(selected: TimersFilter) => {
setEndpoint(makeEndpoint(selected));
setQuery(selected);
}}
/>
<TimerHeader friends={friends} selected={selected} onSelect={onSelect} />
{timers ? (
timers
.map((timer) => ({
@ -73,7 +82,9 @@ export default function Timers() {
start: new Date(timer.start),
}))
.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} />
))
) : (
<></>
)}