Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/import lectures #42

Merged
merged 7 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ yarn-error.log*
.vercel

sw.js*
workbox*.js*
workbox*.js*
package-lock.json
6 changes: 4 additions & 2 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="redux-persist" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
13 changes: 10 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
const withPWA = require('next-pwa')
// @ts-check

module.exports = withPWA({
const pwa = require('next-pwa')
const withPlugins = require('next-compose-plugins')
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
pwa: {
dest: 'public',
register: true,
skipWaiting: true,
},
})
}

module.exports = withPlugins([pwa], nextConfig)
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"@testing-library/react": "^10.2.1",
"@testing-library/user-event": "^12.0.2",
"@types/redux-logger": "^3.0.9",
"cheerio": "^1.0.0-rc.12",
"dotenv": "^8.2.0",
"framer-motion": "^4.0.0",
"jsonschema": "^1.4.0",
"mongoose": "^5.12.7",
"nanoid": "^3.1.22",
"next": "^10.2.0",
"next": "^12.2.2",
"next-compose-plugins": "^2.2.1",
"next-pwa": "^5.5.4",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
Expand Down Expand Up @@ -50,6 +52,7 @@
]
},
"devDependencies": {
"@types/cheerio": "^0.22.31",
"@types/react-beautiful-dnd": "^13.1.2",
"typescript": "^4.7.4"
},
Expand Down
5 changes: 5 additions & 0 deletions public/images/UniPa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"start_url": "/",
"scope": "/",
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#BEBEBE",
"icons": [
{
"src": "/images/manifest-icon-192.maskable.png",
Expand Down
2 changes: 2 additions & 0 deletions src/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Flex, FlexProps, SimpleGrid } from '@chakra-ui/layout';
import ImportLectures from '../features/importLectures/ImportLectures';
import AddLectureButton from '../features/lectures/AddLectureButton';
import ColorModeSwitcher from './ColorModeSwitcher';
// import CopyUrlButton from './CopyUrlButton';
Expand All @@ -10,6 +11,7 @@ export default function Header(props: FlexProps = {}) {
<Logo maxHeight="5em"/>
<SimpleGrid columns={1} gap={2}>
<ColorModeSwitcher />
<ImportLectures/>
{/* <CopyUrlButton allLectures={allLectures} options={options} averageBonus={averageBonus}/> */}
<AddLectureButton />
</SimpleGrid>
Expand Down
28 changes: 28 additions & 0 deletions src/features/importLectures/ImportLectures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Button, Menu, MenuButton, MenuItem, MenuList, useDisclosure } from "@chakra-ui/react"
import React from "react"
import { FaCloudDownloadAlt } from "react-icons/fa"
import ModalFromUnipa, { UnipaLabel } from "./components/UniPa/ModalFromUnipa"

const ImportLecture: React.FC = () => {
const { isOpen: isOpenUnipa, onOpen: onOpenUnipa, onClose: onCloseUnipa } = useDisclosure()

return <>
<Menu isLazy={true}>
<MenuButton as={Button} leftIcon={<FaCloudDownloadAlt />}
size="sm"
fontSize="md"
variant="outline"
>
Importa Materie
</MenuButton>
<MenuList>
<MenuItem onClick={onOpenUnipa}>
<UnipaLabel />
</MenuItem>
</MenuList>
</Menu>
<ModalFromUnipa onOpen={onOpenUnipa} isOpen={isOpenUnipa} onClose={onCloseUnipa} />
</>
}

export default React.memo(ImportLecture)
221 changes: 221 additions & 0 deletions src/features/importLectures/components/UniPa/ModalFromUnipa.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { DownloadIcon, ExternalLinkIcon, LinkIcon } from "@chakra-ui/icons";
import { Button, ButtonGroup, IconButton, Input, InputGroup, InputLeftAddon, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Text, useToast, UseToastOptions, VStack } from "@chakra-ui/react";
import Image from "next/image";
import React, { useCallback, useReducer, useState } from "react";
import { FaPaste } from "react-icons/fa";
import { API_FETCH_UNIPA_URL, FetchFromUnipaResponse } from "../../../../pages/api/unipa/fetch";
import ResultTabs from "./ResultTabs";

const urlPattern = /^(https\:\/\/)?offertaformativa\.unipa\.it\/offweb\/public\/corso\/visualizzaCurriculum\.seam.*/;
const oidCurriculum = /oidCurriculum=(\d+)/;
const onlyDigits = /^\d{4,}$/

const linkToSearchPage = "https://offertaformativa.unipa.it/offweb/public/corso/ricercaSemplice.seam"
const examplePage = "https://offertaformativa.unipa.it/offweb/public/corso/visualizzaCurriculum.seam?oidCurriculum=18418"
const buildUrl = (oid: string | number) => `https://offertaformativa.unipa.it/offweb/public/corso/visualizzaCurriculum.seam?oidCurriculum=${oid}`

export const UnipaLabel = () => <><Image
src={"/images/UniPa.svg"}
alt={""}
width={25}
height={25}
/>
<Text ml={"1em"}> UniPa</Text>
</>

interface ModalState {
isLoading: boolean,
isDataLoaded: boolean,
hasLoadedOnce: boolean,
response?: FetchFromUnipaResponse
}
type ModalActionList = "fetching" | "fetched"
interface ModalAction {
type: ModalActionList;
payload?: FetchFromUnipaResponse
}

const reducer = (state: ModalState, action: ModalAction): ModalState => {
switch (action.type) {
case "fetching": {
return {
...state,
isLoading: true,
isDataLoaded: false,
response: undefined,
hasLoadedOnce: true
}
}
case "fetched": {
if (action.payload !== undefined) {
return {
...state,
response: action.payload,
isLoading: false,
isDataLoaded: true
}
}
}
default: {
return state
}
}
}

const ModalFromUnipa: React.FC<{ isOpen: boolean, onClose: () => void, onOpen: () => void }> = ({ isOpen, onClose, onOpen }) => {
const [url, setUrl] = useState("")
const [data, dispatch] = useReducer(reducer, {
isLoading: false,
isDataLoaded: false,
hasLoadedOnce: false
})
const toast = useToast()

const fetchFromUnipa = useCallback(async (input: string) => {
let oid: string
if (input.match(onlyDigits)) {
oid = input
} else {
if (input.match(urlPattern) === null || input.match(oidCurriculum) === null) {
toast({
title: 'Attenzione',
description: "Il testo inserito non è valido.",
status: 'warning',
duration: 1500,
isClosable: true,
position: "top",
variant: "left-accent"
})
return
}
const matches = input.match(oidCurriculum) as RegExpMatchArray
console.log(matches)
oid = matches[1]
}
dispatch({ type: "fetching" })
let result: FetchFromUnipaResponse = { error: "la richiesta non è andata a buon fine" }
try {
result = await (await fetch(`${API_FETCH_UNIPA_URL}?oidCurriculum=${oid}`, {
method: 'GET',
})).json() as FetchFromUnipaResponse
} finally {
dispatch({ type: "fetched", payload: result })
}
if ("error" in result) {
console.log(result.error)
}
else {
console.log(result.name)
}
console.log(result)
}, [])


const pasteClipboard = async (inp: string | undefined = undefined) => {
let toastMessage: UseToastOptions = {}
try {
if (inp === undefined && !(typeof navigator.clipboard.readText === "function")) {
// @ts-ignore
await navigator.permissions.query({ name: "clipboard-read" })
}
const content = inp ?? await navigator.clipboard.readText()
if ((content.match(urlPattern) !== null && content.match(oidCurriculum) !== null) || content.match(onlyDigits) !== null) {
setUrl(content)
toastMessage = {
title: 'Ottimo',
description: "Testo incollato correttamente",
status: 'success',
duration: 1000,
isClosable: true,
position: "top",
variant: "subtle"
}
}
else {
toastMessage = {
title: 'Attenzione',
description: "Il testo che vuoi incollare non è valido.",
status: 'warning',
duration: 1500,
isClosable: true,
position: "top",
variant: "subtle"
}
}
} catch {
toastMessage = {
title: 'Errore',
description: "Incolla manualmente",
status: 'error',
duration: 1500,
isClosable: true,
position: "top",
variant: "subtle"
}
}
toast(toastMessage);
}
return (
<>
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior={"inside"}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
UniPa
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text>
Cerca il tuo Corso di Studi{' '}
<Link target={"_blank"} textColor={"#0000EE"} href={linkToSearchPage} rel="nofollow">
in questa pagina <ExternalLinkIcon />
</Link>,
copia il link ed incollalo qui sotto. {" "}
<Text as="span" fontSize="xs" textColor={"gray"} ><Link href={examplePage} target="_blank" rel="nofollow">esempio <LinkIcon /></Link></Text>
</Text>
{/* <Center > */}
<VStack mt={"1em"}>
<InputGroup w={"100%"}>
<InputLeftAddon ><LinkIcon /></InputLeftAddon>
<Input placeholder={"https://offertaformativa.unipa.it/offweb/public/corso/visualizzaCurriculum.seam?oidCurriculum=11111"}
isInvalid={
url !== ""
&& (url.match(urlPattern) === null
&& url.match(oidCurriculum) === null
&& url.match(onlyDigits) === null)
}
type="url"
value={url}
variant="outline"
onChange={(e) => setUrl(e.target.value)}
onPaste={(e) => {
e.preventDefault();
e.stopPropagation();
const clipboardData = e.clipboardData;
const pastedData = clipboardData.getData('Text');
pasteClipboard(pastedData)
}}
onKeyUp={(e)=>{
if(e.key === 'Enter' || e.keyCode === 13){
fetchFromUnipa(url)
}
}}
/>
<IconButton colorScheme={"blue"} aria-label="incolla" icon={<FaPaste />} onClick={() => pasteClipboard()} />
</InputGroup>
<ButtonGroup>
<Button isLoading={data.isLoading} loadingText={"Importando..."} leftIcon={<DownloadIcon />} onClick={() => fetchFromUnipa(url)}>
Recupera le lezioni
</Button>
</ButtonGroup>
<ResultTabs result={data.response} closeModal={onClose}/>
</VStack>
{/* </Center> */}
</ModalBody>
</ModalContent>
</Modal>
</>
)
}

export default React.memo(ModalFromUnipa)
Loading