From 552dfd4a165eb77e6ca221326a8a3b81a56bcc7f Mon Sep 17 00:00:00 2001 From: zainuldeen <78583049+Zain-ul-din@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:06:57 +0500 Subject: [PATCH] past papers ##16 --- src/components/Ads/FrontPageAd.tsx | 25 +++- src/components/pastpaper/PastPaper.tsx | 122 ++++++++++++++++-- src/components/pastpaper/PastPapersToast.tsx | 67 ++++++++++ src/components/pastpaper/UploadModal.tsx | 123 +++++++++++++++---- src/components/pastpaper/pastpapers.tsx | 92 ++++++++++---- src/lib/constant.ts | 3 +- src/lib/pastpaper/crud.ts | 7 ++ src/lib/pastpaper/index.ts | 5 + src/lib/pastpaper/upload.ts | 38 +++++- src/pages/_app.tsx | 2 + src/pages/index.tsx | 2 +- src/pages/pastpaper/index.tsx | 16 +++ 12 files changed, 440 insertions(+), 62 deletions(-) create mode 100644 src/components/pastpaper/PastPapersToast.tsx diff --git a/src/components/Ads/FrontPageAd.tsx b/src/components/Ads/FrontPageAd.tsx index 273108d..03a6497 100644 --- a/src/components/Ads/FrontPageAd.tsx +++ b/src/components/Ads/FrontPageAd.tsx @@ -1,7 +1,9 @@ -import { Button, Flex, Heading, Stack, Text } from '@chakra-ui/react'; +import { Button, Center, Flex, Heading, Stack, Text } from '@chakra-ui/react'; import { motion } from 'framer-motion'; +import Link from 'next/link'; import router from 'next/router'; import useTyperEffect from '~/hooks/useTyperEffect'; +import { ROUTING } from '../../lib/constant'; // TODO: take data from props @@ -11,6 +13,27 @@ export default function FrontPageAd() { speed: 40 }); + return ( + <> +
+ + + +
+ + ); + return ( void; onUpVote: () => void; onDelete: () => void; + onEdit: () => void; + markAsSpam: () => void; }) { const [isUnder500] = useMediaQuery('(max-width: 500px)'); const [user] = useAuthState(firebase.firebaseAuth); @@ -44,11 +49,32 @@ export default function PastPaper({ useEffect(() => { if (!user) return setEdit(false); const isAuthorizedUser = user.uid === model.uploader_uid; - const isAdmin = user.uid === process.env.NEXT_PUBLIC_ADMIN_EMAIL; - setEdit(isAuthorizedUser || isAdmin); + const isAdmin = user.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL; + // check if can edit post any more + const upVotesCount = (model.up_votes || []).length; + const downVotesCount = (model.down_votes || []).length; + const totalVotes = upVotesCount + downVotesCount; + const downVotesPercentage = (downVotesCount / totalVotes) * 100; + const isEditLocked = upVotesCount > 5 && downVotesPercentage < 50; + const isEditByUser = !isEditLocked && isAuthorizedUser; + setEdit(isEditByUser || isAdmin); }, [user, model]); const { onOpen, isOpen, onClose } = useDisclosure(); + const [showImg, setShowImg] = useState(false); + + useEffect(() => { + if (showImg) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + } + + // Clean up on unmount + return () => { + document.body.style.overflow = 'auto'; + }; + }, [showImg]); return ( <> @@ -63,26 +89,90 @@ export default function PastPaper({ {model.subject_name} - - {model.exam_type} - + past_paper { + setShowImg(true); + }} style={{ objectFit: 'cover', borderRadius: '0.2rem', minWidth: '250px', maxWidth: '250px', maxHeight: '350px', - minHeight: '350px' + minHeight: '350px', + cursor: 'zoom-in', + filter: model.spam ? 'blur(5px)' : '' }} loading="lazy" /> + + + {model.exam_type} + + {model.spam && ( + + This post mark as spam by community see on our own risk. + {user && ( + <> + {user.uid === model.uploader_uid && ( + <> Consider uploading material with correct data. + )} + + )} + + )} + {showImg && ( + + + + past_paper { + setShowImg(true); + }} + style={{ + objectFit: 'contain', + borderRadius: '0.2rem' + }} + loading="lazy" + /> + + + )} + @@ -107,7 +197,7 @@ export default function PastPaper({ onUpVote(); }}> - {model.up_votes?.length || 0} + {model.up_votes ? model.up_votes.length : 0} @@ -136,7 +226,7 @@ export default function PastPaper({ {canEdit && ( - + diff --git a/src/components/pastpaper/PastPapersToast.tsx b/src/components/pastpaper/PastPapersToast.tsx new file mode 100644 index 0000000..699f074 --- /dev/null +++ b/src/components/pastpaper/PastPapersToast.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react'; +import PastPaper from './PastPaper'; +import { HStack, useToast, Text, Flex, Stack, Button, useDisclosure } from '@chakra-ui/react'; +import { CloseIcon, InfoIcon } from '@chakra-ui/icons'; +import OneTap from '../OneTap'; +import { BiLeftArrow, BiRightArrow, BiRightArrowAlt } from 'react-icons/bi'; +import { ROUTING } from '~/lib/constant'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function PastPaperToast() { + const toast = useToast(); + const [show, setShow] = useState(true); + const router = useRouter(); + const toastId = 'past-paper-toast'; + + useEffect(() => { + if (router.pathname === ROUTING.past_papers || !show) return; + setShow(false); + + toast({ + id: toastId, + title: '📃 Past Papers', + description: 'View Past Papers', + duration: 1000 * 30, + position: 'bottom-right', + size: 'sm', + colorScheme: 'gray', + render: () => { + return ( + + + + + + + 📃 Past Papers + + + Browse Past Papers + + + + + + + + ); + }, + isClosable: true + }); + }); + return <>; +} diff --git a/src/components/pastpaper/UploadModal.tsx b/src/components/pastpaper/UploadModal.tsx index 9dc1fb6..5d15430 100644 --- a/src/components/pastpaper/UploadModal.tsx +++ b/src/components/pastpaper/UploadModal.tsx @@ -25,7 +25,7 @@ import { Stack } from '@chakra-ui/react'; -import React, { useReducer, useRef, useState, useCallback } from 'react'; +import React, { useReducer, useRef, useState, useCallback, useEffect } from 'react'; import Image from 'next/image'; @@ -39,25 +39,22 @@ import { import Tesseract from 'tesseract.js'; import Loader from '../design/Loader'; -import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; -import { v4 as uuidv4 } from 'uuid'; -import { doc, serverTimestamp, setDoc } from 'firebase/firestore'; -import { User } from 'firebase/auth'; import useAllSubjects from '~/hooks/useAllSubjects'; import { FaCamera, FaImages } from 'react-icons/fa'; import Webcam from 'react-webcam'; import { dataURLtoFile } from '~/lib/util'; import { useUserCredentials } from '~/hooks/hooks'; -import upload from '~/lib/pastpaper/upload'; +import upload, { updatePastPaper } from '~/lib/pastpaper/upload'; +import { PastPaperDocType } from '~/lib/pastpaper/types'; const UploadModal = ({ isOpen, onClose, - staticData + defaultData }: { isOpen: boolean; onClose: () => void; - staticData: { [department: string]: Array }; + defaultData: PastPaperDocType | null; }) => { const [loading, setLoading] = useState({ validatingImage: false, @@ -79,6 +76,28 @@ const UploadModal = ({ err: undefined }); + useEffect(() => { + if (!defaultData) { + dispatch({ type: '', payload: '' } as any); + return; + } + + dispatch({ + type: InputActionKind.subject, + payload: defaultData.subject_name + }); + + dispatch({ + type: InputActionKind.examType, + payload: defaultData.exam_type + }); + + dispatch({ + type: InputActionKind.visibility, + payload: defaultData.visibility + }); + }, [defaultData]); + const handleInputFile = () => { if (!inputRef.current) return; inputRef.current.click(); @@ -140,7 +159,7 @@ const UploadModal = ({ return res; }; - const handleSubmit = () => { + const handleSubmit = async () => { dispatch({ type: InputActionKind.examType, payload: input.examType.value }); dispatch({ type: InputActionKind.subject, payload: input.subject.value }); @@ -150,6 +169,30 @@ const UploadModal = ({ }) .filter((err) => err != undefined).length; + // if updating + if (defaultData && errors == 0 && selectedFile.img == undefined) { + setLoading({ + uploading: true, + validatingImage: false + }); + + await updatePastPaper({ + file: null, + visibility: input.visibility.value, + examType: input.examType.value, + subject_name: input.subject.value, + uid: defaultData.uid + }); + + setLoading({ + uploading: false, + validatingImage: false + }); + + onClose(); + return; + } + if (selectedFile.img == undefined) setSelectedFile({ img: null, err: InputError.image }); errors = selectedFile.img == null ? errors + 1 : errors; @@ -206,14 +249,26 @@ const UploadModal = ({ uploading: true })); - await upload({ - file: selectedFile.img, - confidence: avgConfidence, - currUser: user!, - visibility: input.visibility.value, - examType: input.examType.value, - subject_name: input.subject.value - }); + if (defaultData) { + await updatePastPaper({ + file: selectedFile.img, + visibility: input.visibility.value, + examType: input.examType.value, + subject_name: input.subject.value, + uid: defaultData.uid, + confidence: avgConfidence + }); + } else { + // update + await upload({ + file: selectedFile.img, + confidence: avgConfidence, + currUser: user!, + visibility: input.visibility.value, + examType: input.examType.value, + subject_name: input.subject.value + }); + } // reset everything @@ -251,7 +306,9 @@ const UploadModal = ({ - {`Past Papers Upload Form`} + + {defaultData ? `Past Papers Edit Form` : `Past Papers Upload Form`} +
{ @@ -295,6 +352,7 @@ const UploadModal = ({ )} + {defaultData && <>} {!selectedFile.img && ( @@ -407,12 +467,21 @@ const UploadModal = ({ {loading.validatingImage && Please Wait Validating Image Using AI} {loading.uploading && Uploading Image to Cloud}
- + {defaultData ? ( + + ) : ( + + )}
@@ -428,7 +497,8 @@ const AutoCompleteSearch = ({ placeholder, value, onSelectOption, - error + error, + helperText }: { title: string; placeholder: string; @@ -442,6 +512,7 @@ const AutoCompleteSearch = ({ }) => boolean | void) | undefined; error: string | undefined; + helperText: string; }) => { return ( <> @@ -453,6 +524,7 @@ const AutoCompleteSearch = ({ background={'var(--card-color)'} _hover={{ background: 'var(--card-color)' }} placeholder={placeholder} + // value={value} /> {options.map((opt, cid) => ( @@ -466,6 +538,7 @@ const AutoCompleteSearch = ({ ))} + {helperText && Prev: {helperText}} {error && ( {error} diff --git a/src/components/pastpaper/pastpapers.tsx b/src/components/pastpaper/pastpapers.tsx index 8aa4235..bcb1f71 100644 --- a/src/components/pastpaper/pastpapers.tsx +++ b/src/components/pastpaper/pastpapers.tsx @@ -1,20 +1,38 @@ -import { Button, Flex, HStack, Input, useDisclosure } from '@chakra-ui/react'; +import { Button, Flex, HStack, Input, Spinner, Stack, Text, useDisclosure } from '@chakra-ui/react'; -import React, { useEffect } from 'react'; +import React, { useMemo, useState } from 'react'; import UploadModal from './UploadModal'; import PastPaper from './PastPaper'; import { usePastPaperPagination } from '~/lib/pastpaper'; -import { deletePastPaper, pastPaperDownVote, pastPaperUpVote } from '~/lib/pastpaper/crud'; +import { + deletePastPaper, + pastPaperDownVote, + pastPaperMarkAsSpam, + pastPaperUpVote +} from '~/lib/pastpaper/crud'; +import { PastPaperDocType } from '~/lib/pastpaper/types'; +import { useAuthState } from 'react-firebase-hooks/auth'; +import { firebase } from '~/lib/firebase'; export default function PastPapers() { const { isOpen, onOpen, onClose } = useDisclosure(); - const [pastPapers, { validate }] = usePastPaperPagination(); + const [pastPapers, { validate, loading }] = usePastPaperPagination(); + const [selectedPaper, setSelectedPaper] = useState(null); + const [input, setInput] = useState(''); + + const filterPapers = useMemo(() => { + return pastPapers.filter((p) => { + return p.subject_name.toLowerCase().includes(input.toLowerCase()); + }); + }, [input, pastPapers]); + + const [user] = useAuthState(firebase.firebaseAuth); return ( <> {/* header */} - - - - + + { + setInput(e.target.value); + }} + /> + + + +
+ âš  Disclaimer + {`Past papers are just to help you understand the exam format. They don't guarantee passing or high scores, as final exams may not include exactly their content.`} +
+
+ + {/* past papers */} - {pastPapers.map((model, idx) => { + {loading && } + {filterPapers.length === 0 && !loading && ( + <> + + 📃 Not Results found. Be the first to upload it. + + + )} + {filterPapers.map((model, idx) => { return ( { - pastPaperDownVote(model.uid, ''); + if (!user) return; + await pastPaperDownVote(model.uid, user.uid); validate(model.uid); }} onUpVote={async () => { - pastPaperUpVote(model.uid, ''); + if (!user) return; + await pastPaperUpVote(model.uid, user.uid); + validate(model.uid); + }} + onEdit={() => { + setSelectedPaper(model); + onOpen(); + }} + markAsSpam={() => { + pastPaperMarkAsSpam(model.uid, !model.spam); validate(model.uid); }} /> ); })} - {/* past paper upload modal */} - +
); diff --git a/src/lib/constant.ts b/src/lib/constant.ts index dbff7f1..401cdea 100644 --- a/src/lib/constant.ts +++ b/src/lib/constant.ts @@ -80,7 +80,8 @@ export const ROUTING = { teachers: '/timetable/teachers', rooms: '/timetable/rooms', clash_resolver: '/util/timetable_clashresolver', - room_activities: '/room-activities' + room_activities: '/room-activities', + past_papers: '/pastpaper' }; export const APIS_ENDPOINTS = { diff --git a/src/lib/pastpaper/crud.ts b/src/lib/pastpaper/crud.ts index 62dcc28..f532b09 100644 --- a/src/lib/pastpaper/crud.ts +++ b/src/lib/pastpaper/crud.ts @@ -26,3 +26,10 @@ export const pastPaperDownVote = async (uid: string, doerId: string) => { votes_count: increment(-1) as any } as Partial); }; + +export const pastPaperMarkAsSpam = async (uid: string, spam: boolean) => { + const docRef = doc(pastPapersCol, uid); + return updateDoc(docRef, { + spam + } as Partial); +}; diff --git a/src/lib/pastpaper/index.ts b/src/lib/pastpaper/index.ts index 8da412b..77b404e 100644 --- a/src/lib/pastpaper/index.ts +++ b/src/lib/pastpaper/index.ts @@ -38,6 +38,7 @@ export const usePastPaperPagination = (): [ PastPaperDocType[], { fetchMore: () => void; + loading: boolean; validate: (uid: string) => void; } ] => { @@ -45,13 +46,16 @@ export const usePastPaperPagination = (): [ const [user] = useAuthState(firebase.firebaseAuth); const [userUploads, setUserUploads] = useState([]); const [lastDocRef, setLastDocRef] = useState(undefined); + const [loading, setLoading] = useState(false); useEffect(() => { const fetchData = async () => { + setLoading(true); const initialFeedSnapShot = await fetchFeed(undefined); const initialFeed = initialFeedSnapShot.docs.map((doc) => doc.data() as PastPaperDocType); setPastPapers(initialFeed); setLastDocRef(initialFeedSnapShot.docs.at(-1)); + setLoading(false); }; fetchData(); @@ -105,6 +109,7 @@ export const usePastPaperPagination = (): [ return [ pastPapersAfterFilter, { + loading, fetchMore, validate } diff --git a/src/lib/pastpaper/upload.ts b/src/lib/pastpaper/upload.ts index 24e2712..37da9d8 100644 --- a/src/lib/pastpaper/upload.ts +++ b/src/lib/pastpaper/upload.ts @@ -2,7 +2,7 @@ import { UserDocType } from '../firebase_doctypes'; import { pastPapersCol, uploadBlobToFirestore } from '../firebase'; import { fileToBlob } from '../util'; import { PastPaperDocType } from './types'; -import { doc, serverTimestamp, setDoc } from 'firebase/firestore'; +import { doc, serverTimestamp, setDoc, updateDoc } from 'firebase/firestore'; interface UploadProps { file: File | null; @@ -13,7 +13,41 @@ interface UploadProps { visibility: boolean; } -export default async function upload({ +interface UpdateProps { + file: File | null; + subject_name: string; + examType: string; + visibility: boolean; + uid: string; + confidence?: number; +} + +export async function updatePastPaper({ + file, + subject_name, + examType, + visibility, + uid, + confidence +}: UpdateProps) { + let photoUrl = undefined; + if (file) { + photoUrl = await uploadBlobToFirestore(fileToBlob(file)); + } + const docRef = doc(pastPapersCol, uid); + + const docData: Partial = { + subject_name, + visibility, + exam_type: examType, + upload_at: serverTimestamp(), + ...(photoUrl === undefined ? {} : { photo_url: photoUrl, confidence }) + }; + + await updateDoc(docRef, docData); +} + +export default async function uploadPastPaper({ file, subject_name, currUser, diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4644e56..8df4d82 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -23,6 +23,7 @@ import { ROUTING } from '~/lib/constant'; import { useReferrer } from '~/hooks/useReferrer'; import PalestineSupportBanner from '~/components/announcements/PalestineSupportBanner'; import NewFeature from '~/components/design/NewFeature'; +import PastPaperToast from '~/components/pastpaper/PastPapersToast'; // import NewFeature from '~/components/design/NewFeature'; // import UpComingEvent from '~/components/design/UpCommingEvent'; @@ -57,6 +58,7 @@ export default function App({ Component, pageProps }: AppProps) { + {/* {!excludeHeadPages.includes(router.pathname) && (
- {/* */} +
diff --git a/src/pages/pastpaper/index.tsx b/src/pages/pastpaper/index.tsx index ae46545..192d277 100644 --- a/src/pages/pastpaper/index.tsx +++ b/src/pages/pastpaper/index.tsx @@ -3,6 +3,7 @@ import { GetServerSidePropsContext } from 'next'; import Head from 'next/head'; import MainAnimator from '~/components/design/MainAnimator'; import PastPapers from '~/components/pastpaper/pastpapers'; +import { SocialLinks } from '~/components/seo/Seo'; import AllSubjectsProvider from '~/hooks/AllSubjectsProvider'; import { decrypt } from '~/lib/cipher'; import { APIS_ENDPOINTS } from '~/lib/constant'; @@ -38,6 +39,21 @@ export default function PastPaperPage({ subjects }: GetStaticPropsReturnType) { <> Past Papers + + + + + + + + +