diff --git a/eslint.config.mjs b/eslint.config.mjs index 108a976..a323438 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ import typescriptEslintPlugin from "@typescript-eslint/eslint-plugin"; import typescriptEslintParser from "@typescript-eslint/parser"; import reactPlugin from "eslint-plugin-react"; +import reactCompilerPlugin from "eslint-plugin-react-compiler"; import reactHooksPlugin from "eslint-plugin-react-hooks"; import path from "path"; import { fileURLToPath } from "url"; @@ -14,6 +15,7 @@ export default [ plugins: { react: reactPlugin, "react-hooks": reactHooksPlugin, + "react-compiler": reactCompilerPlugin, "@typescript-eslint": typescriptEslintPlugin, }, languageOptions: { @@ -110,28 +112,24 @@ export default [ '@typescript-eslint/no-unused-vars': [ 'off' ], - '@typescript-eslint/ban-types': [ + '@typescript-eslint/no-restricted-types': [ 'error', { 'types': { - 'String': true, - 'Boolean': true, - 'Number': true, - 'Symbol': true, - '{}': false, - 'Object': false, - 'object': false, - 'Function': false - }, - 'extendDefaults': true + 'String': { message: 'Use string instead', fixWith: 'string' }, + 'Boolean': { message: 'Use boolean instead', fixWith: 'boolean' }, + 'Number': { message: 'Use number instead', fixWith: 'number' }, + 'Symbol': { message: 'Use symbol instead', fixWith: 'symbol' } + } } ], - 'react/react-in-jsx-scope': 'off' + 'react/react-in-jsx-scope': 'off', + 'react-compiler/react-compiler': 'warn' }, settings: { react: { - version: "18.3.1", + version: "19.2", }, }, }, diff --git a/package.json b/package.json index 13915c1..86c5898 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "framer-motion": "^12.38.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-error-boundary": "^6.1.1", "react-icons": "^5.5.0", "react-player": "^2.16.0", "use-between": "^1.4.0" @@ -36,8 +37,10 @@ "@typescript-eslint/eslint-plugin": "^8.59.1", "@typescript-eslint/parser": "^8.59.1", "@vitejs/plugin-react": "^6.0.1", + "babel-plugin-react-compiler": "^1.0.0", "eslint": "^10.2.1", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-hooks": "^7.1.1", "postcss": "^8.5.12", "postcss-nested": "^7.0.2", diff --git a/public/configuration/asset-loader.js b/public/configuration/asset-loader.js index 19e2307..7483733 100644 --- a/public/configuration/asset-loader.js +++ b/public/configuration/asset-loader.js @@ -44,6 +44,11 @@ return new URL(".", source); }; + const getDeployBase = () => { + try { return new URL("..", getBase()); } + catch { return new URL("/", location.href); } + }; + const withCacheBust = (url) => { url.searchParams.set("v", Date.now().toString(36)); return url; @@ -71,9 +76,14 @@ const resolveAssetCandidates = (path) => { const base = getBase(); + const deploy = getDeployBase(); const normalized = path.replace(/^\.\//, ""); const file = normalized.split("/").pop(); + const relative = normalized.replace(/^\//, ""); const urls = [ + new URL("src/assets/" + file, deploy), + new URL("assets/" + file, deploy), + new URL(relative, deploy), new URL("./src/assets/" + file, base), new URL("./assets/" + file, base), new URL("/src/assets/" + file, base.origin), @@ -205,7 +215,10 @@ const fetchManifest = async () => { const base = getBase(); + const deploy = getDeployBase(); const candidates = [ + new URL(".vite/manifest.json", deploy), + new URL("manifest.json", deploy), new URL(".vite/manifest.json", base.origin + "/"), new URL("manifest.json", base.origin + "/"), new URL(".vite/manifest.json", base), @@ -221,7 +234,11 @@ const json = await response.json(); if(json && typeof json === "object") { debug("loader: manifest from " + candidate.href); - return { manifest: json, base: new URL(".", candidate.href) }; + let manifestBase = new URL(".", candidate.href); + if(/\/\.vite\/manifest\.json$/.test(candidate.pathname)) { + manifestBase = new URL("..", manifestBase); + } + return { manifest: json, base: manifestBase }; } } catch {} } @@ -247,18 +264,24 @@ const resolveManifestPath = (manifestBase, file) => { if(/^https?:\/\//i.test(file)) return file; if(file.startsWith("/")) return file; - return new URL(file, manifestBase.origin + "/").pathname; + return new URL(file, manifestBase).pathname; }; const isLoaderUrl = (href) => /(?:^|\/)bootstrap\.js(?:$|\?|#)/i.test(href) || /(?:^|\/)asset-loader\.js(?:$|\?|#)/i.test(href); const fetchEntryFromIndexHtml = async () => { const base = getBase(); + const deploy = getDeployBase(); const candidates = [ + new URL("index.html", deploy), + new URL("./", deploy), new URL("/index.html", base.origin + "/"), new URL("/", base.origin + "/") ]; + const seen = new Set(); for(const candidate of candidates) { + if(seen.has(candidate.href)) continue; + seen.add(candidate.href); try { const response = await fetch(withCacheBust(new URL(candidate.href)), { cache: "no-store" }); if(!response.ok) continue; diff --git a/public/configuration/bootstrap.js b/public/configuration/bootstrap.js index 997602f..13c1d8f 100644 --- a/public/configuration/bootstrap.js +++ b/public/configuration/bootstrap.js @@ -1,17 +1,4 @@ (() => { - const API_BASE = "http://localhost:2096"; - - const ensureMobileViewport = () => { - let viewport = document.querySelector('meta[name="viewport"]'); - if(!viewport) { - viewport = document.createElement("meta"); - viewport.name = "viewport"; - document.head.appendChild(viewport); - } - viewport.content = "width=device-width, initial-scale=1, viewport-fit=cover"; - }; - - ensureMobileViewport(); const FALLBACK_API_BASE = ""; const getBase = () => { diff --git a/src/api/ui-settings/UiSettingsContext.tsx b/src/api/ui-settings/UiSettingsContext.tsx index 99685ce..23357f8 100644 --- a/src/api/ui-settings/UiSettingsContext.tsx +++ b/src/api/ui-settings/UiSettingsContext.tsx @@ -215,9 +215,9 @@ export const UiSettingsProvider: FC = ({ children }) => }, [ settings ]); return ( - + { children } - + ); }; diff --git a/src/common/GridContext.tsx b/src/common/GridContext.tsx index 082d4be..1825131 100644 --- a/src/common/GridContext.tsx +++ b/src/common/GridContext.tsx @@ -1,4 +1,4 @@ -import { createContext, FC, ProviderProps, useContext } from 'react'; +import { createContext, FC, ReactNode, useContext } from 'react'; export interface IGridContext { @@ -9,9 +9,9 @@ const GridContext = createContext({ isCssGrid: false }); -export const GridContextProvider: FC> = props => +export const GridContextProvider: FC<{ value: IGridContext; children?: ReactNode }> = props => { - return { props.children }; + return { props.children }; }; export const useGridContext = () => useContext(GridContext); diff --git a/src/common/card/NitroCardContext.tsx b/src/common/card/NitroCardContext.tsx index c296b2a..8be7b98 100644 --- a/src/common/card/NitroCardContext.tsx +++ b/src/common/card/NitroCardContext.tsx @@ -1,4 +1,4 @@ -import { createContext, FC, ProviderProps, useContext } from 'react'; +import { createContext, FC, ReactNode, useContext } from 'react'; interface INitroCardContext { @@ -9,9 +9,9 @@ const NitroCardContext = createContext({ theme: null }); -export const NitroCardContextProvider: FC> = props => +export const NitroCardContextProvider: FC<{ value: INitroCardContext; children?: ReactNode }> = props => { - return { props.children }; + return { props.children }; }; export const useNitroCardContext = () => useContext(NitroCardContext); diff --git a/src/common/card/accordion/NitroCardAccordionContext.tsx b/src/common/card/accordion/NitroCardAccordionContext.tsx index 5e65c30..d8c93b6 100644 --- a/src/common/card/accordion/NitroCardAccordionContext.tsx +++ b/src/common/card/accordion/NitroCardAccordionContext.tsx @@ -1,4 +1,4 @@ -import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react'; +import { createContext, Dispatch, FC, ReactNode, SetStateAction, useContext } from 'react'; export interface INitroCardAccordionContext { @@ -13,9 +13,9 @@ const NitroCardAccordionContext = createContext({ closeAll: null }); -export const NitroCardAccordionContextProvider: FC> = props => +export const NitroCardAccordionContextProvider: FC<{ value: INitroCardAccordionContext; children?: ReactNode }> = props => { - return ; + return ; }; export const useNitroCardAccordionContext = () => useContext(NitroCardAccordionContext); diff --git a/src/components/ads/GoogleAdsView.tsx b/src/components/ads/GoogleAdsView.tsx index 4b65295..0aaf159 100644 --- a/src/components/ads/GoogleAdsView.tsx +++ b/src/components/ads/GoogleAdsView.tsx @@ -9,8 +9,6 @@ interface AdsenseConfig { fullWidthResponsive?: boolean; } -const ADSENSE_SCRIPT_ID = 'google-adsense-script'; - const parsePublisherIdFromAdsTxt = (text: string): string | null => { for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.split('#')[0].trim(); @@ -24,18 +22,6 @@ const parsePublisherIdFromAdsTxt = (text: string): string | null => { return null; }; -const ensureAdsenseScript = (publisherId: string): void => { - if (typeof document === 'undefined') return; - if (document.getElementById(ADSENSE_SCRIPT_ID)) return; - - const script = document.createElement('script'); - script.id = ADSENSE_SCRIPT_ID; - script.async = true; - script.crossOrigin = 'anonymous'; - script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-${ publisherId }`; - document.head.appendChild(script); -}; - export const GoogleAdsView: FC<{}> = () => { const adsEnabled = GetConfigurationValue('show.google.ads', false); const [ isOpen, setIsOpen ] = useState(false); @@ -95,11 +81,6 @@ export const GoogleAdsView: FC<{}> = () => { return () => { cancelled = true; }; }, []); - useEffect(() => { - if (!isOpen || !publisherId || !config) return; - ensureAdsenseScript(publisherId); - }, [ isOpen, publisherId, config ]); - useEffect(() => { if (!isOpen) { pushedRef.current = false; @@ -138,6 +119,11 @@ export const GoogleAdsView: FC<{}> = () => { return ( + { publisherId && +