diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..5b0e01e --- /dev/null +++ b/.env.prod @@ -0,0 +1,9 @@ +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/client/index.html b/client/index.html index 71799b5..dc4d2f6 100644 --- a/client/index.html +++ b/client/index.html @@ -1,17 +1,16 @@ - - - - + + + + - Simponic's Friends - + Lizzy's Friends + - -
- + +
+ + diff --git a/client/package-lock.json b/client/package-lock.json index 4738066..8f62b35 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -19,6 +19,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", + "@types/react-mentions": "^4.1.8", + "@types/react-modal": "^3.13.1", "@vitejs/plugin-react": "^3.1.0", "typescript": "^4.9.3", "vite": "^4.2.0" @@ -826,6 +828,24 @@ "@types/react": "*" } }, + "node_modules/@types/react-mentions": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/react-mentions/-/react-mentions-4.1.8.tgz", + "integrity": "sha512-Go86ozdnh0FTNbiGiDPAcNqYqtab9iGzLOgZPYUKrnhI4539jGzfJtP6rFHcXgi9Koe58yhkeyKYib6Ucul/sQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", diff --git a/client/package.json b/client/package.json index dd29d51..79f4e84 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", + "@types/react-mentions": "^4.1.8", + "@types/react-modal": "^3.13.1", "@vitejs/plugin-react": "^3.1.0", "typescript": "^4.9.3", "vite": "^4.2.0" diff --git a/client/public/people-hugging.svg b/client/public/people-hugging.svg new file mode 100644 index 0000000..03eaa07 --- /dev/null +++ b/client/public/people-hugging.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/vite.svg b/client/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/components/timerCard.tsx b/client/src/components/timerCard.tsx index 55fee4a..b58ca98 100644 --- a/client/src/components/timerCard.tsx +++ b/client/src/components/timerCard.tsx @@ -1,28 +1,42 @@ -import { ago } from "../utils/ago"; import { useEffect, useState } from "react"; +import { ago } from "../utils/ago"; +import { TimerResponse, Friend, TimersFilter } from "../utils/types"; -const replaceReferencedFriendsInName = (name, referencedFriends, onSelect) => { - const friendIdToFriend = referencedFriends.reduce((friendMap, friend) => { - friendMap[friend.id] = friend; - return friendMap; - }, {}); - return name.split(/(@\<\d+\>)/g).map((s) => { +const replaceReferencedFriendsInName = ( + name: string, + referencedFriends: Friend[], + onSelect: (select?: TimersFilter) => void +) => { + const friendIdToFriend = referencedFriends.reduce( + (friendMap: Record, friend) => { + friendMap[friend.id.toString()] = friend; + return friendMap; + }, + {} + ); + + return name.split(/(@\<\d+\>)/g).map((s: string) => { const matches = /@\<(\d+)\>/g.exec(s); if (matches) { const [_match, id] = matches; const name = friendIdToFriend[id].name; - return onSelect({ friendId: id })}>{name}; + return onSelect({ friendId: Number(id) })}>{name}; } return s; }); }; -export default function TimerCard({ timer, onSelect }) { - const [since, setSince] = useState(ago(timer.start)); +export type TimerCardProps = { + timer: TimerResponse; + onSelect: (select?: TimersFilter) => void; +}; + +export default function TimerCard({ timer, onSelect }: TimerCardProps) { + const [since, setSince] = useState(ago(timer.start)); useEffect(() => { - let updateTimersInterval; + let updateTimersInterval: ReturnType; const msTillNextSecond = 1000 - (timer.start.getTime() % 1000); setTimeout(() => { @@ -42,8 +56,8 @@ export default function TimerCard({ timer, onSelect }) { timer.name, timer.referenced_friends, onSelect - ).map((s, i) => ( - {s} + ).map((element: JSX.Element | string, i: number) => ( + {element} ))} ); diff --git a/client/src/components/timerHeader.tsx b/client/src/components/timerHeader.tsx index 5f4e679..358974f 100644 --- a/client/src/components/timerHeader.tsx +++ b/client/src/components/timerHeader.tsx @@ -4,20 +4,31 @@ import { Mention, MentionsInput } from "react-mentions"; import { useAuthContext } from "../context/authContext"; import mentionStyles from "../styles/mention"; import modalStyles from "../styles/modal"; +import { Friend, TimersFilter, TimerResponse } from "../utils/types"; Modal.setAppElement("#root"); -export default function TimerHeader({ friends, selected, onSelect }) { - const [modalOpen, setModalOpen] = useState(false); - const [newTimerName, setNewTimerName] = useState(""); - const [errors, setErrors] = useState([]); +export type TimerHeaderProps = { + friends: Friend[]; + selected?: TimersFilter; + onSelect: (selected?: TimersFilter) => void; +}; + +export default function TimerHeader({ + friends, + selected, + onSelect, +}: TimerHeaderProps) { + const [modalOpen, setModalOpen] = useState(false); + const [newTimerName, setNewTimerName] = useState(""); + const [errors, setErrors] = useState([]); const { friendName, setSignedIn } = useAuthContext(); const logout = () => { fetch("/api/auth/logout").then(() => setSignedIn(false)); }; - const createTimer = (e) => { + const createTimer = (e: any) => { e.preventDefault(); fetch("/api/timers", { @@ -33,7 +44,7 @@ export default function TimerHeader({ friends, selected, onSelect }) { }, }) .then((r) => r.json()) - .then((r) => { + .then((r: TimerResponse) => { if (r.message) { setErrors([r.message]); return; @@ -72,10 +83,11 @@ export default function TimerHeader({ friends, selected, onSelect }) { setNewTimerName(e.target.value)} + onChange={(e: any) => setNewTimerName(e.target.value)} > ({ + trigger="@" + data={friends.map(({ id, name }: Friend) => ({ id: `@<${id}>`, display: `@${name}`, }))} @@ -108,7 +120,7 @@ export default function TimerHeader({ friends, selected, onSelect }) { > all - {friends.map((friend) => ( + {friends.map((friend: Friend) => ( { diff --git a/client/src/context/authContext.tsx b/client/src/context/authContext.tsx index d5f0635..54f89a0 100644 --- a/client/src/context/authContext.tsx +++ b/client/src/context/authContext.tsx @@ -38,7 +38,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { useEffect(() => { if (friendId) { - localStorage.setItem("friendId", friendId); + localStorage.setItem("friendId", friendId.toString()); } }, [friendId]); diff --git a/client/src/hooks/useInitialData.ts b/client/src/hooks/useInitialData.ts index 815f289..bf601df 100644 --- a/client/src/hooks/useInitialData.ts +++ b/client/src/hooks/useInitialData.ts @@ -1,4 +1,4 @@ -import { io } from "socket.io-client"; +import { io, Socket } from "socket.io-client"; import { useState, useEffect } from "react"; export interface UseInitialDataProps { @@ -11,7 +11,7 @@ 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 [socket, setSocket] = useState(); const refreshData = () => fetch(endpoint) diff --git a/client/src/main.tsx b/client/src/main.tsx index da866e2..9238530 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -6,7 +6,7 @@ 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 ProtectedRoute from "./routes/protected"; import "chota"; import "./styles/index.css"; diff --git a/client/src/routes/login.tsx b/client/src/routes/login.tsx index c466196..0a58290 100644 --- a/client/src/routes/login.tsx +++ b/client/src/routes/login.tsx @@ -1,9 +1,12 @@ import { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; import { useAuthContext } from "../context/authContext"; +import { SignThisTokenResponse, TokenResponse } from "../utils/types"; import "../styles/login.css"; -const requestTokenSubmit = async (name) => +const requestTokenSubmit = async ( + name: string +): Promise => fetch( "/api/auth?" + new URLSearchParams({ @@ -11,7 +14,7 @@ const requestTokenSubmit = async (name) => }) ).then((r) => r.json()); -const submitSignedToken = async (signature) => +const submitSignedToken = async (signature: string): Promise => fetch("/api/auth", { method: "POST", body: JSON.stringify({ @@ -23,41 +26,43 @@ const submitSignedToken = async (signature) => }).then((r) => r.json()); export default function Login() { - const [token, setToken] = useState(""); - const [errors, setErrors] = useState([]); + const [token, setToken] = useState(""); + const [errors, setErrors] = useState([]); const { signedIn, setSignedIn, setSessionOver, setFriendId, setFriendName } = useAuthContext(); - const getTokenFormSubmission = async (e) => { + const getTokenFormSubmission = async (e: any) => { e.preventDefault(); const { error, message, token } = await requestTokenSubmit( e.target.name.value ); - if (error && message) { + if (message && error) { setErrors([message]); return; } - setErrors([]); - setToken(token); + if (token) { + setErrors([]); + setToken(token); + } }; - const signTokenFormSubmission = async (e) => { + const signTokenFormSubmission = async (e: any) => { e.preventDefault(); const { error, message, token, expiration, friend } = await submitSignedToken(e.target.signature.value); - if (token) { + if (token && expiration && friend) { setSignedIn(true); setSessionOver(new Date(expiration)); - setFriendId(friend.id.toString()); + setFriendId(friend.id); setFriendName(friend.name); return; } - if (error & message) { - setErrors([message]); + if (error && message) { + setErrors([message as string]); } }; @@ -97,7 +102,7 @@ export default function Login() {