diff --git a/publish-frontend/package-lock.json b/publish-frontend/package-lock.json index 1a0c9e12f..aab415783 100644 --- a/publish-frontend/package-lock.json +++ b/publish-frontend/package-lock.json @@ -25,6 +25,7 @@ "@tiptap/react": "^2.0.3", "@tiptap/starter-kit": "^2.0.3", "date-fns": "^2.30.0", + "formik": "^2.4.4", "framer-motion": "^10.13.1", "next": "13.4.19", "next-auth": "^4.22.1", @@ -4142,6 +4143,42 @@ "is-callable": "^1.1.3" } }, + "node_modules/formik": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.4.tgz", + "integrity": "sha512-MV99upag7fCC3JfsI60WcxhymwNZnJUcMcnGuoz6mDf78SUfBbKjmfcA9LzHx4lEmjzmOflhP7oqz+ZQv5eStg==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/formik/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/framer-motion": { "version": "10.16.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.2.tgz", @@ -5012,6 +5049,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, @@ -6476,6 +6523,11 @@ "version": "1.3.1", "license": "MIT" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tippy.js": { "version": "6.3.7", "license": "MIT", diff --git a/publish-frontend/package.json b/publish-frontend/package.json index e97cbbac5..4e87521be 100644 --- a/publish-frontend/package.json +++ b/publish-frontend/package.json @@ -28,6 +28,7 @@ "@tiptap/react": "^2.0.3", "@tiptap/starter-kit": "^2.0.3", "date-fns": "^2.30.0", + "formik": "^2.4.4", "framer-motion": "^10.13.1", "next": "13.4.19", "next-auth": "^4.22.1", diff --git a/publish-frontend/src/components/post-form.jsx b/publish-frontend/src/components/post-form.jsx index 85e16daf1..5b4c511d0 100644 --- a/publish-frontend/src/components/post-form.jsx +++ b/publish-frontend/src/components/post-form.jsx @@ -3,22 +3,42 @@ import Tiptap from '@/components/tiptap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEdit } from '@fortawesome/free-solid-svg-icons'; import slugify from 'slugify'; -import { Img } from '@chakra-ui/react'; +import { + Flex, + Img, + Box, + Button, + Text, + Select, + Input, + Spacer, + Stack, + FormControl, + FormErrorMessage +} from '@chakra-ui/react'; +import { Field, Form, Formik } from 'formik'; +import { createPost, updatePost } from '@/lib/posts'; -const PostForm = ({ tags, initialValues }) => { - // editing title +const PostForm = ({ tags, author, initialValues }) => { const [title, setTitle] = useState('this is the title'); const [isEditingTitle, setIsEditingTitle] = useState(false); const [clientTags, setClientTags] = useState([]); - - const [isFocused, setIsFocused] = useState(false); + const [clientTagsId, setClientTagsId] = useState([]); const [postUrl, setPostUrl] = useState(''); - const [featureImage, setFeatureImage] = useState(null); + const [featureImage, setFeatureImage] = useState(''); + const [content, setContent] = useState(initialValues?.attributes.body || ''); + + const [id, setPostId] = useState(null); useEffect(() => { + function removeTag(tag) { + const newTags = clientTags.filter(t => t !== tag); + setClientTags(newTags); + } + function createNewTags() { const tagContainer = document.getElementById('tag-container'); @@ -38,233 +58,284 @@ const PostForm = ({ tags, initialValues }) => { tagContainer.appendChild(tagElement); }); } - createNewTags(); }, [clientTags]); + useEffect(() => { + if (initialValues) { + const { title, body } = initialValues.attributes; + const { id } = initialValues; + + console.log(initialValues); + + setTitle(title); + setContent(body); + setPostId(id); + // setClientTags(); + } + }, [initialValues]); + function handleFileInputChange(event) { const file = event.target.files[0]; setFeatureImage(URL.createObjectURL(file)); } - function handleTitleChange() { - const newTitle = document.getElementById('title-input').value; - setTitle(newTitle); - } - - function removeTag(tag) { - const newTags = clientTags.filter(t => t !== tag); - setClientTags(newTags); - } - function addTag(event) { if (!clientTags.includes(event.target.value)) { const newTags = [...clientTags, event.target.value]; + + const newTagsInt = []; + newTags.forEach(tag => { + tags.forEach(t => { + if (tag === t.attributes.name) { + newTagsInt.push(t.id); + } + }); + }); + setClientTags(newTags); + setClientTagsId(newTagsInt); } } + function handleContentChange(content) { + setContent(content); + } + + const handleSubmit = async session => { + const token = session.user.jwt; + const data = { + data: { + title: title, + slug: slugify(postUrl != '' ? postUrl : title, { + lower: true, + specialChar: false + }), + body: content, + tags: clientTagsId, + author: [author], + locale: 'en' + } + }; + + try { + if (!id) { + const res = await createPost(JSON.stringify(data), token); + setPostId(res.data.id); + console.log('created'); + } else { + await updatePost(id, JSON.stringify(data), token); + console.log('updated'); + } + } catch (error) { + console.log(error); + } + }; + return ( -
-
- {!isFocused ? ( - - ) : ( -
-

Feature Image

-
- {featureImage ? ( - Feature Image - ) : ( - <> - - - - )} -
- {featureImage && ( - - )} -

Tags

-
- -

Authors

- -

Publish Date

-
- - -
-

Post URL

- - -
-
- -
- )} -
-
-
-
- {isEditingTitle ? ( + + + + + + freeCodeCamp.org + + + {!featureImage ? ( <> - - - + + {' '} ) : ( - + feature )} -
-
- - -
-
-
setIsFocused(true)}> - + + + {featureImage && ( + + )} + + Tags + + + + Publish Date + + + + + + Post Url + + + + + + + + {!isEditingTitle ? ( + <> + setIsEditingTitle(true)}> + {title} + + + + + + ) : ( + { + setTitle(values.title); + setIsEditingTitle(false); + actions.setSubmitting(false); + }} + > + {props => ( +
+ + + {({ field, form }) => ( + + + + {form.errors.title} + + + )} + + + +
+ )} +
+ )} + + + + +
+
+
-
-
+ + ); }; - -function ManagePosts() { - const [showDrafts, setShowDrafts] = useState(true); - const [showPinned, setShowPinned] = useState(true); - const [showPublished, setShowPublished] = useState(true); - - return ( - <> - - {showDrafts && ( -
- -
- )} - - - {showPinned && ( -
- -
- )} - - - {showPublished && ( -
- -
- )} - - - - ); -} - export default PostForm; diff --git a/publish-frontend/src/components/tiptap.jsx b/publish-frontend/src/components/tiptap.jsx index 531e53c84..231200efe 100644 --- a/publish-frontend/src/components/tiptap.jsx +++ b/publish-frontend/src/components/tiptap.jsx @@ -4,11 +4,11 @@ import { faHeader, faImage, faItalic, - faList, faListUl, faQuoteLeft, faStrikethrough } from '@fortawesome/free-solid-svg-icons'; +import { Box, Button } from '@chakra-ui/react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Prose } from '@nikolovlazar/chakra-ui-prose'; import Image from '@tiptap/extension-image'; @@ -27,93 +27,113 @@ function ToolBar({ editor }) { }, [editor]); return ( -
- - - - - +
- - - + 3 +
- - +
- -
+ + ); } -const Tiptap = ({ defaultValue }) => { +const Tiptap = ({ handleContentChange, defaultValue }) => { const editor = useEditor({ extensions: [ StarterKit.configure({ @@ -134,16 +154,16 @@ const Tiptap = ({ defaultValue }) => { inline: true }) ], - content: defaultValue - ? defaultValue - : `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla malesuada tortor nec purus viverra, ac laoreet nulla hendrerit. Proin ac vehicula lacus. Donec nulla diam, volutpat eu interdum non, pharetra non mi. In tempor nisi augue, vel volutpat lorem gravida id. Quisque sodales augue in aliquet lacinia. Phasellus interdum convallis orci, sollicitudin pharetra enim fringilla eu. Pellentesque suscipit laoreet ante ut luctus. Etiam sagittis massa id magna efficitur volutpat. Aenean id nulla ut tellus porttitor sagittis ac ut nunc. Fusce non velit vitae purus aliquam finibus convallis vitae justo. In pellentesque risus risus, vitae tincidunt augue iaculis eget. Morbi sed risus lobortis, euismod augue sit amet, lobortis sem. - - Nunc vitae enim mauris. Aliquam volutpat dignissim diam, at sodales neque rutrum at. Etiam vestibulum ut orci imperdiet interdum. Duis ut venenatis purus. Aenean ac ultrices sapien. Curabitur sed diam nulla. Nunc ultrices, nisi vitae facilisis dapibus, augue nisi feugiat nisl, id sodales quam libero a sapien. Aliquam dolor justo, gravida rutrum leo in, hendrerit pulvinar elit. Quisque laoreet diam arcu, vel congue quam ullamcorper non. Quisque elit elit, condimentum nec tristique efficitur, lacinia id magna. Donec nec nibh eu nulla vestibulum efficitur. In tempus condimentum tempor. Aliquam eu ligula sed libero aliquam facilisis. Phasellus porttitor accumsan risus dictum placerat. Aenean suscipit velit at odio imperdiet, quis sodales dui molestie.`, + content: defaultValue ? defaultValue : '', autofocus: true, editorProps: { attributes: { class: 'prose focus:outline-none' } + }, + onUpdate: ({ editor }) => { + console.log('update'); + handleContentChange(editor.getHTML()); } }); diff --git a/publish-frontend/src/lib/invite-user.js b/publish-frontend/src/lib/invite-user.js index b385e3328..e31d9f02a 100644 --- a/publish-frontend/src/lib/invite-user.js +++ b/publish-frontend/src/lib/invite-user.js @@ -23,5 +23,4 @@ export async function inviteUser(email, token) { console.error(`inviteUser Failed. email: ${email}, Error: `, error); throw new Error(`inviteUser Failed. email: ${email}, Error: ${error}`); } - } diff --git a/publish-frontend/src/lib/posts.js b/publish-frontend/src/lib/posts.js index f36df705b..5ac1550bb 100644 --- a/publish-frontend/src/lib/posts.js +++ b/publish-frontend/src/lib/posts.js @@ -68,15 +68,14 @@ export async function createPost(JSONdata, token) { // Tell the server we're sending JSON. headers: { 'Content-Type': 'application/json', + accept: 'application/json', Authorization: `Bearer ${token}` }, // Body of the request is the JSON data we created above. body: JSONdata }; - // Send the form data to our forms API and get a response. const res = await fetch(endpoint, options); - if (!res.ok) { throw new Error('createPost Failed'); } diff --git a/publish-frontend/src/pages/api/auth/[...nextauth].js b/publish-frontend/src/pages/api/auth/[...nextauth].js index 7d3296ceb..bd80807c6 100644 --- a/publish-frontend/src/pages/api/auth/[...nextauth].js +++ b/publish-frontend/src/pages/api/auth/[...nextauth].js @@ -92,7 +92,7 @@ export const authOptions = { // Add the role name to session JWT token.name = userData?.username || null; token.userRole = userData?.role?.name || null; - console.log('token.userRole:', token.userRole); + token.id = userData?.id || null; } } // The returned value will be encrypted, and it is stored in a cookie. @@ -105,6 +105,7 @@ export const authOptions = { // Decrypt the token in the cookie and return needed values session.user.jwt = token.jwt; // JWT token to access the Strapi API session.user.role = token.userRole; + session.user.id = token.id; return session; } }, diff --git a/publish-frontend/src/pages/index.js b/publish-frontend/src/pages/index.js index 2f8c39181..b3faadb3a 100644 --- a/publish-frontend/src/pages/index.js +++ b/publish-frontend/src/pages/index.js @@ -172,7 +172,7 @@ export default function IndexPage({ posts, users, tags }) { {posts.data.map(post => { const title = post.attributes.title; - const name = post.attributes.author.data.attributes.name; + const name = 'sem'; const tag = post.attributes.tags.data[0]?.attributes.name; const relativeUpdatedAt = intlFormatDistance( new Date(post.attributes.updatedAt), diff --git a/publish-frontend/src/pages/posts/[postId].js b/publish-frontend/src/pages/posts/[postId].js index 32b7e34f4..5fc35f108 100644 --- a/publish-frontend/src/pages/posts/[postId].js +++ b/publish-frontend/src/pages/posts/[postId].js @@ -1,10 +1,8 @@ import PostForm from '@/components/post-form'; -import { getPost, updatePost } from '@/lib/posts'; +import { getPost } from '@/lib/posts'; import { getTags } from '@/lib/tags'; import { authOptions } from '@/pages/api/auth/[...nextauth]'; import { getServerSession } from 'next-auth/next'; -import { useSession } from 'next-auth/react'; -import { useState } from 'react'; export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); @@ -12,65 +10,14 @@ export async function getServerSideProps(context) { const { data: tags } = await getTags(session.user.jwt); const { data: post } = await getPost(postId, session.user.jwt); return { - props: { tags, post } + props: { tags, post, author: session?.user?.id } }; } -export default function EditPostPage({ tags, post }) { - // Get auth data from the session - const { data: session } = useSession(); - // declare state variables - const [content, setContent] = useState(post.attributes.body); - - const handleContentChange = updatedContent => { - setContent(updatedContent); - }; - - // Handles the submit event on form submit. - const handleSubmit = async (event, session) => { - // Stop the form from submitting and refreshing the page. - event.preventDefault(); - - // Construct the data to be sent to the server - const data = { - // Need to nest in data object because Strapi expects so - data: { - title: event.target.title.value, - body: content, // Get the content from the state - slug: event.target.slug.value - } - }; - - // Send the data to the Strapi server in JSON format. - const JSONdata = JSON.stringify(data); - - // Bearer token for authentication - const token = session.user.jwt; - - // Sending request - try { - const result = await updatePost(post.id, JSONdata, token); - console.log('updatePost response: ', JSON.stringify(result)); - alert('Saved!'); - } catch (error) { - console.error('createPost Error:', error); - alert('Failed to save the post'); - } - }; - - // loading screen - if (!post) { - return

Loading...

; - } - +export default function EditPostPage({ tags, post, author }) { return ( <> - handleSubmit(event, session)} - initialValues={post} - onContentChange={handleContentChange} - /> + ); } diff --git a/publish-frontend/src/pages/posts/new.js b/publish-frontend/src/pages/posts/new.js index e2e512233..13d8ae09b 100644 --- a/publish-frontend/src/pages/posts/new.js +++ b/publish-frontend/src/pages/posts/new.js @@ -1,27 +1,25 @@ import React from 'react'; import PostForm from '@/components/post-form'; -import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import { getTags } from '@/lib/tags'; import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); - - // Fetch tags from API const { data: tags } = await getTags(session.user.jwt); - // Pass tags as props to the component return { props: { - tags: tags + tags: tags, + author: session.user.id } }; } -export default function NewPostPage({ tags }) { +export default function NewPostPage({ tags, author }) { return ( <> - {/* // We pass the event to the handleSubmit() function on submit. */} - + ); } diff --git a/publish-frontend/src/styles/globals.css b/publish-frontend/src/styles/globals.css index 32b410f32..51284c8cf 100644 --- a/publish-frontend/src/styles/globals.css +++ b/publish-frontend/src/styles/globals.css @@ -23,106 +23,16 @@ color: rgb(0, 0, 0, 0.8); } -h2 { - font-size: 20px; - font-weight: 600; -} - -ul { - list-style: none; -} - -li { - padding-left: 1rem; - margin: 0.5rem 0; -} - -hr { - background-color: silver; - margin: 1rem 0; -} - -html { - background-color: #E2E8F0; -} - .editor { padding: 1rem; margin: 1.5rem 0 0 5rem; overflow: hidden; } -.post-container { - width: 100%; - background-color: white; -} -.menu { - display: flex; - padding: 0.2rem; - margin: 1rem 0 0 0; - border: 1px solid silver; - border-radius: 0.25rem; -} - -.side-drawer { - background-color: white; - border-right: 1px solid silver; - overflow-y: scroll; - padding: 1rem; - height: 100vh; - min-width: 375px; - max-width: 375px; -} - -.header { - display: flex; - justify-content: space-between; - margin: 1rem; -} - -.title-pos { - width: 500px; - margin: 0 0 0 5rem; -} - -.title { - font-size: 1.5rem; - font-weight: 600; -} - -.title-input { - width: 80%; - font-size: 1.5rem; +h2 { + font-size: 20px; font-weight: 600; - border: none; - outline: none; -} - -.toolbar{ - display: flex; - padding: 0.2rem; - margin: 1rem 0 0 0; - border: 1px solid silver; - border-radius: 0.25rem; -} - -.toolbar button { - padding: 0.2rem; - margin: 0.1rem 0.2rem; -} - -.toolbar button:hover { - background-color: #e2e8f0; - border-radius: 0.25rem; -} - -.icon-margin span { - margin: 0 1rem 0 0.5rem; -} - -.icon-margin-left span { - margin: 0 0.5rem 0 1rem; } .vl { @@ -131,85 +41,6 @@ html { margin: 0 0.5rem; } - -.menu button:hover { - background-color: #e2e8f0; - border-radius: 0.25rem; -} - -.page { - display: flex; -} - -.submit-button { - background-color: #1d4ed8; - color: white; - border: 1px solid; - border-radius: 1rem; - padding: 0.5rem 1rem; -} - -.delete-button { - background-color: #e53e3e !important; -} - -.full-width-btn { - margin: 1rem 0.5rem 0 0; - width: 100%; -} - -.navigation { - border-bottom: 1px solid silver; - padding-bottom: 2rem; -} - -.preview-button { - text-decoration: underline; - margin-right: 1rem; -} - -.dropdown-button { - display: block; - border: 1px 0 0 1px solid silver; -} - -.feature-image { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 175px; - max-height: 175px; - border-radius: 0.5rem; - background-color: #f1f5f9; -} - -.feature-image img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 0.5rem; -} - - -.tag-selector, input[type="text"], input[type="date"], input[type="time"] { - width: 100%; - padding: 0.5rem; - max-height: 175px; - border: 1px solid #727886; - border-radius: 0.5rem; - background-color: #f1f5f9; -} - -.input-title { - margin-top: 1rem; -} - -.time-date-input { - display: flex; - justify-content: space-between; -} - .tag { display: inline-block; background-color: #22c55e;