Fix timer service not selecting timers with referenced friends, a little frontend rework

This commit is contained in:
Elizabeth Hunt 2023-04-04 09:11:34 -06:00
parent a00fa5c194
commit a6ced3f22f
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
13 changed files with 140 additions and 126 deletions

View File

@ -1,3 +1,26 @@
import { ago } from "../utils/ago";
import { useEffect, useState } from "react";
export default function TimerCard({ timer }) { export default function TimerCard({ timer }) {
return <h1>{timer.name}</h1>; const [since, setSince] = useState(ago(timer.start));
useEffect(() => {
let updateTimersInterval;
const msTillNextSecond = 1000 - (timer.start.getTime() % 1000);
setTimeout(() => {
updateTimersInterval = setInterval(
() => setSince(ago(timer.start)),
1_000
);
}, msTillNextSecond);
return () => clearInterval(updateTimersInterval);
}, []);
return (
<h1>
<code>{since}</code> since {timer.name}
</h1>
);
} }

View File

@ -3,7 +3,12 @@ import { useAuthContext } from "../context/authContext";
export default function TimerHeader({ onSelect }) { export default function TimerHeader({ onSelect }) {
const [friends, setFriends] = useState([]); const [friends, setFriends] = useState([]);
const { friendName } = useAuthContext(); const { friendName, setSignedIn } = useAuthContext();
const [selected, setSelected] = useState();
const logout = () => {
fetch("/api/auth/logout").then(() => setSignedIn(false));
};
useEffect(() => { useEffect(() => {
fetch("/api/auth/friends") fetch("/api/auth/friends")
@ -12,13 +17,42 @@ export default function TimerHeader({ onSelect }) {
}, []); }, []);
return ( return (
<> <nav className="nav">
<div>{friendName}</div> <div className="nav-left">
<div className="tabs">
<a
onClick={() => {
setSelected(undefined);
onSelect(undefined);
}}
className={selected ? "" : "active"}
>
all
</a>
{friends.map((friend) => ( {friends.map((friend) => (
<div key={friend.id}> <a
<p>{friend.name}</p> key={friend.id}
</div> onClick={() => {
setSelected(friend.id);
onSelect({ friendId: friend.id });
}}
className={selected === friend.id ? "active" : ""}
>
{friend.name}
</a>
))} ))}
</> </div>
</div>
<div className="nav-right">
<details className="dropdown">
<summary className="button outline">{friendName}</summary>
<div className="card">
<a onClick={logout} className="text-error">
Logout
</a>
</div>
</details>
</div>
</nav>
); );
} }

View File

@ -10,10 +10,11 @@ export interface UseInitialDataProps {
export const useInitialData = <T>(props: UseInitialDataProps) => { export const useInitialData = <T>(props: UseInitialDataProps) => {
const [data, setData] = useState<T>(); const [data, setData] = useState<T>();
const [query, setQuery] = useState(props?.query); const [query, setQuery] = useState(props?.query);
const [endpoint, setEndpoint] = useState(props.initialDataEndpoint);
const [socket, setSocket] = useState(); const [socket, setSocket] = useState();
const refreshData = () => const refreshData = () =>
fetch(props.initialDataEndpoint) fetch(endpoint)
.then((r) => r.json()) .then((r) => r.json())
.then((r: T) => { .then((r: T) => {
setData(r); setData(r);
@ -35,5 +36,14 @@ export const useInitialData = <T>(props: UseInitialDataProps) => {
}; };
}, [query]); }, [query]);
return { data, refreshData, query, setQuery, socket, setData }; return {
data,
refreshData,
query,
setQuery,
socket,
setData,
setEndpoint,
endpoint,
};
}; };

View File

@ -1,69 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -1,15 +1,16 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "chota";
import { AuthProvider } from "./context/authContext"; import { AuthProvider } from "./context/authContext";
import NotFound from "./routes/notFound"; import NotFound from "./routes/notFound";
import Login from "./routes/login"; import Login from "./routes/login";
import Timers from "./routes/timers"; import Timers from "./routes/timers";
import ProtectedRoute from "./routes/protected.tsx"; import ProtectedRoute from "./routes/protected.tsx";
import "chota";
import "./styles/index.css";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",

View File

@ -4,7 +4,7 @@ import { useAuthContext } from "../context/authContext";
export default function ProtectedRoute({ children }) { export default function ProtectedRoute({ children }) {
const { signedIn } = useAuthContext(); const { signedIn } = useAuthContext();
// if (!signedIn) return <Navigate to="/login" />; if (!signedIn) return <Navigate to="/login" />;
return children; return children;
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import TimerCard from "../components/timerCard"; import TimerCard from "../components/timerCard";
import TimerHeader from "../components/timerHeader"; import TimerHeader from "../components/timerHeader";
import { useInitialData } from "../hooks/useInitialData"; import { useInitialData } from "../hooks/useInitialData";
@ -7,10 +8,6 @@ export type TimersFilter = {
friendId: undefined | number; // when undefined, get all friendId: undefined | number; // when undefined, get all
}; };
export type TimersProps = {
filter: TimersFilter;
};
export type Friend = { export type Friend = {
id: number; id: number;
name: number; name: number;
@ -24,26 +21,14 @@ export type TimerResponse = {
referenced_friends: Friend[]; referenced_friends: Friend[];
}; };
const putSince = (timers) => const makeEndpoint = (filter: TimersFilter) => {
timers.map((timer) => ({
...timer,
since: "10 seconds ago",
}));
const makeInitialDataEndpoint = (filter: TimersFilter) => {
let url = "/api/timers"; let url = "/api/timers";
if (filter && typeof filter.friendId !== "undefined") if (filter && typeof filter.friendId !== "undefined")
url += `?id=${filter.friendId}`; url += `/friend?id=${filter.friendId}`;
return url; return url;
}; };
const makeQuery = (filter: TimersFilter) => { export default function Timers() {
if (filter && typeof filter.friendId !== "undefined")
return { friend: filter.friendId };
return {};
};
export default function Timers({ filter }: TimersProps) {
const { const {
data: timers, data: timers,
refreshData: refreshTimers, refreshData: refreshTimers,
@ -51,10 +36,11 @@ export default function Timers({ filter }: TimersProps) {
query, query,
setQuery, setQuery,
socket, socket,
setEndpoint,
} = useInitialData<TimerResponse[]>({ } = useInitialData<TimerResponse[]>({
initialDataEndpoint: makeInitialDataEndpoint(filter), initialDataEndpoint: makeEndpoint({}),
namespace: "/events/timers", namespace: "/events/timers",
query: makeQuery(filter), query: {},
}); });
useEffect(() => { useEffect(() => {
@ -72,20 +58,25 @@ export default function Timers({ filter }: TimersProps) {
}); });
}, [socket]); }, [socket]);
useEffect(() => {
const updateTimersInterval = setInterval(() => {
setTimers((timers) => putSince(timers));
}, 1_000);
return () => clearInterval(updateTimersInterval);
}, []);
return ( return (
<> <div className="container">
<TimerHeader /> <TimerHeader
{timers?.map((timer) => ( onSelect={(selected: TimersFilter) => {
<TimerCard timer={timer} key={timer.id} /> setEndpoint(makeEndpoint(selected));
))} setQuery(selected);
</> }}
/>
{timers ? (
timers
.map((timer) => ({
...timer,
start: new Date(timer.start),
}))
.sort(({ start: startA }, { start: startB }) => startB - startA)
.map((timer) => <TimerCard timer={timer} key={timer.id} />)
) : (
<></>
)}
</div>
); );
} }

View File

@ -0,0 +1,3 @@
a {
cursor: pointer;
}

21
client/src/utils/ago.ts Normal file
View File

@ -0,0 +1,21 @@
// thanks, chatgpt
export function ago(date) {
const timeElapsed = Date.now() - date.getTime();
const days = Math.floor(timeElapsed / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(timeElapsed % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor((timeElapsed % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeElapsed % (1000 * 60)) / 1000);
let result = "";
if (days) {
result += `${days} day${days === 1 ? "" : "s"} `;
}
if (hours || minutes || seconds) {
result += `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}
return result.trim();
}

View File

@ -11,6 +11,7 @@ export class RetrieveFriendDTO {
name: string; name: string;
@ValidateIf((rfd) => !rfd.id || rfd.name) @ValidateIf((rfd) => !rfd.id || rfd.name)
@Type(() => Number)
id: number; id: number;
} }

View File

@ -11,7 +11,6 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.use(cookieParser()); app.use(cookieParser());
// TODO: Remove
app.enableCors(); app.enableCors();
// All WS connections must be auth'd // All WS connections must be auth'd

View File

@ -39,7 +39,6 @@ export class TimerController {
public async getFriendTimers(@Query() { id, name }: RetrieveFriendDTO) { public async getFriendTimers(@Query() { id, name }: RetrieveFriendDTO) {
const friend = await this.authService.findFriendByNameOrId(name, id); const friend = await this.authService.findFriendByNameOrId(name, id);
if (!friend) throw new NotFoundException('Friend not found by that query'); if (!friend) throw new NotFoundException('Friend not found by that query');
return await this.timerService.friendTimers(friend); return await this.timerService.friendTimers(friend);
} }

View File

@ -35,14 +35,15 @@ export class TimerService {
return this.prismaService.timer.findMany({ return this.prismaService.timer.findMany({
select: { select: {
...TimerService.TIMER_SELECT, ...TimerService.TIMER_SELECT,
referenced_friends: { ...TimerService.INCLUDE_FRIENDS_SELECT,
},
where: { where: {
id: friend.id, referenced_friends: {
some: {
id: {
equals: friend.id,
}, },
select: AuthService.FRIEND_SELECT,
}, },
created_by: {
select: AuthService.FRIEND_SELECT,
}, },
}, },
}); });