begin work on protected posts
This commit is contained in:
parent
65b0c8f7f3
commit
3f0212c5c6
14 changed files with 136 additions and 50 deletions
|
@ -2,9 +2,9 @@ import React from 'react'
|
||||||
import MoonIcon from '@geist-ui/icons/moon'
|
import MoonIcon from '@geist-ui/icons/moon'
|
||||||
import SunIcon from '@geist-ui/icons/sun'
|
import SunIcon from '@geist-ui/icons/sun'
|
||||||
import { Select } from '@geist-ui/core'
|
import { Select } from '@geist-ui/core'
|
||||||
import { ThemeProps } from '../../pages/_app'
|
|
||||||
// import { useAllThemes, useTheme } from '@geist-ui/core'
|
// import { useAllThemes, useTheme } from '@geist-ui/core'
|
||||||
import styles from './header.module.css'
|
import styles from './header.module.css'
|
||||||
|
import { ThemeProps } from '@lib/types'
|
||||||
|
|
||||||
const Controls = ({ changeTheme, theme }: ThemeProps) => {
|
const Controls = ({ changeTheme, theme }: ThemeProps) => {
|
||||||
const switchThemes = (type: string | string[]) => {
|
const switchThemes = (type: string | string[]) => {
|
||||||
|
|
|
@ -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 { useRouter } from 'next/router';
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import generateUUID from '@lib/generate-uuid';
|
import generateUUID from '@lib/generate-uuid';
|
||||||
import Document from '../document';
|
import DocumentComponent from '../document';
|
||||||
import FileDropzone from './drag-and-drop';
|
import FileDropzone from './drag-and-drop';
|
||||||
import styles from './post.module.css'
|
import styles from './post.module.css'
|
||||||
import Title from './title';
|
import Title from './title';
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
|
import type { PostVisibility, Document as DocumentType } from '@lib/types';
|
||||||
export type Document = {
|
import PasswordModal from './password';
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Post = () => {
|
const Post = () => {
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [title, setTitle] = useState<string>()
|
const [title, setTitle] = useState<string>()
|
||||||
const [docs, setDocs] = useState<Document[]>([{
|
const [docs, setDocs] = useState<DocumentType[]>([{
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
id: generateUUID()
|
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)
|
const [isSubmitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
@ -31,29 +58,30 @@ const Post = () => {
|
||||||
setDocs(docs.filter((doc) => doc.id !== id))
|
setDocs(docs.filter((doc) => doc.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (visibility: string) => {
|
const onSubmit = async (visibility: PostVisibility, password?: string) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
const response = await fetch('/server-api/posts/create', {
|
|
||||||
method: 'POST',
|
if (visibility === 'protected' && !password) {
|
||||||
headers: {
|
setPasswordModalVisible(true)
|
||||||
'Content-Type': 'application/json',
|
return
|
||||||
'Authorization': `Bearer ${Cookies.get("drift-token")}`
|
}
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
await sendRequest('/server-api/posts/create', {
|
||||||
title,
|
title,
|
||||||
files: docs,
|
files: docs,
|
||||||
visibility,
|
visibility,
|
||||||
userId: Cookies.get("drift-userid"),
|
password,
|
||||||
})
|
userId: Cookies.get('drift-userid') || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
if (json.id)
|
|
||||||
router.push(`/post/${json.id}`)
|
|
||||||
else {
|
|
||||||
setToast({ text: json.error.message, type: "error" })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClosePasswordModal = () => {
|
||||||
|
setPasswordModalVisible(false)
|
||||||
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTitle = useCallback((title: string, id: string) => {
|
const updateTitle = useCallback((title: string, id: string) => {
|
||||||
|
@ -64,7 +92,7 @@ const Post = () => {
|
||||||
setDocs(docs.map((doc) => doc.id === id ? { ...doc, content } : doc))
|
setDocs(docs.map((doc) => doc.id === id ? { ...doc, content } : doc))
|
||||||
}, [docs])
|
}, [docs])
|
||||||
|
|
||||||
const uploadDocs = useCallback((files: Document[]) => {
|
const uploadDocs = useCallback((files: DocumentType[]) => {
|
||||||
// if no title is set and the only document is empty,
|
// if no title is set and the only document is empty,
|
||||||
const isFirstDocEmpty = docs.length === 1 && docs[0].title === '' && docs[0].content === ''
|
const isFirstDocEmpty = docs.length === 1 && docs[0].title === '' && docs[0].content === ''
|
||||||
const shouldSetTitle = !title && isFirstDocEmpty
|
const shouldSetTitle = !title && isFirstDocEmpty
|
||||||
|
@ -87,7 +115,7 @@ const Post = () => {
|
||||||
{
|
{
|
||||||
docs.map(({ content, id, title }) => {
|
docs.map(({ content, id, title }) => {
|
||||||
return (
|
return (
|
||||||
<Document
|
<DocumentComponent
|
||||||
remove={() => remove(id)}
|
remove={() => remove(id)}
|
||||||
key={id}
|
key={id}
|
||||||
editable={true}
|
editable={true}
|
||||||
|
@ -120,7 +148,9 @@ const Post = () => {
|
||||||
<ButtonDropdown.Item main onClick={() => onSubmit('private')}>Create Private</ButtonDropdown.Item>
|
<ButtonDropdown.Item main onClick={() => onSubmit('private')}>Create Private</ButtonDropdown.Item>
|
||||||
<ButtonDropdown.Item onClick={() => onSubmit('public')} >Create Public</ButtonDropdown.Item>
|
<ButtonDropdown.Item onClick={() => onSubmit('public')} >Create Public</ButtonDropdown.Item>
|
||||||
<ButtonDropdown.Item onClick={() => onSubmit('unlisted')} >Create Unlisted</ButtonDropdown.Item>
|
<ButtonDropdown.Item onClick={() => onSubmit('unlisted')} >Create Unlisted</ButtonDropdown.Item>
|
||||||
|
<ButtonDropdown.Item onClick={() => onSubmit('protected')} >Create with Password</ButtonDropdown.Item>
|
||||||
</ButtonDropdown>
|
</ButtonDropdown>
|
||||||
|
<PasswordModal isOpen={passwordModalVisible} onClose={onClosePasswordModal} onSubmit={(password) => onSubmit('protected', password)} />
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
|
|
50
client/components/new-post/password/index.tsx
Normal file
50
client/components/new-post/password/index.tsx
Normal file
|
@ -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<string>()
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState<string>()
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (!password || !confirmPassword) {
|
||||||
|
setError('Please enter a password')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitAfterVerify(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
{<Modal visible={isOpen} >
|
||||||
|
<Modal.Title>Enter a password</Modal.Title>
|
||||||
|
<Modal.Content>
|
||||||
|
{!error && <Note type="warning" label='Warning'>
|
||||||
|
This doesn't protect your post from the server administrator.
|
||||||
|
</Note>}
|
||||||
|
{error && <Note type="error" label='Error'>
|
||||||
|
{error}
|
||||||
|
</Note>}
|
||||||
|
<Spacer />
|
||||||
|
<Input width={"100%"} label="Password" marginBottom={1} htmlType="password" placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
|
||||||
|
<Input width={"100%"} label="Confirm" htmlType="password" placeholder="Confirm Password" onChange={(e) => setConfirmPassword(e.target.value)} />
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Action passive onClick={onClose}>Cancel</Modal.Action>
|
||||||
|
<Modal.Action onClick={onSubmit}>Submit</Modal.Action>
|
||||||
|
</Modal>}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default PasswordModal
|
|
@ -1,6 +1,5 @@
|
||||||
import { Badge } from "@geist-ui/core"
|
import { Badge } from "@geist-ui/core"
|
||||||
|
import { Visibility } from "@lib/types"
|
||||||
type Visibility = "unlisted" | "private" | "public"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
|
|
12
client/lib/types.d.ts
vendored
Normal file
12
client/lib/types.d.ts
vendored
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -7,11 +7,7 @@ import useSharedState from '@lib/hooks/use-shared-state';
|
||||||
import 'react-loading-skeleton/dist/skeleton.css'
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
import { SkeletonTheme } from 'react-loading-skeleton';
|
import { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { ThemeProps } from '@lib/types';
|
||||||
export type ThemeProps = {
|
|
||||||
theme: "light" | "dark" | string,
|
|
||||||
changeTheme: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppProps<P = any> = {
|
type AppProps<P = any> = {
|
||||||
pageProps: P;
|
pageProps: P;
|
||||||
|
|
|
@ -2,11 +2,11 @@ import styles from '@styles/Home.module.css'
|
||||||
import { Page, Spacer, Text } from '@geist-ui/core'
|
import { Page, Spacer, Text } from '@geist-ui/core'
|
||||||
|
|
||||||
import Header from '@components/header'
|
import Header from '@components/header'
|
||||||
import { ThemeProps } from './_app'
|
|
||||||
import Document from '@components/document'
|
import Document from '@components/document'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import ShiftBy from '@components/shift-by'
|
import ShiftBy from '@components/shift-by'
|
||||||
import PageSeo from '@components/page-seo'
|
import PageSeo from '@components/page-seo'
|
||||||
|
import { ThemeProps } from '@lib/types'
|
||||||
|
|
||||||
export function getStaticProps() {
|
export function getStaticProps() {
|
||||||
const introDoc = process.env.WELCOME_CONTENT
|
const introDoc = process.env.WELCOME_CONTENT
|
||||||
|
|
|
@ -3,12 +3,10 @@ import NewPost from '@components/new-post'
|
||||||
import { Page } from '@geist-ui/core'
|
import { Page } from '@geist-ui/core'
|
||||||
import useSignedIn from '@lib/hooks/use-signed-in'
|
import useSignedIn from '@lib/hooks/use-signed-in'
|
||||||
import Header from '@components/header'
|
import Header from '@components/header'
|
||||||
import { ThemeProps } from './_app'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import PageSeo from '@components/page-seo'
|
import PageSeo from '@components/page-seo'
|
||||||
|
import { ThemeProps } from '@lib/types'
|
||||||
|
|
||||||
const New = ({ theme, changeTheme }: ThemeProps) => {
|
const New = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
const router = useRouter()
|
|
||||||
const isSignedIn = useSignedIn()
|
const isSignedIn = useSignedIn()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,10 +6,10 @@ import { useEffect, useState } from "react";
|
||||||
import Document from '../../components/document'
|
import Document from '../../components/document'
|
||||||
import Header from "../../components/header";
|
import Header from "../../components/header";
|
||||||
import VisibilityBadge from "../../components/visibility-badge";
|
import VisibilityBadge from "../../components/visibility-badge";
|
||||||
import { ThemeProps } from "../_app";
|
|
||||||
import PageSeo from "components/page-seo";
|
import PageSeo from "components/page-seo";
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { ThemeProps } from "@lib/types";
|
||||||
|
|
||||||
const Post = ({ theme, changeTheme }: ThemeProps) => {
|
const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
const [post, setPost] = useState<any>()
|
const [post, setPost] = useState<any>()
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Page } from "@geist-ui/core";
|
||||||
import PageSeo from "@components/page-seo";
|
import PageSeo from "@components/page-seo";
|
||||||
import Auth from "@components/auth";
|
import Auth from "@components/auth";
|
||||||
import Header from "@components/header";
|
import Header from "@components/header";
|
||||||
import { ThemeProps } from "./_app";
|
import { ThemeProps } from "@lib/types";
|
||||||
|
|
||||||
const SignIn = ({ theme, changeTheme }: ThemeProps) => (
|
const SignIn = ({ theme, changeTheme }: ThemeProps) => (
|
||||||
<Page width={"100%"}>
|
<Page width={"100%"}>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Page } from "@geist-ui/core";
|
||||||
import Auth from "@components/auth";
|
import Auth from "@components/auth";
|
||||||
import Header from "@components/header";
|
import Header from "@components/header";
|
||||||
import PageSeo from '@components/page-seo';
|
import PageSeo from '@components/page-seo';
|
||||||
import { ThemeProps } from "./_app";
|
import { ThemeProps } from "@lib/types";
|
||||||
|
|
||||||
const SignUp = ({ theme, changeTheme }: ThemeProps) => (
|
const SignUp = ({ theme, changeTheme }: ThemeProps) => (
|
||||||
<Page width="100%">
|
<Page width="100%">
|
||||||
|
|
|
@ -48,6 +48,9 @@ export class Post extends Model {
|
||||||
@Column
|
@Column
|
||||||
visibility!: string;
|
visibility!: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
password?: string;
|
||||||
|
|
||||||
@UpdatedAt
|
@UpdatedAt
|
||||||
@Column
|
@Column
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
|
@ -26,7 +26,6 @@ posts.post('/create', jwt, async (req, res, next) => {
|
||||||
throw new Error("Please provide a visibility.")
|
throw new Error("Please provide a visibility.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the "post" object
|
|
||||||
const newPost = new Post({
|
const newPost = new Post({
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
visibility: req.body.visibility,
|
visibility: req.body.visibility,
|
||||||
|
@ -35,7 +34,6 @@ posts.post('/create', jwt, async (req, res, next) => {
|
||||||
await newPost.save()
|
await newPost.save()
|
||||||
await newPost.$add('users', req.body.userId);
|
await newPost.$add('users', req.body.userId);
|
||||||
const newFiles = await Promise.all(req.body.files.map(async (file) => {
|
const newFiles = await Promise.all(req.body.files.map(async (file) => {
|
||||||
// Establish a "file" for each file in the request
|
|
||||||
const newFile = new File({
|
const newFile = new File({
|
||||||
title: file.title,
|
title: file.title,
|
||||||
content: file.content,
|
content: file.content,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import config from '../lib/config';
|
||||||
import { sequelize } from '../lib/sequelize';
|
import { sequelize } from '../lib/sequelize';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await sequelize.sync();
|
await sequelize.sync({ force: true });
|
||||||
createServer(app)
|
createServer(app)
|
||||||
.listen(
|
.listen(
|
||||||
config.port,
|
config.port,
|
||||||
|
|
Loading…
Reference in a new issue