diff --git a/.eslintrc.js b/.eslintrc.js index 48a2963ff..9fdf58ddf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,10 @@ module.exports = { "@typescript-eslint/strict-boolean-expressions": 0, "@typescript-eslint/no-floating-promises": 0, "@typescript-eslint/prefer-nullish-coalescing": 0, - "no-useless-return": 0, "@typescript-eslint/restrict-template-expressions": 0, + "@typescript-eslint/promise-function-async": 0, + "@typescript-eslint/no-misused-promises": ["error", {"checksConditionals": false}], + "no-return-assign": 0, + "no-useless-return": 0, }, }; diff --git a/Rules b/Rules index eb01f5191..71e9e94cd 100644 --- a/Rules +++ b/Rules @@ -59,6 +59,11 @@ compile "/js/pages/TheSurahPage.tsx" do write "/js/pages/surah.js.gz" end +compile "/js/pages/TheSurahPage/package.ts" do + filter :webpack, exe: "./node_modules/webpack/bin/webpack.js" + write "/js/pages/TheSurahPage/package.js" +end + ## # /js/pages/redirect-to-random-surah.js compile "/js/pages/redirect-to-random-surah.ts" do diff --git a/package-lock.json b/package-lock.json index 625bb3960..897dcefa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "devDependencies": { + "@types/css-font-loading-module": "^0.0.7", "@types/react": "^18.0.18", "@types/react-dom": "^18.0.6", "classnames": "^2.3.2", @@ -186,6 +187,12 @@ "node": ">= 8" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", @@ -4506,6 +4513,12 @@ "fastq": "^1.6.0" } }, + "@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "dev": true + }, "@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", diff --git a/package.json b/package.json index 590c75f77..0eab34870 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "eslint": "node ./node_modules/eslint/bin/eslint.js src/" }, "devDependencies": { + "@types/css-font-loading-module": "^0.0.7", "@types/react": "^18.0.18", "@types/react-dom": "^18.0.6", "classnames": "^2.3.2", diff --git a/src/html/TheSurahPage.html.erb b/src/html/TheSurahPage.html.erb index 330a2dc61..c38b6325e 100644 --- a/src/html/TheSurahPage.html.erb +++ b/src/html/TheSurahPage.html.erb @@ -2,11 +2,49 @@ Al-Quran: Loading - + -
- +
+
+ + Loading... +
+
+
+
+ diff --git a/src/js/hooks/useSurah.ts b/src/js/hooks/useSurah.ts index 3dcaa5843..36af299db 100644 --- a/src/js/hooks/useSurah.ts +++ b/src/js/hooks/useSurah.ts @@ -5,11 +5,10 @@ export default function (locale: string, surahId: number) { const [surah, setSurah] = useState(null); useEffect(() => { - (async () => { - const res = await fetch(`/${locale}/${surahId}/surah.json`); - const json = await res.json(); - setSurah(Quran.Surah.fromJSON(json.shift(), json)); - })(); + const path = `/${locale}/${surahId}/surah.json`; + const text = document.querySelector(`script[src="${path}"]`).innerText; + const json = JSON.parse(text); + setSurah(Quran.Surah.fromJSON(json.shift(), json)); }, []); return { diff --git a/src/js/lib/WebPackage.ts b/src/js/lib/WebPackage.ts new file mode 100644 index 000000000..441625073 --- /dev/null +++ b/src/js/lib/WebPackage.ts @@ -0,0 +1,49 @@ +import { + WebPackage, + PackageSpec, + Package, + ReporterFunction, +} from "./WebPackage/types"; +import FontLoader from "./WebPackage/FontLoader"; +import ImageLoader from "./WebPackage/ImageLoader"; +import CSSLoader from "./WebPackage/CSSLoader"; +import ScriptLoader from "./WebPackage/ScriptLoader"; +import OtherLoader from "./WebPackage/OtherLoader"; + +export default function (pkgspec: PackageSpec): WebPackage { + const self: WebPackage = Object.create(null); + const pkg: Package = {fonts: [], images: [], stylesheets: [], scripts: [], others: []}; + const { fonts, images, stylesheets, scripts, others, onprogress } = pkgspec; + const total = [...fonts, ...images, ...stylesheets, ...scripts].length; + + let index = 0; + const reporter: ReporterFunction = (el) => { + index++; + if (onprogress && index <= total) { + onprogress(100 * (index / total)); + } + return el; + }; + + let fetcher: Promise | null = null; + self.fetch = () => { + if (fetcher) { + return fetcher; + } else { + fetcher = FontLoader(fonts, reporter) + .then((fonts: FontFace[]) => pkg.fonts.push(...fonts)) + .then(() => ImageLoader(images, reporter)) + .then((images: HTMLElement[]) => pkg.images.push(...images)) + .then(() => CSSLoader(stylesheets, reporter)) + .then((stylesheets: HTMLElement[]) => pkg.stylesheets.push(...stylesheets)) + .then(() => ScriptLoader(scripts, reporter)) + .then((scripts: HTMLElement[]) => pkg.scripts.push(...scripts)) + .then(() => OtherLoader(others, reporter)) + .then((others: HTMLElement[]) => pkg.others.push(...others)) + .then(() => pkg); + return fetcher; + } + }; + + return self; +} diff --git a/src/js/lib/WebPackage/CSSLoader.ts b/src/js/lib/WebPackage/CSSLoader.ts new file mode 100644 index 000000000..573e52b75 --- /dev/null +++ b/src/js/lib/WebPackage/CSSLoader.ts @@ -0,0 +1,15 @@ +import { ReporterFunction } from "./types"; + +export default function( + stylesheets: string[] | undefined, + reporter: ReporterFunction +) { + return Promise.all( + (stylesheets || []).map((href) => { + return fetch(href) + .then((res) => res.text()) + .then((innerText) => Object.assign(document.createElement("style"), {innerText})) + .then((el) => reporter(el)); + }) + ); +} diff --git a/src/js/lib/WebPackage/FontLoader.ts b/src/js/lib/WebPackage/FontLoader.ts new file mode 100644 index 000000000..c2311d01b --- /dev/null +++ b/src/js/lib/WebPackage/FontLoader.ts @@ -0,0 +1,12 @@ +import { ReporterFunction } from "./types"; + +export default function( + fonts: Array<[string, string]> | undefined, + reporter: ReporterFunction +) { + return Promise.all( + (fonts || []).map((font) => { + return new FontFace(...font).load().then((font) => reporter(font)); + }) + ); +} diff --git a/src/js/lib/WebPackage/ImageLoader.ts b/src/js/lib/WebPackage/ImageLoader.ts new file mode 100644 index 000000000..f4fb459ee --- /dev/null +++ b/src/js/lib/WebPackage/ImageLoader.ts @@ -0,0 +1,17 @@ +import { ReporterFunction } from "./types"; + +export default function( + images: string[] | undefined, + reporter: ReporterFunction +) { + return Promise.all( + (images || []).map((src) => { + return new Promise((resolve, reject) => { + const el = document.createElement("img"); + el.onload = () => resolve(el); + el.onerror = reject; + el.src = src; + }).then((el) => reporter(el)); + }) + ); +} diff --git a/src/js/lib/WebPackage/OtherLoader.ts b/src/js/lib/WebPackage/OtherLoader.ts new file mode 100644 index 000000000..2cb04b00a --- /dev/null +++ b/src/js/lib/WebPackage/OtherLoader.ts @@ -0,0 +1,15 @@ +import { ReporterFunction } from "./types"; + +export default function( + others: string[] | undefined, + reporter: ReporterFunction +) { + return Promise.all( + (others || []).map((src) => { + return fetch(src) + .then((res) => res.text()) + .then((text) => Object.assign(document.createElement("script"), {type: "text/plain", src, text})) + .then((el) => reporter(el)); + }) + ); +} diff --git a/src/js/lib/WebPackage/ScriptLoader.ts b/src/js/lib/WebPackage/ScriptLoader.ts new file mode 100644 index 000000000..b7e8d5216 --- /dev/null +++ b/src/js/lib/WebPackage/ScriptLoader.ts @@ -0,0 +1,15 @@ +import { ReporterFunction } from "./types"; + +export default function( + scripts: string[] | undefined, + reporter: ReporterFunction +) { + return Promise.all( + (scripts || []).map((src) => { + return fetch(src) + .then((res) => res.text()) + .then((text) => Object.assign(document.createElement("script"), {type: "application/javascript", text})) + .then((el) => reporter(el)); + }) + ); +} diff --git a/src/js/lib/WebPackage/types.ts b/src/js/lib/WebPackage/types.ts new file mode 100644 index 000000000..8cd9b492d --- /dev/null +++ b/src/js/lib/WebPackage/types.ts @@ -0,0 +1,23 @@ +export interface WebPackage { + fetch: () => Promise, +} + +export interface Package { + scripts: HTMLElement[], + stylesheets: HTMLElement[], + images: HTMLElement[], + fonts: FontFace[], + others: HTMLElement[] +} + +export interface PackageSpec { + scripts?: string[], + stylesheets?: string[], + images?: string[], + fonts?: Array<[string, string]>, + others?: string[], + onprogress?: (percent: number) => any +} + +export type PackageItem = HTMLElement | FontFace; +export type ReporterFunction = (el: PackageItem) => PackageItem; diff --git a/src/js/pages/TheSurahPage/package.ts b/src/js/pages/TheSurahPage/package.ts new file mode 100644 index 000000000..534bec6c1 --- /dev/null +++ b/src/js/pages/TheSurahPage/package.ts @@ -0,0 +1,30 @@ +import WebPackage from "lib/WebPackage"; + +(function() { + const parent: HTMLElement = document.querySelector(".webpackage.loader"); + const progressBar: HTMLProgressElement = parent.querySelector("progress"); + const progressNumber: HTMLSpanElement = parent.querySelector(".percentage"); + const { locale, surahId } = document.querySelector(".surah").dataset; + + WebPackage({ + scripts: ["/js/pages/surah.js"], + stylesheets: ["/css/surah.css"], + images: ["/images/moon.svg", "/images/leaf.svg"], + others: [`/${locale}/${surahId}/surah.json`], + fonts: [ + ["Kanit Regular", "url(/fonts/kanit-regular.ttf)"], + ["Roboto Mono Regular", "url(/fonts/roboto-mono-regular.ttf)"] + ], + onprogress: (percent: number) => { + progressBar.value = percent; + progressNumber.innerText = `${percent.toFixed(0)}%`; + } + }).fetch() + .then((pkg) => { + parent.remove(); + pkg.fonts.forEach((f) => document.fonts.add(f)); + pkg.stylesheets.forEach((s) => document.head.appendChild(s)); + pkg.others.forEach((o) => document.body.appendChild(o)); + pkg.scripts.forEach((s) => document.body.appendChild(s)); + }); +})();