diff --git a/client/components/Link.tsx b/client/components/Link.tsx index e96f89bb..2cbb65cd 100644 --- a/client/components/Link.tsx +++ b/client/components/Link.tsx @@ -1,12 +1,17 @@ import type { LinkProps } from "@geist-ui/core" import { Link as GeistLink } from "@geist-ui/core" -import { useRouter } from "next/router"; +import { useRouter } from "next/router" const Link = (props: LinkProps) => { - const { basePath } = useRouter(); - const propHrefWithoutLeadingSlash = props.href && props.href.startsWith("/") ? props.href.substring(1) : props.href; - const href = basePath ? `${basePath}/${propHrefWithoutLeadingSlash}` : props.href; - return + const { basePath } = useRouter() + const propHrefWithoutLeadingSlash = + props.href && props.href.startsWith("/") + ? props.href.substring(1) + : props.href + const href = basePath + ? `${basePath}/${propHrefWithoutLeadingSlash}` + : props.href + return } export default Link diff --git a/client/components/admin/index.tsx b/client/components/admin/index.tsx index 55158e09..be14adf1 100644 --- a/client/components/admin/index.tsx +++ b/client/components/admin/index.tsx @@ -1,100 +1,132 @@ -import { Text, Fieldset, Spacer, Link } from '@geist-ui/core' -import { Post, User } from '@lib/types' -import Cookies from 'js-cookie' -import { useEffect, useState } from 'react' -import useSWR from 'swr' -import styles from './admin.module.css' -import PostModal from './post-modal-link' +import { Text, Fieldset, Spacer, Link } from "@geist-ui/core" +import { Post, User } from "@lib/types" +import Cookies from "js-cookie" +import { useEffect, useState } from "react" +import useSWR from "swr" +import styles from "./admin.module.css" +import PostModal from "./post-modal-link" -export const adminFetcher = (url: string) => fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${Cookies.get('drift-token')}`, - } -}).then(res => res.json()) +export const adminFetcher = (url: string) => + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Cookies.get("drift-token")}` + } + }).then((res) => res.json()) const Admin = () => { - const { data: posts, error: postsError } = useSWR('/server-api/admin/posts', adminFetcher) - const { data: users, error: usersError } = useSWR('/server-api/admin/users', adminFetcher) - const [postSizes, setPostSizes] = useState<{ [key: string]: number }>({}) - const byteToMB = (bytes: number) => Math.round(bytes / 1024 / 1024 * 100) / 100 - useEffect(() => { - if (posts) { - // sum the sizes of each file per post - const sizes = posts.reduce((acc, post) => { - const size = post.files?.reduce((acc, file) => acc + file.html.length, 0) || 0 - return { ...acc, [post.id]: byteToMB(size) } - }, {}) - setPostSizes(sizes) - } - }, [posts]) + const { data: posts, error: postsError } = useSWR( + "/server-api/admin/posts", + adminFetcher + ) + const { data: users, error: usersError } = useSWR( + "/server-api/admin/users", + adminFetcher + ) + const [postSizes, setPostSizes] = useState<{ [key: string]: number }>({}) + const byteToMB = (bytes: number) => + Math.round((bytes / 1024 / 1024) * 100) / 100 + useEffect(() => { + if (posts) { + // sum the sizes of each file per post + const sizes = posts.reduce((acc, post) => { + const size = + post.files?.reduce((acc, file) => acc + file.html.length, 0) || 0 + return { ...acc, [post.id]: byteToMB(size) } + }, {}) + setPostSizes(sizes) + } + }, [posts]) - return ( -
- Administration -
- Users - {users && {users.length} users} - {!users && Loading...} - {usersError && An error occured} - {users && - - - - - - - - - - {users?.map(user => ( - - - - - - - ))} - -
UsernamePostsCreatedRole
{user.username}{user.posts?.length}{new Date(user.createdAt).toLocaleDateString()} {new Date(user.createdAt).toLocaleTimeString()}{user.role}
} - -
- -
- Posts - {posts && {posts.length} posts} - {!posts && Loading...} - {postsError && An error occured} - {posts && - - - - - - - - - - - {posts?.map((post) => ( - - - - - - - - ))} - -
TitleVisibilityCreatedAuthorSize
{post.visibility}{new Date(post.createdAt).toLocaleDateString()} {new Date(post.createdAt).toLocaleTimeString()}{post.users?.length ? post.users[0].username : Deleted}{postSizes[post.id] ? `${postSizes[post.id]} MB` : ''}
} - {Object.keys(postSizes).length &&
- Total size: {Object.values(postSizes).reduce((prev, curr) => prev + curr)} MB -
} -
- -
- ) + return ( +
+ Administration +
+ Users + {users && {users.length} users} + {!users && Loading...} + {usersError && An error occured} + {users && ( + + + + + + + + + + + {users?.map((user) => ( + + + + + + + ))} + +
UsernamePostsCreatedRole
{user.username}{user.posts?.length} + {new Date(user.createdAt).toLocaleDateString()}{" "} + {new Date(user.createdAt).toLocaleTimeString()} + {user.role}
+ )} +
+ +
+ Posts + {posts && {posts.length} posts} + {!posts && Loading...} + {postsError && An error occured} + {posts && ( + + + + + + + + + + + + {posts?.map((post) => ( + + + + + + + + ))} + +
TitleVisibilityCreatedAuthorSize
+ + {post.visibility} + {new Date(post.createdAt).toLocaleDateString()}{" "} + {new Date(post.createdAt).toLocaleTimeString()} + + {post.users?.length ? ( + post.users[0].username + ) : ( + Deleted + )} + + {postSizes[post.id] ? `${postSizes[post.id]} MB` : ""} +
+ )} + {Object.keys(postSizes).length && ( +
+ + Total size:{" "} + {Object.values(postSizes).reduce((prev, curr) => prev + curr)} MB + +
+ )} +
+
+ ) } -export default Admin \ No newline at end of file +export default Admin diff --git a/client/components/admin/post-modal-link.tsx b/client/components/admin/post-modal-link.tsx index 6e78b14a..4a8fcbbe 100644 --- a/client/components/admin/post-modal-link.tsx +++ b/client/components/admin/post-modal-link.tsx @@ -1,51 +1,57 @@ -import { Link, Modal, useModal } from "@geist-ui/core"; -import { Post } from "@lib/types"; -import Cookies from "js-cookie"; -import useSWR from "swr"; -import { adminFetcher } from "."; -import styles from './admin.module.css' +import { Link, Modal, useModal } from "@geist-ui/core" +import { Post } from "@lib/types" +import Cookies from "js-cookie" +import useSWR from "swr" +import { adminFetcher } from "." +import styles from "./admin.module.css" -const PostModal = ({ id }: { - id: string, -}) => { - const { visible, setVisible, bindings } = useModal() - const { data: post, error } = useSWR(`/server-api/admin/post/${id}`, adminFetcher) - if (error) return failed to load - if (!post) return loading... +const PostModal = ({ id }: { id: string }) => { + const { visible, setVisible, bindings } = useModal() + const { data: post, error } = useSWR( + `/server-api/admin/post/${id}`, + adminFetcher + ) + if (error) return failed to load + if (!post) return loading... - const deletePost = async () => { - await fetch(`/server-api/admin/post/${id}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${Cookies.get("drift-token")}`, - } - }) - setVisible(false) - } + const deletePost = async () => { + await fetch(`/server-api/admin/post/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Cookies.get("drift-token")}` + } + }) + setVisible(false) + } - return ( - <> - setVisible(true)}>{post.title} - - {post.title} - Click an item to expand - {post.files?.map((file) => ( -
- -
- {file.title} -
-
-
-
-
- ) - )} - Delete - setVisible(false)}>Close -
- ) + return ( + <> + setVisible(true)}> + {post.title} + + + {post.title} + Click an item to expand + {post.files?.map((file) => ( +
+ +
+ {file.title} +
+
+
+
+ ))} + + Delete + + setVisible(false)}> + Close + +
+ + ) } -export default PostModal \ No newline at end of file +export default PostModal diff --git a/client/components/app/index.tsx b/client/components/app/index.tsx index cb6686a1..ab0a85a2 100644 --- a/client/components/app/index.tsx +++ b/client/components/app/index.tsx @@ -4,60 +4,63 @@ import type { NextComponentType, NextPageContext } from "next" import { SkeletonTheme } from "react-loading-skeleton" const App = ({ - Component, - pageProps, + Component, + pageProps }: { - Component: NextComponentType - pageProps: any + Component: NextComponentType + pageProps: any }) => { - const skeletonBaseColor = 'var(--light-gray)' - const skeletonHighlightColor = 'var(--lighter-gray)' + const skeletonBaseColor = "var(--light-gray)" + const skeletonHighlightColor = "var(--lighter-gray)" - const customTheme = Themes.createFromLight( - { - type: "custom", - palette: { - background: 'var(--bg)', - foreground: 'var(--fg)', - accents_1: 'var(--lightest-gray)', - accents_2: 'var(--lighter-gray)', - accents_3: 'var(--light-gray)', - accents_4: 'var(--gray)', - accents_5: 'var(--darker-gray)', - accents_6: 'var(--darker-gray)', - accents_7: 'var(--darkest-gray)', - accents_8: 'var(--darkest-gray)', - border: 'var(--light-gray)', - warning: 'var(--warning)' - }, - expressiveness: { - dropdownBoxShadow: '0 0 0 1px var(--light-gray)', - shadowSmall: '0 0 0 1px var(--light-gray)', - shadowLarge: '0 0 0 1px var(--light-gray)', - shadowMedium: '0 0 0 1px var(--light-gray)', - }, - layout: { - gap: 'var(--gap)', - gapHalf: 'var(--gap-half)', - gapQuarter: 'var(--gap-quarter)', - gapNegative: 'var(--gap-negative)', - gapHalfNegative: 'var(--gap-half-negative)', - gapQuarterNegative: 'var(--gap-quarter-negative)', - radius: 'var(--radius)', - }, - font: { - mono: 'var(--font-mono)', - sans: 'var(--font-sans)', - } - } - ) - return ( - - -
- - - ) + const customTheme = Themes.createFromLight({ + type: "custom", + palette: { + background: "var(--bg)", + foreground: "var(--fg)", + accents_1: "var(--lightest-gray)", + accents_2: "var(--lighter-gray)", + accents_3: "var(--light-gray)", + accents_4: "var(--gray)", + accents_5: "var(--darker-gray)", + accents_6: "var(--darker-gray)", + accents_7: "var(--darkest-gray)", + accents_8: "var(--darkest-gray)", + border: "var(--light-gray)", + warning: "var(--warning)" + }, + expressiveness: { + dropdownBoxShadow: "0 0 0 1px var(--light-gray)", + shadowSmall: "0 0 0 1px var(--light-gray)", + shadowLarge: "0 0 0 1px var(--light-gray)", + shadowMedium: "0 0 0 1px var(--light-gray)" + }, + layout: { + gap: "var(--gap)", + gapHalf: "var(--gap-half)", + gapQuarter: "var(--gap-quarter)", + gapNegative: "var(--gap-negative)", + gapHalfNegative: "var(--gap-half-negative)", + gapQuarterNegative: "var(--gap-quarter-negative)", + radius: "var(--radius)" + }, + font: { + mono: "var(--font-mono)", + sans: "var(--font-sans)" + } + }) + return ( + + + +
+ + + + ) } -export default App \ No newline at end of file +export default App diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx index 32017904..5838bfe2 100644 --- a/client/components/auth/index.tsx +++ b/client/components/auth/index.tsx @@ -1,131 +1,154 @@ -import { FormEvent, useEffect, useState } from 'react' -import { Button, Input, Text, Note } from '@geist-ui/core' -import styles from './auth.module.css' -import { useRouter } from 'next/router' -import Link from '../Link' -import Cookies from "js-cookie"; -import useSignedIn from '@lib/hooks/use-signed-in' +import { FormEvent, useEffect, useState } from "react" +import { Button, Input, Text, Note } from "@geist-ui/core" +import styles from "./auth.module.css" +import { useRouter } from "next/router" +import Link from "../Link" +import Cookies from "js-cookie" +import useSignedIn from "@lib/hooks/use-signed-in" -const NO_EMPTY_SPACE_REGEX = /^\S*$/; -const ERROR_MESSAGE = "Provide a non empty username and a password with at least 6 characters"; +const NO_EMPTY_SPACE_REGEX = /^\S*$/ +const ERROR_MESSAGE = + "Provide a non empty username and a password with at least 6 characters" const Auth = ({ page }: { page: "signup" | "signin" }) => { - const router = useRouter(); + const router = useRouter() - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [serverPassword, setServerPassword] = useState(''); - const [errorMsg, setErrorMsg] = useState(''); - const [requiresServerPassword, setRequiresServerPassword] = useState(false); - const signingIn = page === 'signin' - const { signin } = useSignedIn(); - useEffect(() => { - async function fetchRequiresPass() { - if (!signingIn) { - const resp = await fetch("/server-api/auth/requires-passcode", { - method: "GET", - }) - if (resp.ok) { - const res = await resp.json() - setRequiresServerPassword(res.requiresPasscode) - } else { - setErrorMsg("Something went wrong. Is the server running?") - } - } - } - fetchRequiresPass() - }, [page, signingIn]) + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [serverPassword, setServerPassword] = useState("") + const [errorMsg, setErrorMsg] = useState("") + const [requiresServerPassword, setRequiresServerPassword] = useState(false) + const signingIn = page === "signin" + const { signin } = useSignedIn() + useEffect(() => { + async function fetchRequiresPass() { + if (!signingIn) { + const resp = await fetch("/server-api/auth/requires-passcode", { + method: "GET" + }) + if (resp.ok) { + const res = await resp.json() + setRequiresServerPassword(res.requiresPasscode) + } else { + setErrorMsg("Something went wrong. Is the server running?") + } + } + } + fetchRequiresPass() + }, [page, signingIn]) + const handleJson = (json: any) => { + signin(json.token) + Cookies.set("drift-userid", json.userId) - const handleJson = (json: any) => { - signin(json.token) - Cookies.set('drift-userid', json.userId); + router.push("/new") + } - router.push('/new') - } + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + if ( + !signingIn && + (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) + ) + return setErrorMsg(ERROR_MESSAGE) + if ( + !signingIn && + requiresServerPassword && + !NO_EMPTY_SPACE_REGEX.test(serverPassword) + ) + return setErrorMsg(ERROR_MESSAGE) + else setErrorMsg("") - const handleSubmit = async (e: FormEvent) => { - e.preventDefault() - if (!signingIn && (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6)) return setErrorMsg(ERROR_MESSAGE) - if (!signingIn && requiresServerPassword && !NO_EMPTY_SPACE_REGEX.test(serverPassword)) return setErrorMsg(ERROR_MESSAGE) - else setErrorMsg(''); + const reqOpts = { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ username, password, serverPassword }) + } - const reqOpts = { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ username, password, serverPassword }) - } + try { + const signUrl = signingIn + ? "/server-api/auth/signin" + : "/server-api/auth/signup" + const resp = await fetch(signUrl, reqOpts) + const json = await resp.json() + if (!resp.ok) throw new Error(json.error.message) - try { - const signUrl = signingIn ? '/server-api/auth/signin' : '/server-api/auth/signup'; - const resp = await fetch(signUrl, reqOpts); - const json = await resp.json(); - if (!resp.ok) throw new Error(json.error.message); + handleJson(json) + } catch (err: any) { + setErrorMsg(err.message ?? "Something went wrong") + } + } - handleJson(json) - } catch (err: any) { - setErrorMsg(err.message ?? "Something went wrong") - } - } + return ( +
+
+
+

{signingIn ? "Sign In" : "Sign Up"}

+
+
+
+ setUsername(event.target.value)} + placeholder="Username" + required + scale={4 / 3} + /> + setPassword(event.target.value)} + placeholder="Password" + required + scale={4 / 3} + /> + {requiresServerPassword && ( + setServerPassword(event.target.value)} + placeholder="Server Password" + required + scale={4 / 3} + /> + )} - return ( -
-
-
-

{signingIn ? 'Sign In' : 'Sign Up'}

-
- -
- setUsername(event.target.value)} - placeholder="Username" - required - scale={4 / 3} - /> - setPassword(event.target.value)} - placeholder="Password" - required - scale={4 / 3} - /> - {requiresServerPassword && setServerPassword(event.target.value)} - placeholder="Server Password" - required - scale={4 / 3} - />} - - -
-
- {signingIn ? ( - - Don't have an account?{" "} - Sign up - - ) : ( - - Already have an account?{" "} - Sign in - - )} -
- {errorMsg && {errorMsg}} - -
-
- ) + +
+
+ {signingIn ? ( + + Don't have an account?{" "} + + Sign up + + + ) : ( + + Already have an account?{" "} + + Sign in + + + )} +
+ {errorMsg && ( + + {errorMsg} + + )} + +
+
+ ) } -export default Auth \ No newline at end of file +export default Auth diff --git a/client/components/badges/created-ago-badge/index.tsx b/client/components/badges/created-ago-badge/index.tsx index d2f25457..9b84991b 100644 --- a/client/components/badges/created-ago-badge/index.tsx +++ b/client/components/badges/created-ago-badge/index.tsx @@ -1,22 +1,27 @@ -import { Badge, Tooltip } from "@geist-ui/core"; -import { timeAgo } from "@lib/time-ago"; -import { useMemo, useState, useEffect } from "react"; +import { Badge, Tooltip } from "@geist-ui/core" +import { timeAgo } from "@lib/time-ago" +import { useMemo, useState, useEffect } from "react" -const CreatedAgoBadge = ({ createdAt }: { - createdAt: string | Date; -}) => { - const createdDate = useMemo(() => new Date(createdAt), [createdAt]) - const [time, setTimeAgo] = useState(timeAgo(createdDate)) +const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => { + const createdDate = useMemo(() => new Date(createdAt), [createdAt]) + const [time, setTimeAgo] = useState(timeAgo(createdDate)) - useEffect(() => { - const interval = setInterval(() => { - setTimeAgo(timeAgo(createdDate)) - }, 1000) - return () => clearInterval(interval) - }, [createdDate]) + useEffect(() => { + const interval = setInterval(() => { + setTimeAgo(timeAgo(createdDate)) + }, 1000) + return () => clearInterval(interval) + }, [createdDate]) - const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}` - return ( Created {time}) + const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}` + return ( + + {" "} + + Created {time} + + + ) } export default CreatedAgoBadge diff --git a/client/components/badges/expiration-badge/index.tsx b/client/components/badges/expiration-badge/index.tsx index 7901d31c..9151d915 100644 --- a/client/components/badges/expiration-badge/index.tsx +++ b/client/components/badges/expiration-badge/index.tsx @@ -1,60 +1,66 @@ -import { Badge, Tooltip } from "@geist-ui/core"; -import { timeUntil } from "@lib/time-ago"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { Badge, Tooltip } from "@geist-ui/core" +import { timeUntil } from "@lib/time-ago" +import { useCallback, useEffect, useMemo, useState } from "react" const ExpirationBadge = ({ - postExpirationDate, - // onExpires -}: { - postExpirationDate: Date | string | null - onExpires?: () => void + postExpirationDate +}: // onExpires +{ + postExpirationDate: Date | string | null + onExpires?: () => void }) => { - const expirationDate = useMemo(() => postExpirationDate ? new Date(postExpirationDate) : null, [postExpirationDate]) - const [timeUntilString, setTimeUntil] = useState(expirationDate ? timeUntil(expirationDate) : null); + const expirationDate = useMemo( + () => (postExpirationDate ? new Date(postExpirationDate) : null), + [postExpirationDate] + ) + const [timeUntilString, setTimeUntil] = useState( + expirationDate ? timeUntil(expirationDate) : null + ) - useEffect(() => { - let interval: NodeJS.Timer | null = null; - if (expirationDate) { - interval = setInterval(() => { - if (expirationDate) { - setTimeUntil(timeUntil(expirationDate)) - } - }, 1000) - } + useEffect(() => { + let interval: NodeJS.Timer | null = null + if (expirationDate) { + interval = setInterval(() => { + if (expirationDate) { + setTimeUntil(timeUntil(expirationDate)) + } + }, 1000) + } - return () => { - if (interval) { - clearInterval(interval) - } - } - }, [expirationDate]) + return () => { + if (interval) { + clearInterval(interval) + } + } + }, [expirationDate]) - const isExpired = useMemo(() => { - return timeUntilString && timeUntilString === "in 0 seconds" - }, [timeUntilString]) + const isExpired = useMemo(() => { + return timeUntilString && timeUntilString === "in 0 seconds" + }, [timeUntilString]) - // useEffect(() => { - // // check if expired every - // if (isExpired) { - // if (onExpires) { - // onExpires(); - // } - // } - // }, [isExpired, onExpires]) + // useEffect(() => { + // // check if expired every + // if (isExpired) { + // if (onExpires) { + // onExpires(); + // } + // } + // }, [isExpired, onExpires]) - if (!expirationDate) { - return null; - } + if (!expirationDate) { + return null + } - return ( - - - {isExpired ? "Expired" : `Expires ${timeUntilString}`} - - - ) + return ( + + + {isExpired ? "Expired" : `Expires ${timeUntilString}`} + + + ) } -export default ExpirationBadge \ No newline at end of file +export default ExpirationBadge diff --git a/client/components/badges/visibility-badge/index.tsx b/client/components/badges/visibility-badge/index.tsx index b640b573..0385463f 100644 --- a/client/components/badges/visibility-badge/index.tsx +++ b/client/components/badges/visibility-badge/index.tsx @@ -2,22 +2,22 @@ import { Badge } from "@geist-ui/core" import type { PostVisibility } from "@lib/types" type Props = { - visibility: PostVisibility + visibility: PostVisibility } const VisibilityBadge = ({ visibility }: Props) => { - const getBadgeType = () => { - switch (visibility) { - case "public": - return "success" - case "private": - return "warning" - case "unlisted": - return "default" - } - } + const getBadgeType = () => { + switch (visibility) { + case "public": + return "success" + case "private": + return "warning" + case "unlisted": + return "default" + } + } - return ({visibility}) + return {visibility} } export default VisibilityBadge diff --git a/client/components/button-dropdown/index.tsx b/client/components/button-dropdown/index.tsx index 7000059b..635f972b 100644 --- a/client/components/button-dropdown/index.tsx +++ b/client/components/button-dropdown/index.tsx @@ -1,116 +1,116 @@ import Button from "@components/button" import React, { useCallback, useEffect } from "react" import { useState } from "react" -import styles from './dropdown.module.css' -import DownIcon from '@geist-ui/icons/arrowDown' +import styles from "./dropdown.module.css" +import DownIcon from "@geist-ui/icons/arrowDown" type Props = { - type?: "primary" | "secondary" - loading?: boolean - disabled?: boolean - className?: string - iconHeight?: number + type?: "primary" | "secondary" + loading?: boolean + disabled?: boolean + className?: string + iconHeight?: number } type Attrs = Omit, keyof Props> type ButtonDropdownProps = Props & Attrs -const ButtonDropdown: React.FC> = ({ - type, - className, - disabled, - loading, - iconHeight = 24, - ...props -}) => { - const [visible, setVisible] = useState(false) - const [dropdown, setDropdown] = useState(null) +const ButtonDropdown: React.FC< + React.PropsWithChildren +> = ({ type, className, disabled, loading, iconHeight = 24, ...props }) => { + const [visible, setVisible] = useState(false) + const [dropdown, setDropdown] = useState(null) - const onClick = (e: React.MouseEvent) => { - e.stopPropagation() - e.nativeEvent.stopImmediatePropagation() - setVisible(!visible) - } + const onClick = (e: React.MouseEvent) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + setVisible(!visible) + } - const onBlur = () => { - setVisible(false) - } + const onBlur = () => { + setVisible(false) + } - const onMouseDown = (e: React.MouseEvent) => { - e.stopPropagation() - e.nativeEvent.stopImmediatePropagation() - } + const onMouseDown = (e: React.MouseEvent) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + } - const onMouseUp = (e: React.MouseEvent) => { - e.stopPropagation() - e.nativeEvent.stopImmediatePropagation() - } + const onMouseUp = (e: React.MouseEvent) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + } - const onMouseLeave = (e: React.MouseEvent) => { - e.stopPropagation() - e.nativeEvent.stopImmediatePropagation() - setVisible(false) - } + const onMouseLeave = (e: React.MouseEvent) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + setVisible(false) + } - const onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - setVisible(false) - } - } + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + setVisible(false) + } + } - const onClickOutside = useCallback(() => (e: React.MouseEvent) => { - if (dropdown && !dropdown.contains(e.target as Node)) { - setVisible(false) - } - }, [dropdown]) + const onClickOutside = useCallback( + () => (e: React.MouseEvent) => { + if (dropdown && !dropdown.contains(e.target as Node)) { + setVisible(false) + } + }, + [dropdown] + ) - useEffect(() => { - if (visible) { - document.addEventListener("mousedown", onClickOutside) - } else { - document.removeEventListener("mousedown", onClickOutside) - } - - return () => { - document.removeEventListener("mousedown", onClickOutside) - } - }, [visible, onClickOutside]) - - if (!Array.isArray(props.children)) { - return null - } - - return ( -
-
- {props.children[0]} - -
- { - visible && ( -
-
- {props.children.slice(1)} - -
-
- ) - } -
- ) + useEffect(() => { + if (visible) { + document.addEventListener("mousedown", onClickOutside) + } else { + document.removeEventListener("mousedown", onClickOutside) + } + return () => { + document.removeEventListener("mousedown", onClickOutside) + } + }, [visible, onClickOutside]) + if (!Array.isArray(props.children)) { + return null + } + return ( +
+
+ {props.children[0]} + +
+ {visible && ( +
+
+ {props.children.slice(1)} +
+
+ )} +
+ ) } -export default ButtonDropdown \ No newline at end of file +export default ButtonDropdown diff --git a/client/components/button/index.tsx b/client/components/button/index.tsx index 0e85a79a..aa231458 100644 --- a/client/components/button/index.tsx +++ b/client/components/button/index.tsx @@ -1,28 +1,39 @@ -import styles from './button.module.css' -import { forwardRef, Ref } from 'react' +import styles from "./button.module.css" +import { forwardRef, Ref } from "react" type Props = React.HTMLProps & { - children: React.ReactNode - buttonType?: 'primary' | 'secondary' - className?: string - onClick?: (e: React.MouseEvent) => void + children: React.ReactNode + buttonType?: "primary" | "secondary" + className?: string + onClick?: (e: React.MouseEvent) => void } // eslint-disable-next-line react/display-name const Button = forwardRef( - ({ children, onClick, className, buttonType = 'primary', type = 'button', disabled = false, ...props }, ref) => { - return ( - - ) - } + ( + { + children, + onClick, + className, + buttonType = "primary", + type = "button", + disabled = false, + ...props + }, + ref + ) => { + return ( + + ) + } ) export default Button diff --git a/client/components/edit-document-list/index.tsx b/client/components/edit-document-list/index.tsx index 9822b916..04a7023a 100644 --- a/client/components/edit-document-list/index.tsx +++ b/client/components/edit-document-list/index.tsx @@ -2,34 +2,44 @@ import type { Document } from "@lib/types" import DocumentComponent from "@components/edit-document" import { ChangeEvent, memo, useCallback } from "react" -const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle, onPaste }: { - docs: Document[], - updateDocTitle: (i: number) => (title: string) => void - updateDocContent: (i: number) => (content: string) => void - removeDoc: (i: number) => () => void - onPaste: (e: any) => void +const DocumentList = ({ + docs, + removeDoc, + updateDocContent, + updateDocTitle, + onPaste +}: { + docs: Document[] + updateDocTitle: (i: number) => (title: string) => void + updateDocContent: (i: number) => (content: string) => void + removeDoc: (i: number) => () => void + onPaste: (e: any) => void }) => { - const handleOnChange = useCallback((i) => (e: ChangeEvent) => { - updateDocContent(i)(e.target.value) - }, [updateDocContent]) + const handleOnChange = useCallback( + (i) => (e: ChangeEvent) => { + updateDocContent(i)(e.target.value) + }, + [updateDocContent] + ) - return (<>{ - docs.map(({ content, id, title }, i) => { - return ( - - ) - }) - } - ) + return ( + <> + {docs.map(({ content, id, title }, i) => { + return ( + + ) + })} + + ) } export default memo(DocumentList) diff --git a/client/components/edit-document/formatting-icons/index.tsx b/client/components/edit-document/formatting-icons/index.tsx index 5224466b..0988a98b 100644 --- a/client/components/edit-document/formatting-icons/index.tsx +++ b/client/components/edit-document/formatting-icons/index.tsx @@ -1,131 +1,148 @@ -import Bold from '@geist-ui/icons/bold' -import Italic from '@geist-ui/icons/italic' -import Link from '@geist-ui/icons/link' -import ImageIcon from '@geist-ui/icons/image' +import Bold from "@geist-ui/icons/bold" +import Italic from "@geist-ui/icons/italic" +import Link from "@geist-ui/icons/link" +import ImageIcon from "@geist-ui/icons/image" import { RefObject, useCallback, useMemo } from "react" -import styles from '../document.module.css' +import styles from "../document.module.css" import { Button, ButtonGroup } from "@geist-ui/core" // TODO: clean up -const FormattingIcons = ({ textareaRef, setText }: { textareaRef?: RefObject, setText?: (text: string) => void }) => { - // const { textBefore, textAfter, selectedText } = useMemo(() => { - // if (textareaRef && textareaRef.current) { - // const textarea = textareaRef.current - // const text = textareaRef.current.value - // const selectionStart = textarea.selectionStart - // const selectionEnd = textarea.selectionEnd - // const textBefore = text.substring(0, selectionStart) - // const textAfter = text.substring(selectionEnd) - // const selectedText = text.substring(selectionStart, selectionEnd) - // return { textBefore, textAfter, selectedText } - // } - // return { textBefore: '', textAfter: '' } - // }, [textareaRef,]) +const FormattingIcons = ({ + textareaRef, + setText +}: { + textareaRef?: RefObject + setText?: (text: string) => void +}) => { + // const { textBefore, textAfter, selectedText } = useMemo(() => { + // if (textareaRef && textareaRef.current) { + // const textarea = textareaRef.current + // const text = textareaRef.current.value + // const selectionStart = textarea.selectionStart + // const selectionEnd = textarea.selectionEnd + // const textBefore = text.substring(0, selectionStart) + // const textAfter = text.substring(selectionEnd) + // const selectedText = text.substring(selectionStart, selectionEnd) + // return { textBefore, textAfter, selectedText } + // } + // return { textBefore: '', textAfter: '' } + // }, [textareaRef,]) - const handleBoldClick = useCallback(() => { - if (textareaRef?.current && setText) { - const selectionStart = textareaRef.current.selectionStart - const selectionEnd = textareaRef.current.selectionEnd - const text = textareaRef.current.value - const before = text.substring(0, selectionStart) - const after = text.substring(selectionEnd) - const selectedText = text.substring(selectionStart, selectionEnd) + const handleBoldClick = useCallback(() => { + if (textareaRef?.current && setText) { + const selectionStart = textareaRef.current.selectionStart + const selectionEnd = textareaRef.current.selectionEnd + const text = textareaRef.current.value + const before = text.substring(0, selectionStart) + const after = text.substring(selectionEnd) + const selectedText = text.substring(selectionStart, selectionEnd) - const newText = `${before}**${selectedText}**${after}` - setText(newText) - } - }, [setText, textareaRef]) + const newText = `${before}**${selectedText}**${after}` + setText(newText) + } + }, [setText, textareaRef]) - const handleItalicClick = useCallback(() => { - if (textareaRef?.current && setText) { - const selectionStart = textareaRef.current.selectionStart - const selectionEnd = textareaRef.current.selectionEnd - const text = textareaRef.current.value - const before = text.substring(0, selectionStart) - const after = text.substring(selectionEnd) - const selectedText = text.substring(selectionStart, selectionEnd) - const newText = `${before}*${selectedText}*${after}` - setText(newText) - } - }, [setText, textareaRef]) + const handleItalicClick = useCallback(() => { + if (textareaRef?.current && setText) { + const selectionStart = textareaRef.current.selectionStart + const selectionEnd = textareaRef.current.selectionEnd + const text = textareaRef.current.value + const before = text.substring(0, selectionStart) + const after = text.substring(selectionEnd) + const selectedText = text.substring(selectionStart, selectionEnd) + const newText = `${before}*${selectedText}*${after}` + setText(newText) + } + }, [setText, textareaRef]) - const handleLinkClick = useCallback(() => { - if (textareaRef?.current && setText) { - const selectionStart = textareaRef.current.selectionStart - const selectionEnd = textareaRef.current.selectionEnd - const text = textareaRef.current.value - const before = text.substring(0, selectionStart) - const after = text.substring(selectionEnd) - const selectedText = text.substring(selectionStart, selectionEnd) - let formattedText = ''; - if (selectedText.includes('http')) { - formattedText = `[](${selectedText})` - } else { - formattedText = `[${selectedText}](https://)` - } - const newText = `${before}${formattedText}${after}` - setText(newText) - } - }, [setText, textareaRef]) + const handleLinkClick = useCallback(() => { + if (textareaRef?.current && setText) { + const selectionStart = textareaRef.current.selectionStart + const selectionEnd = textareaRef.current.selectionEnd + const text = textareaRef.current.value + const before = text.substring(0, selectionStart) + const after = text.substring(selectionEnd) + const selectedText = text.substring(selectionStart, selectionEnd) + let formattedText = "" + if (selectedText.includes("http")) { + formattedText = `[](${selectedText})` + } else { + formattedText = `[${selectedText}](https://)` + } + const newText = `${before}${formattedText}${after}` + setText(newText) + } + }, [setText, textareaRef]) - const handleImageClick = useCallback(() => { - if (textareaRef?.current && setText) { - const selectionStart = textareaRef.current.selectionStart - const selectionEnd = textareaRef.current.selectionEnd - const text = textareaRef.current.value - const before = text.substring(0, selectionStart) - const after = text.substring(selectionEnd) - const selectedText = text.substring(selectionStart, selectionEnd) - let formattedText = ''; - if (selectedText.includes('http')) { - formattedText = `![](${selectedText})` - } else { - formattedText = `![${selectedText}](https://)` - } - const newText = `${before}${formattedText}${after}` - setText(newText) - } - }, [setText, textareaRef]) + const handleImageClick = useCallback(() => { + if (textareaRef?.current && setText) { + const selectionStart = textareaRef.current.selectionStart + const selectionEnd = textareaRef.current.selectionEnd + const text = textareaRef.current.value + const before = text.substring(0, selectionStart) + const after = text.substring(selectionEnd) + const selectedText = text.substring(selectionStart, selectionEnd) + let formattedText = "" + if (selectedText.includes("http")) { + formattedText = `![](${selectedText})` + } else { + formattedText = `![${selectedText}](https://)` + } + const newText = `${before}${formattedText}${after}` + setText(newText) + } + }, [setText, textareaRef]) - const formattingActions = useMemo(() => [ - { - icon: , - name: 'bold', - action: handleBoldClick - }, - { - icon: , - name: 'italic', - action: handleItalicClick - }, - // { - // icon: , - // name: 'underline', - // action: handleUnderlineClick - // }, - { - icon: , - name: 'hyperlink', - action: handleLinkClick - }, - { - icon: , - name: 'image', - action: handleImageClick - } - ], [handleBoldClick, handleImageClick, handleItalicClick, handleLinkClick]) - - return ( -
- - {formattingActions.map(({ icon, name, action }) => ( -
- ) + const formattingActions = useMemo( + () => [ + { + icon: , + name: "bold", + action: handleBoldClick + }, + { + icon: , + name: "italic", + action: handleItalicClick + }, + // { + // icon: , + // name: 'underline', + // action: handleUnderlineClick + // }, + { + icon: , + name: "hyperlink", + action: handleLinkClick + }, + { + icon: , + name: "image", + action: handleImageClick + } + ], + [handleBoldClick, handleImageClick, handleItalicClick, handleLinkClick] + ) + return ( +
+ + {formattingActions.map(({ icon, name, action }) => ( +
+ ) } export default FormattingIcons diff --git a/client/components/edit-document/index.tsx b/client/components/edit-document/index.tsx index 1b18b336..2c1975c0 100644 --- a/client/components/edit-document/index.tsx +++ b/client/components/edit-document/index.tsx @@ -1,118 +1,172 @@ - - -import { ChangeEvent, memo, useCallback, useMemo, useRef, useState } from "react" -import styles from './document.module.css' -import Trash from '@geist-ui/icons/trash' +import { + ChangeEvent, + memo, + useCallback, + useMemo, + useRef, + useState +} from "react" +import styles from "./document.module.css" +import Trash from "@geist-ui/icons/trash" import FormattingIcons from "./formatting-icons" -import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core" +import { + Button, + ButtonGroup, + Card, + Input, + Spacer, + Tabs, + Textarea, + Tooltip +} from "@geist-ui/core" import Preview from "@components/preview" // import Link from "next/link" type Props = { - title?: string - content?: string - setTitle?: (title: string) => void - setContent?: (content: string) => void - handleOnContentChange?: (e: ChangeEvent) => void - initialTab?: "edit" | "preview" - remove?: () => void - onPaste?: (e: any) => void + title?: string + content?: string + setTitle?: (title: string) => void + setContent?: (content: string) => void + handleOnContentChange?: (e: ChangeEvent) => void + initialTab?: "edit" | "preview" + remove?: () => void + onPaste?: (e: any) => void } -const Document = ({ onPaste, remove, title, content, setTitle, setContent, initialTab = 'edit', handleOnContentChange }: Props) => { - const codeEditorRef = useRef(null) - const [tab, setTab] = useState(initialTab) - // const height = editable ? "500px" : '100%' - const height = "100%"; +const Document = ({ + onPaste, + remove, + title, + content, + setTitle, + setContent, + initialTab = "edit", + handleOnContentChange +}: Props) => { + const codeEditorRef = useRef(null) + const [tab, setTab] = useState(initialTab) + // const height = editable ? "500px" : '100%' + const height = "100%" - const handleTabChange = (newTab: string) => { - if (newTab === 'edit') { - codeEditorRef.current?.focus() - } - setTab(newTab as 'edit' | 'preview') - } + const handleTabChange = (newTab: string) => { + if (newTab === "edit") { + codeEditorRef.current?.focus() + } + setTab(newTab as "edit" | "preview") + } - const onTitleChange = useCallback((event: ChangeEvent) => setTitle ? setTitle(event.target.value) : null, [setTitle]) + const onTitleChange = useCallback( + (event: ChangeEvent) => + setTitle ? setTitle(event.target.value) : null, + [setTitle] + ) - const removeFile = useCallback((remove?: () => void) => { - if (remove) { - if (content && content.trim().length > 0) { - const confirmed = window.confirm("Are you sure you want to remove this file?") - if (confirmed) { - remove() - } - } else { - remove() - } - } - }, [content]) + const removeFile = useCallback( + (remove?: () => void) => { + if (remove) { + if (content && content.trim().length > 0) { + const confirmed = window.confirm( + "Are you sure you want to remove this file?" + ) + if (confirmed) { + remove() + } + } else { + remove() + } + } + }, + [content] + ) - // if (skeleton) { - // return <> - // - //
- //
- // - // {remove && } - //
- //
- //
- // - //
- //
- // - // } + // if (skeleton) { + // return <> + // + //
+ //
+ // + // {remove && } + //
+ //
+ //
+ // + //
+ //
+ // + // } - return ( - <> - -
-
- - {remove &&
-
- {tab === 'edit' && } - - - {/* */} -
- */} +
+ */} -
- */} +
+ */} -
- */} +
+