Link to friend in card
This commit is contained in:
parent
6a1a270c94
commit
ea279b7295
60
client/package-lock.json
generated
60
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
×
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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} />
|
||||
))
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user