Fix timer service not selecting timers with referenced friends, a little frontend rework
This commit is contained in:
parent
a00fa5c194
commit
a6ced3f22f
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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: "/",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
3
client/src/styles/index.css
Normal file
3
client/src/styles/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
21
client/src/utils/ago.ts
Normal file
21
client/src/utils/ago.ts
Normal 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();
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user