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",
|
"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",
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
×
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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} />
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user