diff --git a/src/components/post/CreateConfession.tsx b/src/components/post/CreateConfession.tsx index 80b2362..3a6c2ec 100644 --- a/src/components/post/CreateConfession.tsx +++ b/src/components/post/CreateConfession.tsx @@ -8,159 +8,141 @@ import { validatePost } from '~/utils/postUtil'; import { AuthContext } from '../contexts/AuthContext'; import { College } from '~/utils/CollegeData'; -import {TextEditor} from './TextEditor'; +import { TextEditor } from './TextEditor'; import { Paper } from '@mui/material'; import { SelectChangeEvent } from '@mui/material/Select'; import AutoCompleteWrapper from '~/layouts/textfield/AutoCompleteWrapper'; import LoadingBtn from '~/layouts/buttons/LoadingBtn'; -import MaxWidthModal from '~/layouts/modals/MaxWidthModal'; import TextBtn from '~/layouts/buttons/TextBtn'; import TagInput from './TagInput'; import { Tag } from '~/types/post'; -export default function CreateConfession(props: {isAdmin?: boolean}) { - const { dispatch } = useContext(AuthContext); - - const analytics = useAnalytics(); - const editorRef = useRef(null); - const [editorState, setEditorState] = useState(() => EditorState.createEmpty()); - const [infoModalOpen, setInfoModalOpen] = useState({ - open: false, - statusId: '', - }); - const [loading, setLoading] = useState(false); - const [post, setPost] = useState({ - collegeData: null, - confession: '', - tags: [] - }); - - const changeEditorState = (editorState: EditorState) => { - setEditorState(editorState); - const rawContent = convertToRaw(editorState.getCurrentContent()); - setPost({ - ...post, - confession: JSON.stringify(rawContent) - }); - } - - const handleTagChange = (event: SelectChangeEvent): void => { - const {target: { value }} = event; - setPost({ - ...post, - tags: typeof value === 'string' ? value.split(',') as Tag[] : value, +export default function CreateConfession(props: { isAdmin?: boolean }) { + const { dispatch } = useContext(AuthContext); + + const analytics = useAnalytics(); + const editorRef = useRef(null); + const [editorState, setEditorState] = useState(() => EditorState.createEmpty()); + const [loading, setLoading] = useState(false); + const [post, setPost] = useState({ + collegeData: null, + confession: '', + tags: [], + }); + + const changeEditorState = (editorState: EditorState) => { + setEditorState(editorState); + const rawContent = convertToRaw(editorState.getCurrentContent()); + setPost({ + ...post, + confession: JSON.stringify(rawContent), + }); + }; + + const handleTagChange = (event: SelectChangeEvent): void => { + const { + target: { value }, + } = event; + setPost({ + ...post, + tags: typeof value === 'string' ? (value.split(',') as Tag[]) : value, + }); + }; + + const createConfession = () => { + const errorMsg = validatePost(post.collegeData, post.confession); + + if (errorMsg) { + dispatch(snackBarDispatchMsg(errorMsg, 'error')); + return; + } + setLoading(true); + + if (props.isAdmin) { + createNewPost(post.collegeData as College, post.confession, post.tags, true).then((val) => { + setLoading(false); + clearFields(); + if (typeof val === 'string') { + alert('post created.'); + } }); - } - - const createConfession = () => { - const errorMsg = validatePost(post.collegeData, post.confession); - - if(errorMsg) { - dispatch(snackBarDispatchMsg(errorMsg, "error")); - return; - } - setLoading(true); - - if(props.isAdmin) { - createNewPost(post.collegeData as College, post.confession, post.tags, true).then(val => { - setLoading(false); - clearFields(); - if(typeof val === "string") { - alert("post created."); - } - }); - return; + return; + } + testModeration(post.confession).then((val) => { + if (val.isViolatingContent) { + let msg = val.message || 'Your post contains inappropriate content.'; + dispatch(snackBarDispatchMsg(msg, 'error')); + setLoading(false); + return; } - testModeration(post.confession).then(val => { - if(val.isViolatingContent) { - let msg = val.message || "Your post contains inappropriate content."; - dispatch(snackBarDispatchMsg(msg, "error")); - setLoading(false); - return; - } - - createNewPost(post.collegeData as College, post.confession, post.tags).then(val => { - if(typeof val === "string") return; - clearFields(); - - logEvent(analytics, "create_post", { - college: val.collegeData?.name - }); - - setInfoModalOpen({ - open: true, - statusId: val.statusId - }); - setLoading(false); - }); - }); - } - const clearFields = () => { - setPost({ - collegeData: null, - confession: '', - tags: [] + createNewPost(post.collegeData as College, post.confession, post.tags).then((val) => { + if (typeof val === 'string') return; + clearFields(); + + logEvent(analytics, 'create_post', { + college: val.collegeData?.name, + }); + + setLoading(false); + dispatch(snackBarDispatchMsg("Your confession is live now. Thank you for your contribution.", 'success')); }); - setEditorState(EditorState.createEmpty()); - } - - const setEditorFocus = () => { - editorRef.current?.focus(); - } - - return ( - - setPost({...post, collegeData: newValue})} - /> - Write your confession: -

- Don't use any profanity or hate speech. Such posts won't be approved. You can include references of the person you want to confess to.
- Use ctrl + b, ctrl + i, ctrl + u to bold, italicize, and underline text. -

-
- -
- - - -
- - -
- - setInfoModalOpen({open: false, statusId: ''})} - > - Thank you for submitting your confession! Your post is currently being reviewed by our team. -

Status ID: { infoModalOpen.statusId }

Please use this ID to check the status of your post. Please save it for future reference. -
-
- ) -} + }); + }; -type InfoModal = { - open: boolean; - statusId: string; + const clearFields = () => { + setPost({ + collegeData: null, + confession: '', + tags: [], + }); + setEditorState(EditorState.createEmpty()); + }; + + const setEditorFocus = () => { + editorRef.current?.focus(); + }; + + return ( + + setPost({ ...post, collegeData: newValue })} + /> + Write your confession: +

+ Don't use any profanity or hate speech. Such posts won't be approved. You can include references of the person + you want to confess to. +
+ Use ctrl + b, ctrl + i, ctrl + u to bold, italicize, and underline text. +

+
+ +
+ + + +
+ + +
+
+ ); } type MinPost = { - collegeData: College | null; - confession: string; - tags: Tag[]; -} + collegeData: College | null; + confession: string; + tags: Tag[]; +}; diff --git a/src/components/router/routes.tsx b/src/components/router/routes.tsx index bd47db4..a48ca76 100644 --- a/src/components/router/routes.tsx +++ b/src/components/router/routes.tsx @@ -37,9 +37,6 @@ const BaseRoutes: RouteObject[] = [{ path: '/post/:id', element: }, { - path: '/status', - element: -} ,{ path: '*', element: , }]; diff --git a/src/components/shared/NavBar.tsx b/src/components/shared/NavBar.tsx index 833d631..eb40ed2 100644 --- a/src/components/shared/NavBar.tsx +++ b/src/components/shared/NavBar.tsx @@ -7,70 +7,84 @@ import { useAuth } from '~/lib/firebase'; import { AuthContext } from '../contexts/AuthContext'; const getFormattedUserName = (name: string | null | undefined) => { - if(!name) return "User"; - const firstName = name.split(" ")[0]; - if(firstName.length > 10) return firstName.slice(0, 8) + ".."; - return firstName; -} + if (!name) return 'User'; + const firstName = name.split(' ')[0]; + if (firstName.length > 10) return firstName.slice(0, 8) + '..'; + return firstName; +}; export default function NavBar() { - const { user } = useContext(AuthContext); - - const logOutUser = () => { - const auth = useAuth(); - auth.signOut(); - redirect("/login"); - } - - return ( -
- -
- -
- company logo - confess-me -
- -
- -
-
- Check post status -
-
- { user ?

Hey, { getFormattedUserName(user?.displayName) }

- : - Login} -
-
+ const { user } = useContext(AuthContext); - - -
    + return ( +
    +
    + +
    + company logo + confess-me +
    + +
    -
  • Home
  • -
  • Check post status
  • - {user ? <> -
  • Profile
  • -
  • Logout
  • - : <> -
  • Login
  • - } -
+
+
+ {user ? ( +

Hey, {getFormattedUserName(user?.displayName)}

+ ) : ( + + Login + + )} +
+
+
+ + +
    +
  • + Home +
  • + {user ? ( + <> +
  • + + Profile + +
  • +
  • + Logout +
  • + + ) : ( + <> +
  • + + Login + +
  • + + )} +
+
- ) +
+ ); } diff --git a/src/utils/firebaseUtils/postUtil.ts b/src/utils/firebaseUtils/postUtil.ts index 6938022..62cadb4 100644 --- a/src/utils/firebaseUtils/postUtil.ts +++ b/src/utils/firebaseUtils/postUtil.ts @@ -1,206 +1,220 @@ -import { Post, PostWithStatus, Tag, TextModerationResult, TextModerationReturnType } from "~/types/post"; -import { - endBefore, - equalTo, - limitToLast, - onValue, - orderByChild, - push, - query, - ref, - runTransaction, - set} from "firebase/database"; -import { useDatabase } from "~/lib/firebase"; -import { generateEncodedStatusId, generateViolatingMessage } from "../postUtil"; -import { College } from "../CollegeData"; -import { approvePost } from "./adminUtil"; - +import { Post, PostWithStatus, Tag, TextModerationResult, TextModerationReturnType } from '~/types/post'; +import { + endBefore, + equalTo, + limitToLast, + onValue, + orderByChild, + push, + query, + ref, + runTransaction, + set, +} from 'firebase/database'; +import { useDatabase } from '~/lib/firebase'; +import { generateEncodedStatusId, generateViolatingMessage } from '../postUtil'; +import { College } from '../CollegeData'; export const createNewPost = async (collegeData: College, confession: string, tags: Tag[], isAdmin?: boolean) => { - const db = useDatabase(); - const newPostKey = push(ref(db, 'adminPosts')).key; - - const postToBeModerated: PostWithStatus = { - collegeData: collegeData, - confession: confession, - likesCount: 0, - id: newPostKey as string, - createdAt: Date.now().toString(), - statusId: isAdmin ? "" : generateEncodedStatusId(newPostKey as string), - status: "pending", - commentsCount: 0, - } - - if(tags) postToBeModerated.tags = tags; - if(isAdmin) { - postToBeModerated.isAdmin = true; - return approvePost(postToBeModerated); - } - - await new Promise(resolve => { - set(ref(db, `adminPosts/${newPostKey}`), postToBeModerated).then(() => { - resolve('success'); - }).catch((error) => { - console.error(error); + const db = useDatabase(); + const newPostKey = push(ref(db, 'posts')).key; + + const postToBeModerated: PostWithStatus = { + collegeData: collegeData, + confession: confession, + likesCount: 0, + id: newPostKey as string, + createdAt: Date.now().toString(), + statusId: isAdmin ? '' : generateEncodedStatusId(newPostKey as string), + status: 'approved', + commentsCount: 0, + }; + + if (tags) postToBeModerated.tags = tags; + if (isAdmin) postToBeModerated.isAdmin = true; + + await new Promise((resolve) => { + set(ref(db, `posts/${newPostKey}`), postToBeModerated) + .then(() => { + resolve('success'); + }) + .catch((error) => { + console.error(error); }); - }); + }); - return postToBeModerated; -} + return postToBeModerated; +}; // Get all the posts in order of most recent one export const getAllPosts = async () => { - const posts: Post[] = []; - const db = useDatabase(); - const postsRef = query(ref(db, 'posts'), orderByChild('createdAt')); - - await new Promise(resolve => { - onValue(postsRef, (snapshot) => { - snapshot.forEach(childSnapshot => { - const childData = childSnapshot.val(); - posts.push(childData); - }); - resolve(posts); - }, { onlyOnce: true }); - }); - - return posts.reverse(); -} + const posts: Post[] = []; + const db = useDatabase(); + const postsRef = query(ref(db, 'posts'), orderByChild('createdAt')); + + await new Promise((resolve) => { + onValue( + postsRef, + (snapshot) => { + snapshot.forEach((childSnapshot) => { + const childData = childSnapshot.val(); + posts.push(childData); + }); + resolve(posts); + }, + { onlyOnce: true } + ); + }); + + return posts.reverse(); +}; // Like a post export const togglePostLike = async (postId: string, userId: string) => { - const db = useDatabase(); - const postRef = ref(db, `posts/${postId}`); - - let result: Post | string = ""; - - await new Promise(resolve => { - runTransaction(postRef, (post) => { - if(post) { - if(post.likes && post.likes[userId]) { - post.likesCount--; - post.likes[userId] = null; - } else { - post.likesCount++; - if(!post.likes) { - post.likes = {}; - } - post.likes[userId] = true; - } - } - return post; - }).then(val => { - result = val.snapshot.val(); - resolve(result); - }).catch(err => { - result = "error"; - resolve(result); + const db = useDatabase(); + const postRef = ref(db, `posts/${postId}`); + + let result: Post | string = ''; + + await new Promise((resolve) => { + runTransaction(postRef, (post) => { + if (post) { + if (post.likes && post.likes[userId]) { + post.likesCount--; + post.likes[userId] = null; + } else { + post.likesCount++; + if (!post.likes) { + post.likes = {}; + } + post.likes[userId] = true; + } + } + return post; + }) + .then((val) => { + result = val.snapshot.val(); + resolve(result); + }) + .catch((err) => { + result = 'error'; + resolve(result); }); - }); + }); - return result; -} + return result; +}; // search posts by college name and order by likes count // if likes count is same then order by createdAt export const searchPosts = async (collegeName: string) => { - const posts: Post[] = []; - const db = useDatabase(); - const postsRef = query(ref(db, 'posts'), orderByChild('collegeData/collegeName'), equalTo(collegeName)); - - await new Promise(resolve => { - onValue(postsRef, (snapshot) => { - snapshot.forEach(childSnapshot => { - const childData = childSnapshot.val(); - posts.push(childData); - }); - resolve(posts); - }, { onlyOnce: true }); - }); - - return posts.reverse(); -} + const posts: Post[] = []; + const db = useDatabase(); + const postsRef = query(ref(db, 'posts'), orderByChild('collegeData/collegeName'), equalTo(collegeName)); + + await new Promise((resolve) => { + onValue( + postsRef, + (snapshot) => { + snapshot.forEach((childSnapshot) => { + const childData = childSnapshot.val(); + posts.push(childData); + }); + resolve(posts); + }, + { onlyOnce: true } + ); + }); + + return posts.reverse(); +}; export const reportPost = async (postId: string, userId: string) => { - const db = useDatabase(); - const postRef = ref(db, `posts/${postId}`); - - let result: string | null = null; - - await new Promise(resolve => { - runTransaction(postRef, (post) => { - if(post) { - if(post.reports && post.reports[userId]) { - result = "already_reported"; - } else { - if(!post.reportCounts) post.reportCounts = 0; - post.reportCounts++; - if(!post.reports) post.reports = {}; - post.reports[userId] = true; - result = "success"; - } - } - return post; - }).then(_ => { - resolve(result); - }).catch(_ => { - result = "error"; - resolve(result); + const db = useDatabase(); + const postRef = ref(db, `posts/${postId}`); + + let result: string | null = null; + + await new Promise((resolve) => { + runTransaction(postRef, (post) => { + if (post) { + if (post.reports && post.reports[userId]) { + result = 'already_reported'; + } else { + if (!post.reportCounts) post.reportCounts = 0; + post.reportCounts++; + if (!post.reports) post.reports = {}; + post.reports[userId] = true; + result = 'success'; + } + } + return post; + }) + .then((_) => { + resolve(result); + }) + .catch((_) => { + result = 'error'; + resolve(result); }); - }); + }); - return result; -} + return result; +}; // Run test moderation api to check if the post contains any inappropriate content export const testModeration = async (confession: string): Promise => { - if(import.meta.env.VITE_PROCESS_ENV === "development") return {isViolatingContent: false, message: ""}; - - const openAIAPIKey = import.meta.env.VITE_OPENAI_API_KEY; - const openAIUrl = "https://api.openai.com/v1/moderations"; - - const promise: TextModerationReturnType = await new Promise(resolve => { - fetch(openAIUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${openAIAPIKey}` - }, - body: JSON.stringify({input: confession}) - }) - .then(res => res.json()) - .then(data => { - const isViolatingContent = data.results[0].flagged; - if(isViolatingContent) { - const moderationResult: TextModerationResult = data.results[0].categories; - const message = generateViolatingMessage(moderationResult); - resolve({isViolatingContent, message}); - } else { - resolve({isViolatingContent, message: ""}); - } + if (import.meta.env.VITE_PROCESS_ENV === 'development') return { isViolatingContent: false, message: '' }; + + const openAIAPIKey = import.meta.env.VITE_OPENAI_API_KEY; + const openAIUrl = 'https://api.openai.com/v1/moderations'; + + const promise: TextModerationReturnType = await new Promise((resolve) => { + fetch(openAIUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openAIAPIKey}`, + }, + body: JSON.stringify({ input: confession }), + }) + .then((res) => res.json()) + .then((data) => { + const isViolatingContent = data.results[0].flagged; + if (isViolatingContent) { + const moderationResult: TextModerationResult = data.results[0].categories; + const message = generateViolatingMessage(moderationResult); + resolve({ isViolatingContent, message }); + } else { + resolve({ isViolatingContent, message: '' }); + } }) - .catch(err => { - console.log('error') - resolve({isViolatingContent: false, message: err.message, error: true}) + .catch((err) => { + console.log('error'); + resolve({ isViolatingContent: false, message: err.message, error: true }); }); - }); - - return promise; -} + }); + + return promise; +}; export const getPaginatedPosts = async (limit: number, lastKey: string): Promise => { - const posts: Post[] = []; - const db = useDatabase(); - const postsRef = query(ref(db, 'posts'), orderByChild('createdAt'), endBefore(lastKey), limitToLast(limit)); - - const result: Post[] = await new Promise(resolve => { - onValue(postsRef, (snapshot) => { - snapshot.forEach(childSnapshot => { - posts.push(childSnapshot.val()); - }); - resolve(posts); - }, { onlyOnce: true }); - }); - - return result.reverse(); -} \ No newline at end of file + const posts: Post[] = []; + const db = useDatabase(); + const postsRef = query(ref(db, 'posts'), orderByChild('createdAt'), endBefore(lastKey), limitToLast(limit)); + + const result: Post[] = await new Promise((resolve) => { + onValue( + postsRef, + (snapshot) => { + snapshot.forEach((childSnapshot) => { + posts.push(childSnapshot.val()); + }); + resolve(posts); + }, + { onlyOnce: true } + ); + }); + + return result.reverse(); +};