diff --git a/src/components/RoomActivities.tsx b/src/components/RoomActivities.tsx new file mode 100644 index 0000000..a72abf1 --- /dev/null +++ b/src/components/RoomActivities.tsx @@ -0,0 +1,292 @@ +import { + Alert, + Box, + Button, + Card, + Center, + Flex, + FlexProps, + Heading, + Input, + Text, + Link, + Stack, + useMediaQuery, + Grid, + Divider, + Tooltip +} from '@chakra-ui/react'; +import { motion } from 'framer-motion'; +import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import { RoomActivitiesStateType, UseStateProps } from '~/types/typedef'; +import styles from '~/styles/freeclassroom.module.css'; +import BackBtn from './design/BackBtn'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import DropDown from './design/DropDown'; +import { DAYS_NAME, ROUTING, timetableHeadTitles } from '~/lib/constant'; +import { hashStr } from '~/lib/cipher'; + +type RoomMetaData = { + room: string; + program?: string; +}; + +interface IRoomsType { + 'NB Rooms': Array; + 'OB Rooms': Array; + Labs: Array; + Seminars: Array; + Others: Array; +} + +export default function RoomActivities({ + parentState, + departments +}: { + parentState: UseStateProps; + departments: string[]; +}) { + const [isUnder500] = useMediaQuery('(max-width: 500px)'); + + const [rooms, setRooms] = useState({ + 'NB Rooms': [], + 'OB Rooms': [], + Labs: [], + Seminars: [], + Others: [] + }); + + const [state, setState] = parentState; + const [input, setInput] = useState(''); + + useEffect(() => { + let room_states: IRoomsType = { + 'NB Rooms': [], + 'OB Rooms': [], + Labs: [], + Seminars: [], + Others: [] + }; + + state.rooms + .filter((room) => room.room.toLocaleLowerCase().includes(input)) + .forEach((entry) => { + const roomName = entry.room.toLocaleLowerCase().trim(); + + if (roomName.includes('room') && !roomName.includes('seminar')) { + if (roomName.endsWith('nb')) { + room_states['NB Rooms'].push(entry); + } else { + room_states['OB Rooms'].push(entry); + } + } else if (roomName.includes('lab')) { + room_states['Labs'].push(entry); + } else if (roomName.includes('seminar')) { + room_states['Seminars'].push(entry); + } else { + room_states['Others'].push(entry); + } + }); + + setRooms(room_states); + }, [state.rooms, input]); + + return ( + <> + + + + + + + + + {`Queried at ${state.time.toString()} - Real time calculation`} + +
+ setInput(e.target.value.toLocaleLowerCase())} + /> +
+ +
+ { + const dayIdx = DAYS_NAME.indexOf(d); + setState((prevState) => { + let customDate = prevState.customDate || new Date(); + while (customDate.getDay() != dayIdx) + customDate.setDate(customDate.getDate() + 1); + return { ...prevState, customDate, loading: true }; + }); + }} + options={DAYS_NAME}> + + + + { + const [hours, min] = t.split(':'); + setState((prevState) => { + let customDate = prevState.customDate || new Date(); + // set hours and min here + customDate.setHours(parseInt(hours)); + customDate.setMinutes(parseInt(min)); + return { + ...prevState, + customDate, + loading: true + }; + }); + }} + options={Array.from( + new Set( + timetableHeadTitles + .map((t) => [t.startTime, t.endTime]) + .reduce((acc, curr) => { + return [...acc, ...curr.map((c) => c)]; + }, []) + ) + )}> + + + + +
+ + {/* Colors */} + + {/* {departments.map((dep, idx) => ( + {dep} + ))} */} +
+ {Object.entries(rooms).map(([key, val]: [string, RoomMetaData[]], idx) => { + return ( + +
+ {`${key}`.toUpperCase()} +
+ + {/* */} +
+ ); + })} +
+ + ); +} + +const RoomsRenderer = ({ + isUnder500, + classRooms +}: { + isUnder500: boolean; + classRooms: Array; +}) => { + const containerRef = useRef(null); + const [cardPerRow, setCardPerRow] = useState(8); + + useEffect(() => { + const onWindowResize = () => { + if (!containerRef.current) return; + const containerWidth = containerRef.current.clientWidth; + const EACH_CARD_SIZE = 120; + const newCardPerRow = Math.floor(containerWidth / EACH_CARD_SIZE); + setCardPerRow(newCardPerRow || 1); + }; + + window.addEventListener('resize', onWindowResize); + onWindowResize(); + + return () => { + window.removeEventListener('resize', onWindowResize); + }; + }, [containerRef]); + + return ( + <> + + {classRooms + .sort((a, b) => a.room.length - b.room.length) + .map((room, key) => { + return ; + })} + + + ); +}; + +interface RoomCardProps extends FlexProps { + room: RoomMetaData; +} + +const RoomCard = ({ room, ...rest }: RoomCardProps) => { + const isFree = room.program == undefined; + return ( + + + + + {room.room} + + + + + + {room.program || 'Free'} + + + + + ); +}; diff --git a/src/lib/FirebaseAnalysis.ts b/src/lib/FirebaseAnalysis.ts index e989032..1955ae7 100644 --- a/src/lib/FirebaseAnalysis.ts +++ b/src/lib/FirebaseAnalysis.ts @@ -46,8 +46,9 @@ export enum FIREBASE_ANALYTICS_EVENTS { print_time_table = 'print_time_table', promotion_closed = 'promotion_closed', teacher_timetable = 'teacher_timetable', + room_activities = 'room_activities', // affiliate - educative = 'educative', + educative = 'educative' } import { logEvent } from 'firebase/analytics'; @@ -60,5 +61,5 @@ export function reportFirebaseAnalytics(key: string, val: any) { export function useFirebaseAnalyticsReport(eventName: FIREBASE_ANALYTICS_EVENTS) { useEffect(() => { reportFirebaseAnalytics(eventName.toString(), {}); - }, []); + }, [eventName]); } diff --git a/src/lib/ads.ts b/src/lib/ads.ts index e9a0db0..42a60d7 100644 --- a/src/lib/ads.ts +++ b/src/lib/ads.ts @@ -7,17 +7,20 @@ export const AVAILABLE_ADS = [ { title: 'Educative', link: 'https://click.linksynergy.com/fs-bin/click?id=oQaDKQaTta0&offerid=1095549.8&bids=1095549.8&type=3&subid=0', - description: 'Level up your tech skills and stay ahead of the curve with Educative learning paths' + description: + 'Level up your tech skills and stay ahead of the curve with Educative learning paths' }, { title: 'Educative', link: 'https://click.linksynergy.com/fs-bin/click?id=oQaDKQaTta0&offerid=1095549.7&bids=1095549.7&type=3&subid=0', - description: 'Learn in-demand tech skills and accelerate your career with curated learning paths' + description: + 'Learn in-demand tech skills and accelerate your career with curated learning paths' }, { title: 'Educative', link: 'https://click.linksynergy.com/fs-bin/click?id=oQaDKQaTta0&offerid=1095549.6&bids=1095549.6&type=3&subid=0', - description: 'Curated programming Paths for seamless learning. Accelerate your programming skills on Educative.' + description: + 'Curated programming Paths for seamless learning. Accelerate your programming skills on Educative.' }, { title: 'Educative', @@ -39,7 +42,7 @@ export const AVAILABLE_ADS = [ link: 'https://click.linksynergy.com/fs-bin/click?id=oQaDKQaTta0&offerid=1095549.3&bids=1095549.3&type=3&subid=0', description: 'Learn in-demand programming languages interactively on Educative' } -] +]; export default function getAd() { return AVAILABLE_ADS[Math.floor(Math.random() * AVAILABLE_ADS.length)]; diff --git a/src/lib/cipher.ts b/src/lib/cipher.ts index ee93f23..201e149 100644 --- a/src/lib/cipher.ts +++ b/src/lib/cipher.ts @@ -1,25 +1,27 @@ -import { Encoding, createDecipheriv } from "crypto"; -import sha256 from "crypto-js/sha256" +import { Encoding, createDecipheriv } from 'crypto'; +import sha256 from 'crypto-js/sha256'; const getCredentials = () => ({ - key: Buffer.from(process.env.OPEN_DB_KEY || "", "hex"), - iv: Buffer.from(process.env.OPEN_DB_IV || "", "hex"), + key: Buffer.from(process.env.OPEN_DB_KEY || '', 'hex'), + iv: Buffer.from(process.env.OPEN_DB_IV || '', 'hex') }); -export function decrypt({ - algo, encoding, crypted +export function decrypt({ + algo, + encoding, + crypted }: { - algo: string, - encoding: Encoding, - crypted: string + algo: string; + encoding: Encoding; + crypted: string; }) { - const {key, iv} = getCredentials(); + const { key, iv } = getCredentials(); const decipher = createDecipheriv(algo, key, iv); - let decrypted = decipher.update(crypted, encoding, "utf8"); - decrypted += decipher.final("utf8"); + let decrypted = decipher.update(crypted, encoding, 'utf8'); + decrypted += decipher.final('utf8'); return JSON.parse(decrypted) as T; } export function hashStr(str: string) { - return sha256(str).toString() + return sha256(str).toString(); } diff --git a/src/lib/constant.ts b/src/lib/constant.ts index 11e21ef..4cf2078 100644 --- a/src/lib/constant.ts +++ b/src/lib/constant.ts @@ -90,11 +90,15 @@ export const APIS_ENDPOINTS = { META_DATA: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/meta_data.json', TEACHERS: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/teachers.json', ROOMS: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/rooms.json', - TEACHER_PATHS: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/teacher_paths.json', - ROOM_PATHS: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/rooms_paths.json', - TIMETABLE_PATHS: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/timetable_paths.json', + TEACHER_PATHS: + 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/teacher_paths.json', + ROOM_PATHS: + 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/rooms_paths.json', + TIMETABLE_PATHS: + 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/timetable_paths.json', TIMETABLE: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/', - ALL_TIMETABLES: 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/all_timetables.json' + ALL_TIMETABLES: + 'https://raw.githubusercontent.com/Zain-ul-din/lgu-crawler/master/db/all_timetables.json' }; export const CHAT_CATEGORIES = { diff --git a/src/lib/election.ts b/src/lib/election.ts index 4157c31..b3cd119 100644 --- a/src/lib/election.ts +++ b/src/lib/election.ts @@ -1,4 +1,4 @@ -import { FieldValue } from "firebase/firestore"; +import { FieldValue } from 'firebase/firestore'; export const ELECTION_INPUT_VALIDATION_PROMPT = ` We are in the process of conducting a moderator election on our platform it's a university timetable. One of the tasks involves validating the submissions @@ -35,9 +35,9 @@ User Input for Validation: `; export interface ElectionPromptGeminiRes { - "isValid": boolean - "message"?: string, - "markDown": string + isValid: boolean; + message?: string; + markDown: string; } export interface CandidateDocType { diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 016b62b..720f03d 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -69,7 +69,7 @@ export const electionColRef = collection(firebase.firebaseStore, 'election'); /// /// firebase storage /// -import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; +import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; import { v4 as uuidv4 } from 'uuid'; export async function uploadBlobToFirestore( diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index c044db0..b32e812 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -1,10 +1,9 @@ -import { GoogleGenerativeAI } from "@google/generative-ai"; +import { GoogleGenerativeAI } from '@google/generative-ai'; export default async function askGemini(prompt: string) { const client = new GoogleGenerativeAI(process.env.NEXT_PUBLIC_GEMINI_KEY as string); - const model = client.getGenerativeModel({ model: "gemini-pro"}); + const model = client.getGenerativeModel({ model: 'gemini-pro' }); const result = await model.generateContent(prompt); const res = result.response.text(); return res; } - diff --git a/src/lib/pastpaper/types.ts b/src/lib/pastpaper/types.ts index 5a4a0f8..a314d81 100644 --- a/src/lib/pastpaper/types.ts +++ b/src/lib/pastpaper/types.ts @@ -1,26 +1,24 @@ -import { User } from "firebase/auth"; -import { FieldValue } from "firebase/firestore"; +import { User } from 'firebase/auth'; +import { FieldValue } from 'firebase/firestore'; export interface PastPaperDocType { uid: string; photo_url: string; subject_name: string; - + upload_at: FieldValue; deleted?: boolean; - - // true if, marked as a spam by moderator + + // true if, marked as a spam by moderator spam?: boolean; - - // list of people uid's those post vote + + // list of people uid's those post vote up_votes?: Array; down_votes?: Array; // votes can be in +ve and -ve votes_count: number; - + // embedded user inside in the document to prevent joins - uploader: Pick + uploader: Pick; uploader_uid: string; - } - diff --git a/src/lib/pastpaper/upload.ts b/src/lib/pastpaper/upload.ts index 98170e5..f8d9882 100644 --- a/src/lib/pastpaper/upload.ts +++ b/src/lib/pastpaper/upload.ts @@ -1,25 +1,23 @@ -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 { UserDocType } from '../firebase_doctypes'; +import { pastPapersCol, uploadBlobToFirestore } from '../firebase'; +import { fileToBlob } from '../util'; +import { PastPaperDocType } from './types'; +import { doc, serverTimestamp, setDoc } from 'firebase/firestore'; interface UploadProps { - file: File | null - currUser: UserDocType, + file: File | null; + currUser: UserDocType; subject_name: string; } -export default async function upload( - { file, subject_name, currUser }: UploadProps -) { - if(!file) return; - +export default async function upload({ file, subject_name, currUser }: UploadProps) { + if (!file) return; + // todo: validate file input via gemini OR may be try some free otpion try { - const photo_url = await uploadBlobToFirestore(fileToBlob(file)) - + const photo_url = await uploadBlobToFirestore(fileToBlob(file)); + const docRef = doc(pastPapersCol); const docData: PastPaperDocType = { photo_url, @@ -33,13 +31,12 @@ export default async function upload( }, votes_count: 0, uploader_uid: currUser.uid - } + }; - await setDoc(docRef, docData) - - } catch(err) { + await setDoc(docRef, docData); + } catch (err) { return false; } - return true -} + return true; +} diff --git a/src/lib/util.ts b/src/lib/util.ts index 0d64e17..452b905 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -141,7 +141,7 @@ export const isLectureTime = (timetableData: TimetableData, currTime: Date) => { const time = new Date(currTime); ///! TODO - /// retrieve tolerance value from database. + /// retrieve tolerance value from database. for special occasion const tolerance = 0; // min time.setMinutes(currTime.getMinutes() + tolerance); @@ -158,6 +158,86 @@ export const isLectureTime = (timetableData: TimetableData, currTime: Date) => { return isItTrue; }; +const isNonEmptyStr = (str: string) => str != ''; + +/** + * returns busy rooms atm + * @param timetables + * @param currTime + * @returns + */ +export function getBusyRooms(timetables: Array, currTime: Date) { + const isSameDay = (day: string) => + day.toLocaleLowerCase() == DAYS_NAME[currTime.getDay()].toLocaleLowerCase(); + const getRoomsWhereLectureIsGoingOn = (timetableData: TimetableData[]) => + timetableData.map((data) => (isLectureTime(data, currTime) ? data.roomNo : '')); + + const busyRooms = timetables + .map((timetable) => + Object.entries(timetable.timetable) + .map(([day, timetableData]: [string, Array]) => + isSameDay(day) ? getRoomsWhereLectureIsGoingOn(timetableData) : [] + ) + .flat(1) + ) + .flat(1); + + return Array.from(new Set(busyRooms)).filter(isNonEmptyStr); +} + +/** + * return busy class room along metadata + * @param timetables + * @param currTime + * @returns + */ +export function getBusyRoomsAlongMetaData(timetables: Array, currTime: Date) { + const isSameDay = (day: string) => + day.toLocaleLowerCase() == DAYS_NAME[currTime.getDay()].toLocaleLowerCase(); + const getRoomsWhereLectureIsGoingOn = ( + timetableData: TimetableData[], + timetable: TimetableDocType + ) => + timetableData + .map((data) => + isLectureTime(data, currTime) + ? { room: data.roomNo, program: timetable.payload?.program as string } + : null + ) + .filter((entry): entry is { room: string; program: string } => entry !== null); + + const busyRooms = timetables + .map((timetable) => + Object.entries(timetable.timetable) + .map(([day, timetableData]: [string, Array]) => + isSameDay(day) ? getRoomsWhereLectureIsGoingOn(timetableData, timetable) : [] + ) + .flat(1) + ) + .flat(1); + + return busyRooms.filter((ele, idx, self)=> idx === self.findIndex(t => t.room === ele.room)); +} + +/** + * returns free rooms from busyRooms + * @param timetables + * @param busyRooms + */ +export function getFreeRooms(timetables: Array, busyRooms: string[]) { + const freeRooms = timetables + .map((timetable) => + Object.entries(timetable.timetable) + .map(([day, timetableData]: [string, Array]) => + timetableData.map((data) => (!busyRooms.includes(data.roomNo) ? data.roomNo : '')) + ) + .flat(1) + ) + .flat(1); + + return Array.from(new Set(freeRooms)).filter(isNonEmptyStr); +} + /** * Calculates free classrooms * @param timetables Array @@ -168,37 +248,33 @@ export function calculateFreeClassrooms( timetables: Array, currTime: Date ): Array { - const busyRooms = Array.from( - new Set( - timetables - .map((timetable) => - Object.entries(timetable.timetable) - .map(([day, timetableData]: [string, Array]) => - day.toLocaleLowerCase() == DAYS_NAME[currTime.getDay()].toLocaleLowerCase() - ? timetableData.map((data) => (isLectureTime(data, currTime) ? data.roomNo : '')) - : [] - ) - .reduce((prev, curr) => prev.concat(curr), []) - ) - .reduce((prev, curr) => prev.concat(curr), []) - ) - ).filter((room) => room != ''); - - const freeRooms = Array.from( - new Set( - timetables - .map((timetable) => - Object.entries(timetable.timetable) - .map(([day, timetableData]: [string, Array]) => - timetableData.map((data) => (!busyRooms.includes(data.roomNo) ? data.roomNo : '')) - ) - .reduce((prev, curr) => prev.concat(curr), []) - ) - .reduce((prev, curr) => prev.concat(curr), []) - ) - ).filter((room) => room != ''); + return getFreeRooms(timetables, getBusyRooms(timetables, currTime)); +} + +/** + * returns all departments + * @param timetables + * @returns + */ +export function getDepartments ( + timetables: Array, +) { + return Array.from(new Set(timetables.map(timetable=> timetable.payload?.program as string))); +} + +/** + * calculates Room Activities + * @param timetables + * @param currTime + * @returns + */ +export function calculateTimeActivities(timetables: Array, currTime: Date) { + const busyRoomsMetadata = getBusyRoomsAlongMetaData(timetables, currTime); + const freeRooms = getFreeRooms(timetables, getBusyRooms(timetables, currTime)).map(room => ({ + room: room, program: undefined + })); - return freeRooms; + return [...busyRoomsMetadata, ...freeRooms]; } export function fromFirebaseTimeStamp(time: any): Date { @@ -247,9 +323,9 @@ function userCacheHof() { function userCacheHofGeneric() { let userCache: { [key: string]: boolean } = {}; - const clearCache = ()=> userCache = {}; + const clearCache = () => (userCache = {}); - return (state: UseStateProps<{ [uid: string] : UserDocType }>, uId: string) => { + return (state: UseStateProps<{ [uid: string]: UserDocType }>, uId: string) => { if (uId in userCache) return clearCache; userCache[uId] = true; // user cached @@ -262,8 +338,8 @@ function userCacheHofGeneric() { return { ...prevState, [uId]: { - ...(docSnapShot.docs.map((d) => d.data())[0] as UserDocType), - id: uId + ...(docSnapShot.docs.map((d) => d.data())[0] as UserDocType), + id: uId } }; }); @@ -391,4 +467,4 @@ export function clamp(num: number, min: number, max: number) { export function fileToBlob(file: File): Blob { const blobFile = new Blob([file], { type: file.type }); return blobFile; -} \ No newline at end of file +} diff --git a/src/pages/api/util/cache.ts b/src/pages/api/util/cache.ts index 293f8e8..dcc6c07 100644 --- a/src/pages/api/util/cache.ts +++ b/src/pages/api/util/cache.ts @@ -9,7 +9,6 @@ import { SubjectOjectType, TimetableDataType, TimetableDocType } from '~/types/t var cache: SubjectOjectType = {}; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method == 'POST') { try { const { key } = req.query; @@ -40,14 +39,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< */ async function updateCache() { console.log('At Util Cache, Going to Cache values from firebase'); - const { data } = await axios.get(APIS_ENDPOINTS.ALL_TIMETABLES) - interface APIResponseType extends TimetableDocType - { uid: string } - - const timetables = decrypt>(data).map((t)=> ({ - id: t.uid, ...t - })) - + const { data } = await axios.get(APIS_ENDPOINTS.ALL_TIMETABLES); + interface APIResponseType extends TimetableDocType { + uid: string; + } + + const timetables = decrypt>(data).map((t) => ({ + id: t.uid, + ...t + })); + cache = constructSubjectOjectFromTimetables(timetables); } diff --git a/src/pages/api/util/workflow.ts b/src/pages/api/util/workflow.ts index d0335d5..7fa5401 100644 --- a/src/pages/api/util/workflow.ts +++ b/src/pages/api/util/workflow.ts @@ -1,61 +1,72 @@ -import { FieldValue, arrayUnion, collection, doc, getDoc, getDocs, query, serverTimestamp, setDoc, updateDoc, where } from "firebase/firestore" -import Joi from "joi" -import { NextApiRequest, NextApiResponse } from "next" -import { userColsRef, workFlowColRef } from "~/lib/firebase"; -import { UserDocType } from "~/lib/firebase_doctypes"; -import { fromFirebaseTimeStamp } from "~/lib/util"; +import { + FieldValue, + arrayUnion, + collection, + doc, + getDoc, + getDocs, + query, + serverTimestamp, + setDoc, + updateDoc, + where +} from 'firebase/firestore'; +import Joi from 'joi'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { userColsRef, workFlowColRef } from '~/lib/firebase'; +import { UserDocType } from '~/lib/firebase_doctypes'; +import { fromFirebaseTimeStamp } from '~/lib/util'; interface Payload { user_id: string; session_id: string; } -const schema = Joi.object({ +const schema = Joi.object({ user_id: Joi.string(), session_id: Joi.string().length(26) -}) +}); export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - - if(req.method !== 'POST') { - res.status(405).send('not allowed') + if (req.method !== 'POST') { + res.status(405).send('not allowed'); return; } - const body: Payload = JSON.parse(req.body) + const body: Payload = JSON.parse(req.body); - if(!schema.validate(body)) { - res.status(402).send({ message: 'Invalid Payload' }) + if (!schema.validate(body)) { + res.status(402).send({ message: 'Invalid Payload' }); return; } const usersSnapShot = await getDocs(query(userColsRef, where('uid', '==', body.user_id))); - if(usersSnapShot.docs.length === 0) throw new Error('Invalid User') - const user = usersSnapShot.docs.map((doc)=> doc.data())[0] as UserDocType - - const isAdmin = user.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL; - const quotaDoc = await initWorkFlowDoc() - - if(!isAdmin) { - const { valid, message } = await canTriggerWorkFlow(quotaDoc,user); - if(!valid) { - res.status(405).send({ message }) + if (usersSnapShot.docs.length === 0) throw new Error('Invalid User'); + const user = usersSnapShot.docs.map((doc) => doc.data())[0] as UserDocType; + + const isAdmin = user.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL; + const quotaDoc = await initWorkFlowDoc(); + + if (!isAdmin) { + const { valid, message } = await canTriggerWorkFlow(quotaDoc, user); + if (!valid) { + res.status(405).send({ message }); return; } } - - await triggerWorkFlow(user.uid, body.session_id) + + await triggerWorkFlow(user.uid, body.session_id); res.status(200).send({ message: '🎉 Thank you for your contribution. WorkFlow has been Triggered.', succeed: true - }) - } catch(err) { + }); + } catch (err) { console.log(err); res.status(500).send({ message: 'Something went Wrong' - }) + }); } } @@ -63,48 +74,49 @@ interface WorkFlowQuotaType { users: { idx: string; uid: string; - }[] - last_updated: Date | FieldValue | string, - participants: string [] + }[]; + last_updated: Date | FieldValue | string; + participants: string[]; } async function initWorkFlowDoc() { - const quotaDocRef = doc(workFlowColRef, new Date().toDateString()); + const quotaDocRef = doc(workFlowColRef, new Date().toDateString()); let quotaSnapShot = await getDoc(quotaDocRef); - if(!quotaSnapShot.exists()) { + if (!quotaSnapShot.exists()) { const initialData: WorkFlowQuotaType = { last_updated: serverTimestamp(), users: [], participants: [] - } + }; await setDoc(quotaDocRef, initialData, { merge: true }); return initialData; } - return quotaSnapShot.data() as WorkFlowQuotaType + return quotaSnapShot.data() as WorkFlowQuotaType; } -async function triggerWorkFlow(uid: string, session_id: string,) { - const quotaDocRef = doc(workFlowColRef, new Date().toDateString()); +async function triggerWorkFlow(uid: string, session_id: string) { + const quotaDocRef = doc(workFlowColRef, new Date().toDateString()); const auxDoc = doc(workFlowColRef); - const GITHUB_URL = 'https://api.github.com/repos/Zain-ul-din/lgu-crawler/actions/workflows/89946576/dispatches' - const hook = process.env.RE_DEPLOYMENT_HOOK || 'https://www.google.com' - + const GITHUB_URL = + 'https://api.github.com/repos/Zain-ul-din/lgu-crawler/actions/workflows/89946576/dispatches'; + const hook = process.env.RE_DEPLOYMENT_HOOK || 'https://www.google.com'; + await fetch(GITHUB_URL, { method: 'POST', headers: { - "Accept": "application/vnd.github+json", - "Authorization": `Bearer ${process.env.GITHUB_USER_TOKEN}`, - "X-GitHub-Api-Version": "2022-11-28", + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${process.env.GITHUB_USER_TOKEN}`, + 'X-GitHub-Api-Version': '2022-11-28' }, body: JSON.stringify({ - ref: "master", + ref: 'master', inputs: { session_id, hook } }) - }) - + }); + await updateDoc(quotaDocRef, { last_updated: serverTimestamp(), users: arrayUnion({ @@ -112,17 +124,17 @@ async function triggerWorkFlow(uid: string, session_id: string,) { idx: auxDoc.id }), participants: arrayUnion(uid) - }) + }); } /** * Determines how many work flow can trigger per day -*/ + */ const MAX_WORK_FLOW_PER_DAY = 5; /** * Cool Down between each trigger -*/ + */ const ONE_HOUR_IN_MS = 1000 * 60 * 60; const COOL_DOWN_TIME = ONE_HOUR_IN_MS * 1; const PRO_COOL_DOWN_TIME = ONE_HOUR_IN_MS * 0.25; @@ -133,42 +145,43 @@ const PRO_COOL_DOWN_TIME = ONE_HOUR_IN_MS * 0.25; const MAX_REQ_PER_USER = 2; const MAX_REQ_PER_PRO_USER = 4; +async function canTriggerWorkFlow(quota: WorkFlowQuotaType, user: UserDocType) { + const { last_updated, users } = quota; -async function canTriggerWorkFlow(quota: WorkFlowQuotaType,user: UserDocType) { - const { last_updated, users } = quota - - if(users.length === MAX_WORK_FLOW_PER_DAY) { - return { + if (users.length === MAX_WORK_FLOW_PER_DAY) { + return { message: 'Daily WorkFlow Trigger Quota Exceed', - valid: false, - } + valid: false + }; } const { coolDownTime, maxReqLimit } = { coolDownTime: user.pro ? PRO_COOL_DOWN_TIME : COOL_DOWN_TIME, maxReqLimit: user.pro ? MAX_REQ_PER_PRO_USER : MAX_REQ_PER_USER - } + }; - const timeSpanSinceLastUpdate = new Date().getTime() - fromFirebaseTimeStamp(last_updated).getTime() - const alreadyReqMade = users.filter(u => u.uid === user.uid).length; - - if(alreadyReqMade >= maxReqLimit) { + const timeSpanSinceLastUpdate = + new Date().getTime() - fromFirebaseTimeStamp(last_updated).getTime(); + const alreadyReqMade = users.filter((u) => u.uid === user.uid).length; + + if (alreadyReqMade >= maxReqLimit) { return { valid: false, message: `Your Personal Quota of ${maxReqLimit} request has been exceed` - } + }; } - if(timeSpanSinceLastUpdate < coolDownTime) { - const remainingCoolDownTime = (((coolDownTime - timeSpanSinceLastUpdate) / ONE_HOUR_IN_MS) * 60) | 0; - return { + if (timeSpanSinceLastUpdate < coolDownTime) { + const remainingCoolDownTime = + (((coolDownTime - timeSpanSinceLastUpdate) / ONE_HOUR_IN_MS) * 60) | 0; + return { message: `Someone already triggered a workflow. Please try again after ${remainingCoolDownTime} min(s).`, - valid: false, + valid: false }; } return { message: '', valid: true - } + }; } diff --git a/src/pages/room-activities/index.tsx b/src/pages/room-activities/index.tsx new file mode 100644 index 0000000..5f73c56 --- /dev/null +++ b/src/pages/room-activities/index.tsx @@ -0,0 +1,119 @@ +import axios from 'axios'; +import { GetStaticPropsContext } from 'next'; +import Head from 'next/head'; +import { SocialLinks } from '~/components/seo/Seo'; + +import { FIREBASE_ANALYTICS_EVENTS, useFirebaseAnalyticsReport } from '~/lib/FirebaseAnalysis'; + +import { RoomActivitiesStateType, TimetableDocType } from '~/types/typedef'; + +import { calculateTimeActivities, getDepartments } from '~/lib/util'; +import Loader from '~/components/design/Loader'; +import { Center } from '@chakra-ui/react'; +import Button from '~/components/design/Button'; +import { useEffect, useMemo, useState } from 'react'; +import MainAnimator from '~/components/design/MainAnimator'; +import { APIS_ENDPOINTS } from '~/lib/constant'; +import { decrypt } from '~/lib/cipher'; +import RoomActivities from '~/components/RoomActivities'; + +/** + * @param context + * @returns all timetables + */ +export async function getStaticProps(context: GetStaticPropsContext) { + const { data } = await axios.get(APIS_ENDPOINTS.ALL_TIMETABLES); + const timetables = decrypt(data); + + return { + props: { + timetables + } + }; +} + +export default function RoomActivitiesPage({ + timetables +}: { + timetables: Array; +}) { + useFirebaseAnalyticsReport(FIREBASE_ANALYTICS_EVENTS.room_activities); + + const [state, setState] = useState({ + loading: true, + time: new Date(), + customDate: null, + rooms: [] + }); + + const departments = useMemo(() => getDepartments(timetables), [timetables]); + + useEffect(() => { + const fetchTime = async () => { + const currTime = new Date( + (await axios.get('http://worldtimeapi.org/api/timezone/Asia/Karachi')).data.datetime + ); + setState({ ...state, time: currTime, loading: false }); + }; + + fetchTime(); + + const timeUpdater = setInterval(() => { + const updatedTime = state.time; + updatedTime.setSeconds(state.time.getSeconds() + 1); + setState((prev) => { + return { + ...prev, + rooms: calculateTimeActivities( + timetables, + prev.customDate ? prev.customDate : updatedTime + ), + time: updatedTime, + loading: false + }; + }); + }, 1000); + + return () => clearInterval(timeUpdater); + }, []); + + return ( + <> + + LGU Room Activities + + + + + + + + + + + + + {!state.loading && ( + + )} + {state.loading && ( + <> + Fetching Current time... +
+ +
+ + )} +
+ Timetable Updated At: {new Date(timetables[0].updatedAt).toDateString()} +
+
+ + ); +} diff --git a/src/styles/globals.css b/src/styles/globals.css index 51b19bf..ac6c1fc 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -251,7 +251,6 @@ a { /* Print Module */ @media print { - table { page-break-after: always !important; } @@ -288,42 +287,45 @@ a { transform-origin: 0 0; color: black !important; } - + * { /* styling */ box-sizing: border-box !important; padding: 3px !important; border-color: black !important; } - - th, td, tr, p, span { + + th, + td, + tr, + p, + span { font-size: 1rem !important; } - @page{ + @page { background: black; - margin:0; + margin: 0; size: A4; padding: 0; } - - .print_timetable { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: 100%; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - page-break-inside: avoid; - max-height: 100%; - overflow: hidden; - page-break-after: always; - } + .print_timetable { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + page-break-inside: avoid; + max-height: 100%; + overflow: hidden; + page-break-after: always; + } ::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 0.1px rgba(0, 0, 0, 0) !important; diff --git a/src/types/typedef.ts b/src/types/typedef.ts index 2ea3f71..f584be8 100644 --- a/src/types/typedef.ts +++ b/src/types/typedef.ts @@ -104,6 +104,11 @@ export interface TimetableDocType { timetable: TimetableResponseType; updatedAt: string; id?: string; + payload?: { + program: string; + section: string; + semester: string; + } } export interface UserDataDocType extends User { @@ -143,6 +148,16 @@ export interface FreeClassRoomStateType { customDate: Date | null; } +export interface RoomActivitiesStateType { + loading: boolean; + time: Date; + rooms: { + room: string; + program?: string; + }[]; + customDate: Date | null; +} + import { Dispatch } from 'react'; export type UseStateProps = [T, Dispatch>];