diff --git a/client/components/header/controls.tsx b/client/components/header/controls.tsx index f621c806..ee763129 100644 --- a/client/components/header/controls.tsx +++ b/client/components/header/controls.tsx @@ -2,9 +2,9 @@ import React from 'react' import MoonIcon from '@geist-ui/icons/moon' import SunIcon from '@geist-ui/icons/sun' import { Select } from '@geist-ui/core' -import { ThemeProps } from '../../pages/_app' // import { useAllThemes, useTheme } from '@geist-ui/core' import styles from './header.module.css' +import { ThemeProps } from '@lib/types' const Controls = ({ changeTheme, theme }: ThemeProps) => { const switchThemes = (type: string | string[]) => { diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx index 65b9f5a1..6b401b4d 100644 --- a/client/components/new-post/index.tsx +++ b/client/components/new-post/index.tsx @@ -1,29 +1,56 @@ -import { Button, ButtonDropdown, useToasts } from '@geist-ui/core' +import { Button, ButtonDropdown, Input, Modal, Note, useModal, useToasts } from '@geist-ui/core' import { useRouter } from 'next/router'; import { useCallback, useState } from 'react' import generateUUID from '@lib/generate-uuid'; -import Document from '../document'; +import DocumentComponent from '../document'; import FileDropzone from './drag-and-drop'; import styles from './post.module.css' import Title from './title'; import Cookies from 'js-cookie' - -export type Document = { - title: string - content: string - id: string -} +import type { PostVisibility, Document as DocumentType } from '@lib/types'; +import PasswordModal from './password'; const Post = () => { const { setToast } = useToasts() - const router = useRouter(); const [title, setTitle] = useState() - const [docs, setDocs] = useState([{ + const [docs, setDocs] = useState([{ title: '', content: '', id: generateUUID() }]) + const [passwordModalVisible, setPasswordModalVisible] = useState(false) + const sendRequest = useCallback(async (url: string, data: { visibility?: PostVisibility, title?: string, files?: DocumentType[], password?: string, userId: string }) => { + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${Cookies.get('drift-token')}` + }, + body: JSON.stringify({ + title, + files: docs, + ...data, + }) + }) + + if (res.ok) { + const json = await res.json() + router.push(`/post/${json.id}`) + } else { + const json = await res.json() + setToast({ + text: json.message, + type: 'error' + }) + } + + }, [docs, router, setToast, title]) + + const closePasswordModel = () => { + setPasswordModalVisible(false) + setSubmitting(false) + } const [isSubmitting, setSubmitting] = useState(false) @@ -31,29 +58,30 @@ const Post = () => { setDocs(docs.filter((doc) => doc.id !== id)) } - const onSubmit = async (visibility: string) => { + const onSubmit = async (visibility: PostVisibility, password?: string) => { setSubmitting(true) - const response = await fetch('/server-api/posts/create', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${Cookies.get("drift-token")}` - }, - body: JSON.stringify({ - title, - files: docs, - visibility, - userId: Cookies.get("drift-userid"), - }) + + if (visibility === 'protected' && !password) { + setPasswordModalVisible(true) + return + } + + await sendRequest('/server-api/posts/create', { + title, + files: docs, + visibility, + password, + userId: Cookies.get('drift-userid') || '' }) - const json = await response.json() + + + setSubmitting(false) + } + + const onClosePasswordModal = () => { + setPasswordModalVisible(false) setSubmitting(false) - if (json.id) - router.push(`/post/${json.id}`) - else { - setToast({ text: json.error.message, type: "error" }) - } } const updateTitle = useCallback((title: string, id: string) => { @@ -64,7 +92,7 @@ const Post = () => { setDocs(docs.map((doc) => doc.id === id ? { ...doc, content } : doc)) }, [docs]) - const uploadDocs = useCallback((files: Document[]) => { + const uploadDocs = useCallback((files: DocumentType[]) => { // if no title is set and the only document is empty, const isFirstDocEmpty = docs.length === 1 && docs[0].title === '' && docs[0].content === '' const shouldSetTitle = !title && isFirstDocEmpty @@ -87,7 +115,7 @@ const Post = () => { { docs.map(({ content, id, title }) => { return ( - remove(id)} key={id} editable={true} @@ -120,10 +148,12 @@ const Post = () => { onSubmit('private')}>Create Private onSubmit('public')} >Create Public onSubmit('unlisted')} >Create Unlisted + onSubmit('protected')} >Create with Password + onSubmit('protected', password)} /> ) } -export default Post \ No newline at end of file +export default Post diff --git a/client/components/new-post/password/index.tsx b/client/components/new-post/password/index.tsx new file mode 100644 index 00000000..2305e945 --- /dev/null +++ b/client/components/new-post/password/index.tsx @@ -0,0 +1,50 @@ +import { Input, Modal, Note, Spacer } from "@geist-ui/core" +import { useState } from "react" + +type Props = { + isOpen: boolean + onClose: () => void + onSubmit: (password: string) => void +} + +const PasswordModal = ({ isOpen, onClose, onSubmit: onSubmitAfterVerify }: Props) => { + const [password, setPassword] = useState() + const [confirmPassword, setConfirmPassword] = useState() + const [error, setError] = useState() + + const onSubmit = () => { + if (!password || !confirmPassword) { + setError('Please enter a password') + return + } + + if (password !== confirmPassword) { + setError("Passwords do not match") + return + } + + onSubmitAfterVerify(password) + } + + return (<> + { + Enter a password + + {!error && + This doesn't protect your post from the server administrator. + } + {error && + {error} + } + + setPassword(e.target.value)} /> + setConfirmPassword(e.target.value)} /> + + Cancel + Submit + } + ) +} + + +export default PasswordModal \ No newline at end of file diff --git a/client/components/visibility-badge/index.tsx b/client/components/visibility-badge/index.tsx index 0e9fa908..fa0dfb22 100644 --- a/client/components/visibility-badge/index.tsx +++ b/client/components/visibility-badge/index.tsx @@ -1,6 +1,5 @@ import { Badge } from "@geist-ui/core" - -type Visibility = "unlisted" | "private" | "public" +import { Visibility } from "@lib/types" type Props = { visibility: Visibility diff --git a/client/lib/types.d.ts b/client/lib/types.d.ts new file mode 100644 index 00000000..c10b748a --- /dev/null +++ b/client/lib/types.d.ts @@ -0,0 +1,12 @@ +export type PostVisibility = "unlisted" | "private" | "public" | "protected" + +export type ThemeProps = { + theme: "light" | "dark" | string, + changeTheme: () => void +} + +export type Document = { + title: string + content: string + id: string +} diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 91888263..f0292219 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -7,11 +7,7 @@ import useSharedState from '@lib/hooks/use-shared-state'; import 'react-loading-skeleton/dist/skeleton.css' import { SkeletonTheme } from 'react-loading-skeleton'; import Head from 'next/head'; - -export type ThemeProps = { - theme: "light" | "dark" | string, - changeTheme: () => void -} +import { ThemeProps } from '@lib/types'; type AppProps

= { pageProps: P; diff --git a/client/pages/index.tsx b/client/pages/index.tsx index 771d175b..c8303d83 100644 --- a/client/pages/index.tsx +++ b/client/pages/index.tsx @@ -2,11 +2,11 @@ import styles from '@styles/Home.module.css' import { Page, Spacer, Text } from '@geist-ui/core' import Header from '@components/header' -import { ThemeProps } from './_app' import Document from '@components/document' import Image from 'next/image' import ShiftBy from '@components/shift-by' import PageSeo from '@components/page-seo' +import { ThemeProps } from '@lib/types' export function getStaticProps() { const introDoc = process.env.WELCOME_CONTENT diff --git a/client/pages/new.tsx b/client/pages/new.tsx index b8966fc6..7a6f5948 100644 --- a/client/pages/new.tsx +++ b/client/pages/new.tsx @@ -3,12 +3,10 @@ import NewPost from '@components/new-post' import { Page } from '@geist-ui/core' import useSignedIn from '@lib/hooks/use-signed-in' import Header from '@components/header' -import { ThemeProps } from './_app' -import { useRouter } from 'next/router' import PageSeo from '@components/page-seo' +import { ThemeProps } from '@lib/types' const New = ({ theme, changeTheme }: ThemeProps) => { - const router = useRouter() const isSignedIn = useSignedIn() return ( diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx index 0bd32738..b4f6da19 100644 --- a/client/pages/post/[id].tsx +++ b/client/pages/post/[id].tsx @@ -6,10 +6,10 @@ import { useEffect, useState } from "react"; import Document from '../../components/document' import Header from "../../components/header"; import VisibilityBadge from "../../components/visibility-badge"; -import { ThemeProps } from "../_app"; import PageSeo from "components/page-seo"; import styles from './styles.module.css'; import Cookies from "js-cookie"; +import { ThemeProps } from "@lib/types"; const Post = ({ theme, changeTheme }: ThemeProps) => { const [post, setPost] = useState() diff --git a/client/pages/signin.tsx b/client/pages/signin.tsx index defc7d5b..0b98aabc 100644 --- a/client/pages/signin.tsx +++ b/client/pages/signin.tsx @@ -2,7 +2,7 @@ import { Page } from "@geist-ui/core"; import PageSeo from "@components/page-seo"; import Auth from "@components/auth"; import Header from "@components/header"; -import { ThemeProps } from "./_app"; +import { ThemeProps } from "@lib/types"; const SignIn = ({ theme, changeTheme }: ThemeProps) => ( diff --git a/client/pages/signup.tsx b/client/pages/signup.tsx index 3628ef6d..9aa7b734 100644 --- a/client/pages/signup.tsx +++ b/client/pages/signup.tsx @@ -2,7 +2,7 @@ import { Page } from "@geist-ui/core"; import Auth from "@components/auth"; import Header from "@components/header"; import PageSeo from '@components/page-seo'; -import { ThemeProps } from "./_app"; +import { ThemeProps } from "@lib/types"; const SignUp = ({ theme, changeTheme }: ThemeProps) => ( diff --git a/server/lib/models/Post.ts b/server/lib/models/Post.ts index 7dab48dd..4bc6dc4b 100644 --- a/server/lib/models/Post.ts +++ b/server/lib/models/Post.ts @@ -48,6 +48,9 @@ export class Post extends Model { @Column visibility!: string; + @Column + password?: string; + @UpdatedAt @Column updatedAt!: Date; diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts index a528f310..6456458e 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -26,7 +26,6 @@ posts.post('/create', jwt, async (req, res, next) => { throw new Error("Please provide a visibility.") } - // Create the "post" object const newPost = new Post({ title: req.body.title, visibility: req.body.visibility, @@ -35,7 +34,6 @@ posts.post('/create', jwt, async (req, res, next) => { await newPost.save() await newPost.$add('users', req.body.userId); const newFiles = await Promise.all(req.body.files.map(async (file) => { - // Establish a "file" for each file in the request const newFile = new File({ title: file.title, content: file.content, diff --git a/server/src/server.ts b/server/src/server.ts index 2e96ea8d..134ad9af 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,7 +4,7 @@ import config from '../lib/config'; import { sequelize } from '../lib/sequelize'; (async () => { - await sequelize.sync(); + await sequelize.sync({ force: true }); createServer(app) .listen( config.port,