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 }) {
|
||||
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 }) {
|
||||
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 (
|
||||
<>
|
||||
<div>{friendName}</div>
|
||||
<nav className="nav">
|
||||
<div className="nav-left">
|
||||
<div className="tabs">
|
||||
<a
|
||||
onClick={() => {
|
||||
setSelected(undefined);
|
||||
onSelect(undefined);
|
||||
}}
|
||||
className={selected ? "" : "active"}
|
||||
>
|
||||
all
|
||||
</a>
|
||||
{friends.map((friend) => (
|
||||
<div key={friend.id}>
|
||||
<p>{friend.name}</p>
|
||||
</div>
|
||||
<a
|
||||
key={friend.id}
|
||||
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) => {
|
||||
const [data, setData] = useState<T>();
|
||||
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 = <T>(props: UseInitialDataProps) => {
|
||||
};
|
||||
}, [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 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: "/",
|
||||
|
@ -4,7 +4,7 @@ import { useAuthContext } from "../context/authContext";
|
||||
export default function ProtectedRoute({ children }) {
|
||||
const { signedIn } = useAuthContext();
|
||||
|
||||
// if (!signedIn) return <Navigate to="/login" />;
|
||||
if (!signedIn) return <Navigate to="/login" />;
|
||||
|
||||
return children;
|
||||
}
|
||||
|
@ -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<TimerResponse[]>({
|
||||
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 (
|
||||
<>
|
||||
<TimerHeader />
|
||||
{timers?.map((timer) => (
|
||||
<TimerCard timer={timer} key={timer.id} />
|
||||
))}
|
||||
</>
|
||||
<div className="container">
|
||||
<TimerHeader
|
||||
onSelect={(selected: TimersFilter) => {
|
||||
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;
|
||||
|
||||
@ValidateIf((rfd) => !rfd.id || rfd.name)
|
||||
@Type(() => Number)
|
||||
id: number;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -35,14 +35,15 @@ export class TimerService {
|
||||
return this.prismaService.timer.findMany({
|
||||
select: {
|
||||
...TimerService.TIMER_SELECT,
|
||||
referenced_friends: {
|
||||
...TimerService.INCLUDE_FRIENDS_SELECT,
|
||||
},
|
||||
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