diff --git a/.env.prod b/.env.prod deleted file mode 100644 index 5b0e01e..0000000 --- a/.env.prod +++ /dev/null @@ -1,9 +0,0 @@ -NODE_ENV=production - -POSTGRES_USER=friends -POSTGRES_PASSWORD=password -POSTGRES_DB=friends -POSTGRES_HOSTNAME=friendsdbprod -POSTGRES_PORT=5432 - -DATABASE_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOSTNAME:$POSTGRES_PORT/$POSTGRES_DB diff --git a/.gitignore b/.gitignore index b548b7f..784d9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules # Keep environment variables out of version control .env .env.dev +.env.prod diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..8393aab --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +If people end up using this, more features to add: + ++ Update / delete timers, if referenced friend or created by + - move creation modal to another form component ++ Show history of past times refreshed - more details page. Part of this is + done already at `/api/timers/:id/refreshes`. + +And bad decisions to fix: + ++ friend-tabs should queried from a url path, not from this weird passed-down + on-select bullshit, then we can use ``s diff --git a/client/index.html b/client/index.html index dc4d2f6..96223d5 100644 --- a/client/index.html +++ b/client/index.html @@ -1,16 +1,14 @@ - - - - - - - Lizzy's Friends - - - -
- - + + + + + MAFAP - My Awesome Friends Are Predictable + + +
+ + diff --git a/client/src/components/timerCard.tsx b/client/src/components/timerCard.tsx index b58ca98..a65c9e5 100644 --- a/client/src/components/timerCard.tsx +++ b/client/src/components/timerCard.tsx @@ -27,38 +27,90 @@ const replaceReferencedFriendsInName = ( }); }; +const refreshTimer = (id: number) => + fetch(`/api/timers/${id}/refresh`, { + method: "POST", + }); + export type TimerCardProps = { timer: TimerResponse; onSelect: (select?: TimersFilter) => void; }; export default function TimerCard({ timer, onSelect }: TimerCardProps) { - const [since, setSince] = useState(ago(timer.start)); + const [since, setSince] = useState(""); useEffect(() => { + const start = new Date(timer.start); let updateTimersInterval: ReturnType; - const msTillNextSecond = 1000 - (timer.start.getTime() % 1000); + const msTillNextSecond = 1000 - (start.getTime() % 1000); + + setSince(ago(start)); setTimeout(() => { - updateTimersInterval = setInterval( - () => setSince(ago(timer.start)), - 1_000 - ); + updateTimersInterval = setInterval(() => setSince(ago(start)), 1_000); }, msTillNextSecond); return () => clearInterval(updateTimersInterval); - }, []); + }, [timer.start]); return ( -

- {since}{" "} - {replaceReferencedFriendsInName( - timer.name, - timer.referenced_friends, - onSelect - ).map((element: JSX.Element | string, i: number) => ( - {element} - ))} -

+
+
+
+

+ {since || "..."} +

+
+

+ {replaceReferencedFriendsInName( + timer.name, + timer.referenced_friends, + onSelect + ).map((element: JSX.Element | string, i: number) => ( + + {element} + + ))} +

+
+
+ +
+ {timer.timer_refreshes && timer.timer_refreshes.length ? ( + + + onSelect({ + friendId: timer.timer_refreshes[0].refreshed_by.id, + }) + } + > + {timer.timer_refreshes[0].refreshed_by.name} + {" "} + refreshed it last + + ) : ( + "has not yet been refreshed..." + )} +
+ +
+
); } diff --git a/client/src/components/timerHeader.tsx b/client/src/components/timerHeader.tsx index 358974f..08e2965 100644 --- a/client/src/components/timerHeader.tsx +++ b/client/src/components/timerHeader.tsx @@ -2,11 +2,9 @@ import Modal from "react-modal"; import { useEffect, useState } from "react"; import { Mention, MentionsInput } from "react-mentions"; import { useAuthContext } from "../context/authContext"; +import { Friend, TimersFilter, TimerResponse } from "../utils/types"; import mentionStyles from "../styles/mention"; import modalStyles from "../styles/modal"; -import { Friend, TimersFilter, TimerResponse } from "../utils/types"; - -Modal.setAppElement("#root"); export type TimerHeaderProps = { friends: Friend[]; @@ -62,49 +60,46 @@ export default function TimerHeader({ onRequestClose={() => setModalOpen(false)} style={modalStyles} > -
-
- +
+
-
- setNewTimerName(e.target.value)} - > - ({ - id: `@<${id}>`, - display: `@${name}`, - }))} - /> - - {errors.length ? ( - errors.map((error, i) => ( -
- {error} -
- )) - ) : ( - <> - )} - -
+

New Timer

+

+ Use @ and the autocomplete menu to reference a user +

+ + setModalOpen(false)} className="button outline"> + × + +
+
+
+ setNewTimerName(e.target.value)} + > + ({ + id: `@<${id}>`, + display: `@${name}`, + }))} + /> + + {errors.length ? ( + errors.map((error, i) => ( +
+ {error} +
+ )) + ) : ( + <> + )} + +
@@ -135,8 +130,8 @@ export default function TimerHeader({
setModalOpen(true)} style={{ marginTop: "1rem" }} + onClick={() => setModalOpen(true)} className="button outline" > + @@ -145,7 +140,7 @@ export default function TimerHeader({ {friendName} - + Logout diff --git a/client/src/context/authContext.tsx b/client/src/context/authContext.tsx index 54f89a0..86cbfd7 100644 --- a/client/src/context/authContext.tsx +++ b/client/src/context/authContext.tsx @@ -25,7 +25,7 @@ const AuthContext = createContext({ export const useAuthContext = () => useContext(AuthContext); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { - const [signedIn, setSignedIn] = useState(false); + const [signedIn, setSignedIn] = useState(); const [sessionOver, setSessionOver] = useState(new Date()); const [friendId, setFriendId] = useState(null); const [friendName, setFriendName] = useState(null); diff --git a/client/src/main.tsx b/client/src/main.tsx index 9238530..6086472 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import Modal from "react-modal"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { AuthProvider } from "./context/authContext"; @@ -27,6 +28,8 @@ const router = createBrowserRouter([ }, ]); +Modal.setAppElement("#root"); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/client/src/routes/login.tsx b/client/src/routes/login.tsx index 0a58290..7200123 100644 --- a/client/src/routes/login.tsx +++ b/client/src/routes/login.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { Navigate } from "react-router-dom"; import { useAuthContext } from "../context/authContext"; +import { Navigate } from "react-router-dom"; import { SignThisTokenResponse, TokenResponse } from "../utils/types"; import "../styles/login.css"; @@ -28,6 +28,7 @@ const submitSignedToken = async (signature: string): Promise => export default function Login() { const [token, setToken] = useState(""); const [errors, setErrors] = useState([]); + const [authFinished, setAuthFinished] = useState(false); const { signedIn, setSignedIn, setSessionOver, setFriendId, setFriendName } = useAuthContext(); @@ -58,6 +59,8 @@ export default function Login() { setFriendId(friend.id); setFriendName(friend.name); + setAuthFinished(true); + return; } @@ -66,10 +69,6 @@ export default function Login() { } }; - if (signedIn) { - return ; - } - if (!token) return (
@@ -87,38 +86,41 @@ export default function Login() { <> )} - +
); - return ( -
-
-
Please sign the following payload with your PGP key:
- {token} -
-
-