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,
},
},
});