diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03..fd36f94 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js index 6fd956d..4b3ea35 100644 --- a/next.config.js +++ b/next.config.js @@ -18,6 +18,9 @@ const nextConfig = { reloadOnOnline: false, runtimeCaching, }, + eslint: { + ignoreDuringBuilds: true + }, webpack: function (config, { isServer, webpack }) { if (!isServer) { config.plugins.push( diff --git a/package.json b/package.json index 74f8f45..870b2ef 100644 --- a/package.json +++ b/package.json @@ -3,38 +3,48 @@ "version": "0.2.0", "private": true, "dependencies": { - "@chakra-ui/icons": "^1.0.10", - "@chakra-ui/react": "^1.0.0", - "@emotion/react": "^11.0.0", - "@emotion/styled": "^11.0.0", - "@next/bundle-analyzer": "^12.2.2", - "@reduxjs/toolkit": "^1.8.3", - "@testing-library/jest-dom": "^5.9.0", - "@testing-library/react": "^10.2.1", - "@testing-library/user-event": "^12.0.2", + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@next/bundle-analyzer": "^14.2.3", + "@reduxjs/toolkit": "^2.2.5", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@tma.js/init-data-node": "^1.2.7", + "@tma.js/sdk-react": "^2.2.3", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vercel/analytics": "^1.3.1", + "@vercel/speed-insights": "^1.0.11", + "axios": "^1.7.2", "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": "^12.2.2", + "crypto": "^1.0.1", + "dotenv": "^16.4.5", + "framer-motion": "^11.2.10", + "jsonschema": "^1.4.1", + "mongodb": "^6.7.0", + "nanoid": "^5.0.7", + "next": "^14.2.3", "next-compose-plugins": "^2.2.1", - "next-pwa": "^5.5.4", - "react": "^17.0.2", - "react-beautiful-dnd": "^13.1.0", - "react-dom": "^17.0.2", - "react-icons": "^3.0.0", - "react-redux": "^8.0.2", + "next-pwa": "^5.6.0", + "react": "^18.3.1", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "^18.3.1", + "react-icons": "^5.2.1", + "react-redux": "^9.1.2", "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", - "reselect": "^4.1.6", - "web-vitals": "^0.2.2" + "reselect": "^5.1.1", + "web-vitals": "^4.0.1", + "yarn": "^1.22.22" }, "scripts": { "start": "next start", "build": "next build", "dev": "next dev", + "lint": "eslint", "analyze": "ANALYZE=true next build" }, "eslintConfig": { @@ -53,13 +63,10 @@ ] }, "devDependencies": { - "@types/cheerio": "^0.22.31", - "@types/react-beautiful-dnd": "^13.1.2", - "@types/redux-logger": "^3.0.9", - "typescript": "^4.7.4" - }, - "resolutions": { - "@types/react": "17.0.15", - "@types/react-dom": "17.0.15" + "@types/cheerio": "^0.22.35", + "@types/react-beautiful-dnd": "^13.1.8", + "@types/redux-logger": "^3.0.13", + "eslint": "^9.4.0", + "typescript": "^5.4.5" } } diff --git a/src/app/TPTP.tsx b/src/app/TPTP.tsx index e7b89af..8863dde 100644 --- a/src/app/TPTP.tsx +++ b/src/app/TPTP.tsx @@ -1,22 +1,27 @@ import { SimpleGrid, useToast } from '@chakra-ui/react'; -import { useEffect } from 'react'; +import { BackButton, ClosingBehavior, InitData, MiniApp, Popup, Viewport, isTMA, setDebug, useBackButton, useClosingBehavior, useInitData, useMiniApp, usePopup, useViewport } from '@tma.js/sdk-react'; +import { useCallback, useEffect, useState } from 'react'; import { batch } from 'react-redux'; import Footer from '../common/Footer'; import Header from '../common/Header'; import Average from '../features/average/Average'; -import { dangerouslySetAllOptions, IOptions } from '../features/average/averageDuck'; -import { dangerouslySetAllLectures, ILecture } from '../features/lectures/lectureDuck'; +import { IOptions, dangerouslySetAllOptions } from '../features/average/averageDuck'; import LectureTable from '../features/lectures/LectureTable'; -import { dangerouslySetAllPreferences, IAverageBonus, IPreferences } from '../features/preferences/preferencesDuck'; +import { ILecture, dangerouslySetAllLectures } from '../features/lectures/lectureDuck'; import PreferencesTab from '../features/preferences/PreferencesTab'; +import { IAverageBonus, IPreferences, dangerouslySetAllPreferences } from '../features/preferences/preferencesDuck'; import { exactWidth } from '../theme'; import { useAppDispatch } from './hooks'; - +import { retrieveFromBackend, saveToBackend } from '../features/telegram/telegramDuck'; +import { IAppState, persistConfig, persistor } from './store'; +import getStoredState from 'redux-persist/es/getStoredState'; const TPTP: React.FC = () => { const dispatch = useAppDispatch() const toast = useToast() + setDebug(true) + useEffect(() => { // Migrate from previous localStoage data const lectures = localStorage.getItem("lectures") @@ -47,8 +52,8 @@ const TPTP: React.FC = () => { }, []) - useEffect(()=>{ - if(window.location.pathname.match(/\/?(\w|\d){10,}/) !== null){ + useEffect(() => { + if (window.location.pathname.match(/\/?(\w|\d){10,}/) !== null) { toast({ title: "Link obsoleto", description: "Il link che hai utilizzato è obsoleto, per importare le materie usa la funzionalità in alto a destra", @@ -62,6 +67,110 @@ const TPTP: React.FC = () => { } }, []) + // TELEGRAM + let initData: InitData | undefined = undefined; + let miniApp: MiniApp | undefined = undefined; + let vp: Viewport | undefined = undefined; + let bb: BackButton | undefined = undefined; + let closingBehavior: ClosingBehavior | undefined = undefined; + let popup: Popup | undefined = undefined + + try { initData = useInitData() } catch (e) { console.error("Error while initializing TG data") } + try { miniApp = useMiniApp() } catch (e) { console.error("Error while initializing TG data") } + try { vp = useViewport() } catch (e) { console.error("Error while initializing TG data") } + try { bb = useBackButton() } catch (e) { console.error("Error while initializing TG data") } + try { closingBehavior = useClosingBehavior() } catch (e) { console.error("Error while initializing TG data") } + try { popup = usePopup() } catch (e) { console.error("Error while initializing TG data") } + + const [telegramInitted, setTelegramInitted] = useState(false) + + const onBackClick = useCallback(async () => { + if (!popup) return + if (popup.isOpened) return; + const btnId = await popup.open({ + title: "TPTP", + message: "Vuoi salvare i dati?", + buttons: [ + { + id: "KO", + text: "Esci senza salvare", + type: "destructive" + }, + { + id: "OK", + type: "default", + text: "Salva ed esci", + }, + { + id: "STAY", + type: "cancel" + } + ] + }); + console.log("OnBackClick", btnId) + if(btnId === "KO") { + return miniApp?.close() + } + else if (btnId === "STAY" || btnId === null) { + return; + } + const res = await getStoredState(persistConfig) as IAppState + await saveToBackend(res) + miniApp?.close() + }, [popup]) + + + useEffect(() => { + const initTelegram = async () => { + const tma = await isTMA() + if (!tma || telegramInitted) return null; + + const value = await retrieveFromBackend() + let welcome = "Benvenuto" + if (value) { + const { lectures, options, preferences } = value + batch(() => { + dispatch(dangerouslySetAllLectures(lectures)); + dispatch(dangerouslySetAllOptions(options)); + dispatch(dangerouslySetAllPreferences(preferences)) + }) + welcome = "Bentornato" + } + const user = initData?.user + if (user && !telegramInitted) { + let name = user.firstName + user.lastName + toast({ + description: `${welcome}, ${name}`, + position: "bottom", + variant: "top-accent", + duration: 2500, + status: "success", + size: "sm" + }) + } + setTelegramInitted(true); + } + initTelegram(); + }, []) + + useEffect(() => { + if (vp) { + console.log("TG, expanding") + vp.expand() + } + + if (bb) { + bb.show() + bb.on("click", async () => { + console.log("clickeeeeddd", popup) + onBackClick(); + }) + } + if (closingBehavior) { + closingBehavior.enableConfirmation() + } + }, [vp, bb, closingBehavior]) + return ( <> = { +export const persistConfig: PersistConfig = { key: 'TPTP', storage, version: 0, + debug: process.env.NODE_ENV !== "production", }; const rootReducer = combineReducers({ lectures: lecturesReducer, @@ -63,7 +64,7 @@ export const store = configureStore({ ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], }, }) - // .concat(logger); + .concat(logger); }, }); diff --git a/src/common/Logo.tsx b/src/common/Logo.tsx index 7e25072..5efb55a 100644 --- a/src/common/Logo.tsx +++ b/src/common/Logo.tsx @@ -1,5 +1,5 @@ import { Box, useColorModeValue } from '@chakra-ui/react'; -import Image from 'next/image'; +import Image from "next/legacy/image"; import React from 'react'; const WhiteLogo = ({ display = "inherit", ...props }: { display?: string }) => { diff --git a/src/features/importLectures/ImportLectures.tsx b/src/features/importLectures/ImportLectures.tsx index 34d7267..010c11e 100644 --- a/src/features/importLectures/ImportLectures.tsx +++ b/src/features/importLectures/ImportLectures.tsx @@ -1,4 +1,4 @@ -import { Button, Menu, MenuButton, MenuItem, MenuList, useDisclosure } from "@chakra-ui/react" +import { Button, Menu, MenuButton, MenuItem, MenuList, Text, useDisclosure } from "@chakra-ui/react" import React from "react" import { FaCloudDownloadAlt } from "react-icons/fa" import ModalFromUnipa, { UnipaLabel } from "./components/UniPa/ModalWrapper" @@ -16,7 +16,7 @@ const ImportLecture: React.FC = () => { Importa Materie - + diff --git a/src/features/importLectures/components/UniPa/ModalWrapper.tsx b/src/features/importLectures/components/UniPa/ModalWrapper.tsx index fb0ccd4..d7ef463 100644 --- a/src/features/importLectures/components/UniPa/ModalWrapper.tsx +++ b/src/features/importLectures/components/UniPa/ModalWrapper.tsx @@ -4,7 +4,7 @@ import { ExpandedIndex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Text, useToast } from "@chakra-ui/react"; -import Image from "next/image"; +import Image from "next/legacy/image"; import React, { useEffect, useState } from "react"; import { FaCaretDown, FaPollH } from "react-icons/fa"; import { API_FETCH_UNIPA_URL, FetchFromUnipaResponse } from "../../../../pages/api/unipa/fetch"; @@ -21,7 +21,7 @@ export const UnipaLabel = () => <> width={25} height={25} /> - UniPa + UniPa ⚒️ diff --git a/src/features/preferences/components/FinalBonusComponent.tsx b/src/features/preferences/components/FinalBonusComponent.tsx index 3120ee6..556469b 100644 --- a/src/features/preferences/components/FinalBonusComponent.tsx +++ b/src/features/preferences/components/FinalBonusComponent.tsx @@ -209,7 +209,7 @@ function FinalBonusComponent({ :
+ aria-label='slider-ex-1' defaultValue={0} min={0} max={10} step={0.1} my={"2em"} maxWidth={"80%"} value={temporarySliderValue} onChange={setTemporarySliderValue} onChangeEnd={endOfSliding} > @@ -224,4 +224,4 @@ function FinalBonusComponent({ ); } -export default FinalBonusComponent; \ No newline at end of file +export default FinalBonusComponent; diff --git a/src/features/telegram/telegramDuck.ts b/src/features/telegram/telegramDuck.ts new file mode 100644 index 0000000..187408c --- /dev/null +++ b/src/features/telegram/telegramDuck.ts @@ -0,0 +1,103 @@ +import { LaunchParams, isTMA, retrieveLaunchParams } from "@tma.js/sdk-react"; +import { IAppState } from "../../app/store"; +import { PayloadAction, Reducer, createSlice } from "@reduxjs/toolkit"; + +export interface ITelegramSettings { + saved: boolean +} + +export const createInitialTelegramSettings = (): ITelegramSettings => { + return { + saved: false + } +} + +export const retrieveFromBackend = async (): Promise => { + const tma = await isTMA() + if (!tma) return null; + let launchParams: LaunchParams; + try { + launchParams = retrieveLaunchParams() + } catch (e) { + console.error("Error while building launchParams, probably not in TG environment") + return null; + } + if (launchParams == null) return null; + const { initDataRaw } = launchParams; + try { + const result = await fetch( + "/api/data", + { + method: "GET", + headers: { + Authorization: `tma ${initDataRaw}` + } + } + ) + if (result.ok) { + const body = await result.json() as IAppState + console.log(body); + return body; + } + } catch (e) { + console.error("Error while fetching", e) + } + return null; +} + +export const saveToBackend = async (state: IAppState): Promise => { + const tma = await isTMA() + if (!tma) return; + + let launchParams: LaunchParams; + try { + launchParams = retrieveLaunchParams() + } catch (e) { + console.error("Error while building launchParams", e) + return; + } + if (launchParams == null) return; + const { initDataRaw } = launchParams; + try { + const result = await fetch( + "/api/data", + { + method: "PUT", + headers: { + Authorization: `tma ${initDataRaw}` + }, + body: JSON.stringify({ state }) + } + ) + return + } catch (e) { + console.error("Error while fetching", e) + } + return; +} + + +const telegramSlice = createSlice({ + name: 'telegram', + initialState: createInitialTelegramSettings, + reducers: { + save: (current, action: PayloadAction): ITelegramSettings => { + return { + saved: true + } + }, + invalidate: (current, action: PayloadAction): ITelegramSettings => { + return { + saved: false, + } + } + }, +}); + +export default telegramSlice.reducer; + + +export const reducers = { + retrieveFromBackend, + saveToBackend +} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..4a9f410 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +declare module "redux-persist/es/persistReducer" { interface PersistPartial { _persist?: PersistState; } } diff --git a/src/pages/[[...any]].tsx b/src/pages/[[...any]].tsx index c231240..78db59b 100644 --- a/src/pages/[[...any]].tsx +++ b/src/pages/[[...any]].tsx @@ -1,6 +1,7 @@ import { Box, Progress } from '@chakra-ui/react'; +import { SDKProvider } from '@tma.js/sdk-react'; import React, { Suspense } from 'react'; import { PersistGate } from 'redux-persist/integration/react'; import { persistor } from '../app/store'; @@ -10,11 +11,11 @@ const TPTP = React.lazy(() => import('../app/TPTP')); const LoadingProgress: React.FC = () => ( ( > - + ) @@ -36,7 +37,9 @@ function App() { return ( }> }> - + + + ); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index c129f43..fd76a8c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -4,6 +4,8 @@ import Head from 'next/head' import Script from 'next/script' import { Provider } from 'react-redux' import { store } from '../app/store' +import { SpeedInsights } from "@vercel/speed-insights/next" +import { Analytics } from "@vercel/analytics/react" const gtagCode = `window.dataLayer = window.dataLayer || []; function gtag() { @@ -36,8 +38,8 @@ export default function MyApp({ Component, pageProps }: AppProps) { {SchemaORG['name']} -