From a6ced3f22f44037b236048a0ac3a2179bcc0b40c Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Tue, 4 Apr 2023 09:11:34 -0600 Subject: [PATCH] Fix timer service not selecting timers with referenced friends, a little frontend rework --- client/src/components/timerCard.tsx | 25 +++++++++- client/src/components/timerHeader.tsx | 50 +++++++++++++++---- client/src/hooks/useInitialData.ts | 14 +++++- client/src/index.css | 69 --------------------------- client/src/main.tsx | 5 +- client/src/routes/protected.tsx | 2 +- client/src/routes/timers.tsx | 61 ++++++++++------------- client/src/styles/index.css | 3 ++ client/src/utils/ago.ts | 21 ++++++++ server/src/dto/dtos.ts | 1 + server/src/main.ts | 1 - server/src/timer/timer.controller.ts | 1 - server/src/timer/timer.service.ts | 13 ++--- 13 files changed, 140 insertions(+), 126 deletions(-) delete mode 100644 client/src/index.css create mode 100644 client/src/styles/index.css create mode 100644 client/src/utils/ago.ts diff --git a/client/src/components/timerCard.tsx b/client/src/components/timerCard.tsx index f37a67a..1afc1eb 100644 --- a/client/src/components/timerCard.tsx +++ b/client/src/components/timerCard.tsx @@ -1,3 +1,26 @@ +import { ago } from "../utils/ago"; +import { useEffect, useState } from "react"; + export default function TimerCard({ timer }) { - return

{timer.name}

; + 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 ( +

+ {since} since {timer.name} +

+ ); } diff --git a/client/src/components/timerHeader.tsx b/client/src/components/timerHeader.tsx index 618f607..16a1d6e 100644 --- a/client/src/components/timerHeader.tsx +++ b/client/src/components/timerHeader.tsx @@ -3,7 +3,12 @@ import { useAuthContext } from "../context/authContext"; export default function TimerHeader({ onSelect }) { 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(() => { fetch("/api/auth/friends") @@ -12,13 +17,42 @@ export default function TimerHeader({ onSelect }) { }, []); return ( - <> -
{friendName}
- {friends.map((friend) => ( -
-

{friend.name}

+ ); } diff --git a/client/src/hooks/useInitialData.ts b/client/src/hooks/useInitialData.ts index 1f5de20..815f289 100644 --- a/client/src/hooks/useInitialData.ts +++ b/client/src/hooks/useInitialData.ts @@ -10,10 +10,11 @@ export interface UseInitialDataProps { export const useInitialData = (props: UseInitialDataProps) => { const [data, setData] = useState(); const [query, setQuery] = useState(props?.query); + const [endpoint, setEndpoint] = useState(props.initialDataEndpoint); const [socket, setSocket] = useState(); const refreshData = () => - fetch(props.initialDataEndpoint) + fetch(endpoint) .then((r) => r.json()) .then((r: T) => { setData(r); @@ -35,5 +36,14 @@ export const useInitialData = (props: UseInitialDataProps) => { }; }, [query]); - return { data, refreshData, query, setQuery, socket, setData }; + return { + data, + refreshData, + query, + setQuery, + socket, + setData, + setEndpoint, + endpoint, + }; }; diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index 2c3fac6..0000000 --- a/client/src/index.css +++ /dev/null @@ -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; - } -} diff --git a/client/src/main.tsx b/client/src/main.tsx index 341b825..da866e2 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,15 +1,16 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import "chota"; import { AuthProvider } from "./context/authContext"; - import NotFound from "./routes/notFound"; import Login from "./routes/login"; import Timers from "./routes/timers"; import ProtectedRoute from "./routes/protected.tsx"; +import "chota"; +import "./styles/index.css"; + const router = createBrowserRouter([ { path: "/", diff --git a/client/src/routes/protected.tsx b/client/src/routes/protected.tsx index 4870b94..a0f4dbd 100644 --- a/client/src/routes/protected.tsx +++ b/client/src/routes/protected.tsx @@ -4,7 +4,7 @@ import { useAuthContext } from "../context/authContext"; export default function ProtectedRoute({ children }) { const { signedIn } = useAuthContext(); - // if (!signedIn) return ; + if (!signedIn) return ; return children; } diff --git a/client/src/routes/timers.tsx b/client/src/routes/timers.tsx index 2457de4..c561838 100644 --- a/client/src/routes/timers.tsx +++ b/client/src/routes/timers.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; + import TimerCard from "../components/timerCard"; import TimerHeader from "../components/timerHeader"; import { useInitialData } from "../hooks/useInitialData"; @@ -7,10 +8,6 @@ export type TimersFilter = { friendId: undefined | number; // when undefined, get all }; -export type TimersProps = { - filter: TimersFilter; -}; - export type Friend = { id: number; name: number; @@ -24,26 +21,14 @@ export type TimerResponse = { referenced_friends: Friend[]; }; -const putSince = (timers) => - timers.map((timer) => ({ - ...timer, - since: "10 seconds ago", - })); - -const makeInitialDataEndpoint = (filter: TimersFilter) => { +const makeEndpoint = (filter: TimersFilter) => { let url = "/api/timers"; if (filter && typeof filter.friendId !== "undefined") - url += `?id=${filter.friendId}`; + url += `/friend?id=${filter.friendId}`; return url; }; -const makeQuery = (filter: TimersFilter) => { - if (filter && typeof filter.friendId !== "undefined") - return { friend: filter.friendId }; - return {}; -}; - -export default function Timers({ filter }: TimersProps) { +export default function Timers() { const { data: timers, refreshData: refreshTimers, @@ -51,10 +36,11 @@ export default function Timers({ filter }: TimersProps) { query, setQuery, socket, + setEndpoint, } = useInitialData({ - initialDataEndpoint: makeInitialDataEndpoint(filter), + initialDataEndpoint: makeEndpoint({}), namespace: "/events/timers", - query: makeQuery(filter), + query: {}, }); useEffect(() => { @@ -72,20 +58,25 @@ export default function Timers({ filter }: TimersProps) { }); }, [socket]); - useEffect(() => { - const updateTimersInterval = setInterval(() => { - setTimers((timers) => putSince(timers)); - }, 1_000); - - return () => clearInterval(updateTimersInterval); - }, []); - return ( - <> - - {timers?.map((timer) => ( - - ))} - +
+ { + setEndpoint(makeEndpoint(selected)); + setQuery(selected); + }} + /> + {timers ? ( + timers + .map((timer) => ({ + ...timer, + start: new Date(timer.start), + })) + .sort(({ start: startA }, { start: startB }) => startB - startA) + .map((timer) => ) + ) : ( + <> + )} +
); } diff --git a/client/src/styles/index.css b/client/src/styles/index.css new file mode 100644 index 0000000..71af95b --- /dev/null +++ b/client/src/styles/index.css @@ -0,0 +1,3 @@ +a { + cursor: pointer; +} diff --git a/client/src/utils/ago.ts b/client/src/utils/ago.ts new file mode 100644 index 0000000..4620d3c --- /dev/null +++ b/client/src/utils/ago.ts @@ -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(); +} diff --git a/server/src/dto/dtos.ts b/server/src/dto/dtos.ts index 59fdd8b..90f21d7 100644 --- a/server/src/dto/dtos.ts +++ b/server/src/dto/dtos.ts @@ -11,6 +11,7 @@ export class RetrieveFriendDTO { name: string; @ValidateIf((rfd) => !rfd.id || rfd.name) + @Type(() => Number) id: number; } diff --git a/server/src/main.ts b/server/src/main.ts index ae21f73..ed1048c 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -11,7 +11,6 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); - // TODO: Remove app.enableCors(); // All WS connections must be auth'd diff --git a/server/src/timer/timer.controller.ts b/server/src/timer/timer.controller.ts index f6bcc42..ff277d6 100644 --- a/server/src/timer/timer.controller.ts +++ b/server/src/timer/timer.controller.ts @@ -39,7 +39,6 @@ export class TimerController { public async getFriendTimers(@Query() { id, name }: RetrieveFriendDTO) { const friend = await this.authService.findFriendByNameOrId(name, id); if (!friend) throw new NotFoundException('Friend not found by that query'); - return await this.timerService.friendTimers(friend); } diff --git a/server/src/timer/timer.service.ts b/server/src/timer/timer.service.ts index d885603..36e6350 100644 --- a/server/src/timer/timer.service.ts +++ b/server/src/timer/timer.service.ts @@ -35,14 +35,15 @@ export class TimerService { return this.prismaService.timer.findMany({ select: { ...TimerService.TIMER_SELECT, + ...TimerService.INCLUDE_FRIENDS_SELECT, + }, + where: { referenced_friends: { - where: { - id: friend.id, + some: { + id: { + equals: friend.id, + }, }, - select: AuthService.FRIEND_SELECT, - }, - created_by: { - select: AuthService.FRIEND_SELECT, }, }, });