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}
-
+
{
+ 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 && (
+
+
+
+ {
+ 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 && (
-
)}
+ >}>
+ }
+ onClick={(e) => {
+ markAsSpam();
+ }}>
+ {model.spam ? 'Mark as not spam' : 'Mark as spam'}
+
+
>
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 (
+
+
+ {
+ toast.close(toastId);
+ }}>
+
+
+
+
+
+ 📃 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`}
+
@@ -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 */}
-
-
-
- Upload
-
-
+
+ {
+ setInput(e.target.value);
+ }}
+ />
+ {
+ onOpen();
+ setSelectedPaper(null);
+ }}>
+ Upload
+
+
+
+
+ âš 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
+
+
+
+
+
+
+
+
+